refactor: replace legacy monolithic code with clean architecture
- Remove old cmd/, internal/config, container, env, events, logging, middleware, proxy, registry packages - Add new clean architecture structure: - internal/domain: core domain types and errors - internal/boundaries: input/output port interfaces - internal/usecase: application business logic - internal/adapters/in: HTTP handlers, CLI, middleware - internal/adapters/out: Docker, filesystem, eventbus, secrets - internal/app: dependency injection and wiring - Add mockery configuration and generated mocks - Add 165 unit and integration tests across all layers
- commit
- f4529175d61ab06ece43bc034a118026e6066e9b
- author
- brice · · 2026-01-12 21:01 UTC
- committer
- brice · · 2026-01-12 21:58 UTC
- parent
- 0453d8d
- signature
- unsigned
98 files · +13754 -16440
A .mockery.yaml +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | template: testify | |
| 2 | + | formatter: goimports | |
| 3 | + | force-file-write: true | |
| 4 | + | dir: '{{.InterfaceDir}}/mocks' | |
| 5 | + | filename: 'mock_{{.InterfaceName | snakecase}}.go' | |
| 6 | + | structname: 'Mock{{.InterfaceName}}' | |
| 7 | + | pkgname: 'mocks' | |
| 8 | + | ||
| 9 | + | packages: | |
| 10 | + | gordon/internal/boundaries/out: | |
| 11 | + | interfaces: | |
| 12 | + | ContainerRuntime: | |
| 13 | + | BlobStorage: | |
| 14 | + | ManifestStorage: | |
| 15 | + | EventBus: | |
| 16 | + | EventPublisher: | |
| 17 | + | EventSubscriber: | |
| 18 | + | EventHandler: | |
| 19 | + | SecretProvider: | |
| 20 | + | EnvLoader: | |
| 21 | + | gordon/internal/boundaries/in: | |
| 22 | + | interfaces: | |
| 23 | + | ContainerService: | |
| 24 | + | RegistryService: | |
| 25 | + | ProxyService: | |
| 26 | + | ConfigService: | |
D cmd/reload.go +0 -85
| @@ -1,85 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "os" | |
| 6 | - | "path/filepath" | |
| 7 | - | "syscall" | |
| 8 | - | ||
| 9 | - | "github.com/spf13/cobra" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | var reloadCmd = &cobra.Command{ | |
| 13 | - | Use: "reload", | |
| 14 | - | Short: "Reload configuration and environment variables into running containers", | |
| 15 | - | Long: `Reload the Gordon configuration and environment variables from .env files into running containers. | |
| 16 | - | This command reloads the config file and redeploys all containers with updated environment | |
| 17 | - | variables from their respective .env files. Containers are recreated to pick up the new configuration and environment variables.`, | |
| 18 | - | Run: runReload, | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | func init() { | |
| 22 | - | rootCmd.AddCommand(reloadCmd) | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | func runReload(cmd *cobra.Command, args []string) { | |
| 26 | - | // Find the Gordon PID file to get the process ID | |
| 27 | - | pidFile := findGordonPidFile() | |
| 28 | - | if pidFile == "" { | |
| 29 | - | fmt.Println("Error: Gordon PID file not found. Is Gordon running?") | |
| 30 | - | fmt.Println("Please start Gordon with 'gordon start' first.") | |
| 31 | - | return | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // Read the PID from the file | |
| 35 | - | pidBytes, err := os.ReadFile(pidFile) | |
| 36 | - | if err != nil { | |
| 37 | - | fmt.Printf("Error reading Gordon PID file: %v\n", err) | |
| 38 | - | return | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | var pid int | |
| 42 | - | if _, err := fmt.Sscanf(string(pidBytes), "%d", &pid); err != nil { | |
| 43 | - | fmt.Printf("Error parsing Gordon PID: %v\n", err) | |
| 44 | - | return | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | // Check if the process is still running | |
| 48 | - | process, err := os.FindProcess(pid) | |
| 49 | - | if err != nil { | |
| 50 | - | fmt.Printf("Error finding Gordon process: %v\n", err) | |
| 51 | - | return | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | // Send SIGUSR1 signal to trigger reload | |
| 55 | - | if err := process.Signal(syscall.SIGUSR1); err != nil { | |
| 56 | - | fmt.Printf("Error sending reload signal to Gordon: %v\n", err) | |
| 57 | - | fmt.Println("Gordon may not be running or may not have permission to send signals.") | |
| 58 | - | return | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | fmt.Println("Manual reload signal sent to Gordon successfully.") | |
| 62 | - | fmt.Println("Check Gordon logs to see the reload progress.") | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | func findGordonPidFile() string { | |
| 66 | - | // Look for PID file in common locations | |
| 67 | - | locations := []string{ | |
| 68 | - | "/tmp/gordon.pid", | |
| 69 | - | "/var/run/gordon.pid", | |
| 70 | - | filepath.Join(os.TempDir(), "gordon.pid"), | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | // Also check in user's home directory | |
| 74 | - | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 75 | - | locations = append(locations, filepath.Join(homeDir, ".gordon.pid")) | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | for _, location := range locations { | |
| 79 | - | if _, err := os.Stat(location); err == nil { | |
| 80 | - | return location | |
| 81 | - | } | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | return "" | |
| 85 | - | } | |
D cmd/reload_test.go +0 -434
| @@ -1,434 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "fmt" | |
| 6 | - | "os" | |
| 7 | - | "path/filepath" | |
| 8 | - | "syscall" | |
| 9 | - | "testing" | |
| 10 | - | ||
| 11 | - | "github.com/spf13/cobra" | |
| 12 | - | "github.com/stretchr/testify/assert" | |
| 13 | - | "github.com/stretchr/testify/require" | |
| 14 | - | ) | |
| 15 | - | ||
| 16 | - | func TestReloadCommand(t *testing.T) { | |
| 17 | - | // Test reload command structure | |
| 18 | - | assert.Equal(t, "reload", reloadCmd.Use) | |
| 19 | - | assert.Contains(t, reloadCmd.Short, "Reload") | |
| 20 | - | assert.Contains(t, reloadCmd.Long, "configuration") | |
| 21 | - | assert.Contains(t, reloadCmd.Long, "environment variables") | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | func TestReloadCmdStructure(t *testing.T) { | |
| 25 | - | // Verify command is properly registered | |
| 26 | - | found := false | |
| 27 | - | for _, cmd := range rootCmd.Commands() { | |
| 28 | - | if cmd.Name() == "reload" { | |
| 29 | - | found = true | |
| 30 | - | break | |
| 31 | - | } | |
| 32 | - | } | |
| 33 | - | assert.True(t, found, "reload command should be registered with root command") | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | func TestReloadCmdHelp(t *testing.T) { | |
| 37 | - | var output bytes.Buffer | |
| 38 | - | ||
| 39 | - | // Create a copy of the command to avoid affecting the global command | |
| 40 | - | cmd := &cobra.Command{ | |
| 41 | - | Use: reloadCmd.Use, | |
| 42 | - | Short: reloadCmd.Short, | |
| 43 | - | Long: reloadCmd.Long, | |
| 44 | - | Run: reloadCmd.Run, | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | cmd.SetOut(&output) | |
| 48 | - | cmd.SetErr(&output) | |
| 49 | - | cmd.SetArgs([]string{"--help"}) | |
| 50 | - | ||
| 51 | - | err := cmd.Execute() | |
| 52 | - | assert.NoError(t, err) | |
| 53 | - | ||
| 54 | - | helpOutput := output.String() | |
| 55 | - | assert.Contains(t, helpOutput, "Reload") | |
| 56 | - | assert.Contains(t, helpOutput, "configuration") | |
| 57 | - | assert.Contains(t, helpOutput, "environment variables") | |
| 58 | - | assert.Contains(t, helpOutput, "containers") | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | func TestFindGordonPidFile(t *testing.T) { | |
| 62 | - | tests := []struct { | |
| 63 | - | name string | |
| 64 | - | setupFunc func(t *testing.T) (string, func()) // returns temp dir and cleanup func | |
| 65 | - | expectedFound bool | |
| 66 | - | }{ | |
| 67 | - | { | |
| 68 | - | name: "PID file exists in temp directory", | |
| 69 | - | setupFunc: func(t *testing.T) (string, func()) { | |
| 70 | - | tempDir := t.TempDir() | |
| 71 | - | pidFile := filepath.Join(tempDir, "gordon.pid") | |
| 72 | - | err := os.WriteFile(pidFile, []byte("12345"), 0644) | |
| 73 | - | require.NoError(t, err) | |
| 74 | - | ||
| 75 | - | cleanup := func() { | |
| 76 | - | // Cleanup function for consistency | |
| 77 | - | } | |
| 78 | - | return pidFile, cleanup | |
| 79 | - | }, | |
| 80 | - | expectedFound: false, // Won't find it because we can't easily mock os.TempDir | |
| 81 | - | }, | |
| 82 | - | { | |
| 83 | - | name: "PID file exists in home directory", | |
| 84 | - | setupFunc: func(t *testing.T) (string, func()) { | |
| 85 | - | // Create temp directory to simulate home | |
| 86 | - | tempDir := t.TempDir() | |
| 87 | - | pidFile := filepath.Join(tempDir, ".gordon.pid") | |
| 88 | - | err := os.WriteFile(pidFile, []byte("12345"), 0644) | |
| 89 | - | require.NoError(t, err) | |
| 90 | - | ||
| 91 | - | // Mock home directory | |
| 92 | - | originalHome := os.Getenv("HOME") | |
| 93 | - | os.Setenv("HOME", tempDir) | |
| 94 | - | ||
| 95 | - | cleanup := func() { | |
| 96 | - | os.Setenv("HOME", originalHome) | |
| 97 | - | } | |
| 98 | - | return pidFile, cleanup | |
| 99 | - | }, | |
| 100 | - | expectedFound: true, | |
| 101 | - | }, | |
| 102 | - | { | |
| 103 | - | name: "no PID file exists", | |
| 104 | - | setupFunc: func(t *testing.T) (string, func()) { | |
| 105 | - | // Set home to a temp directory with no PID file | |
| 106 | - | tempDir := t.TempDir() | |
| 107 | - | originalHome := os.Getenv("HOME") | |
| 108 | - | os.Setenv("HOME", tempDir) | |
| 109 | - | ||
| 110 | - | cleanup := func() { | |
| 111 | - | os.Setenv("HOME", originalHome) | |
| 112 | - | } | |
| 113 | - | return "", cleanup | |
| 114 | - | }, | |
| 115 | - | expectedFound: false, | |
| 116 | - | }, | |
| 117 | - | } | |
| 118 | - | ||
| 119 | - | for _, tt := range tests { | |
| 120 | - | t.Run(tt.name, func(t *testing.T) { | |
| 121 | - | expectedPidFile, cleanup := tt.setupFunc(t) | |
| 122 | - | defer cleanup() | |
| 123 | - | ||
| 124 | - | result := findGordonPidFile() | |
| 125 | - | ||
| 126 | - | if tt.expectedFound { | |
| 127 | - | assert.NotEmpty(t, result) | |
| 128 | - | assert.Equal(t, expectedPidFile, result) | |
| 129 | - | } else { | |
| 130 | - | assert.Empty(t, result) | |
| 131 | - | } | |
| 132 | - | }) | |
| 133 | - | } | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | func TestRunReload_NoPidFile(t *testing.T) { | |
| 137 | - | // Set up environment with no PID file | |
| 138 | - | tempDir := t.TempDir() | |
| 139 | - | originalHome := os.Getenv("HOME") | |
| 140 | - | os.Setenv("HOME", tempDir) | |
| 141 | - | defer os.Setenv("HOME", originalHome) | |
| 142 | - | ||
| 143 | - | // Capture stdout by redirecting it | |
| 144 | - | oldStdout := os.Stdout | |
| 145 | - | r, w, _ := os.Pipe() | |
| 146 | - | os.Stdout = w | |
| 147 | - | ||
| 148 | - | done := make(chan string) | |
| 149 | - | go func() { | |
| 150 | - | var buf bytes.Buffer | |
| 151 | - | buf.ReadFrom(r) | |
| 152 | - | done <- buf.String() | |
| 153 | - | }() | |
| 154 | - | ||
| 155 | - | // Run reload command | |
| 156 | - | runReload(reloadCmd, []string{}) | |
| 157 | - | ||
| 158 | - | // Restore stdout and get output | |
| 159 | - | w.Close() | |
| 160 | - | os.Stdout = oldStdout | |
| 161 | - | outputStr := <-done | |
| 162 | - | ||
| 163 | - | // Should indicate PID file not found | |
| 164 | - | assert.Contains(t, outputStr, "PID file not found") | |
| 165 | - | assert.Contains(t, outputStr, "Is Gordon running?") | |
| 166 | - | } | |
| 167 | - | ||
| 168 | - | func TestRunReload_WithValidPidFile(t *testing.T) { | |
| 169 | - | // Create a test PID file | |
| 170 | - | tempDir := t.TempDir() | |
| 171 | - | pidFile := filepath.Join(tempDir, ".gordon.pid") | |
| 172 | - | ||
| 173 | - | // Write current process PID to file (so it's valid) | |
| 174 | - | pid := os.Getpid() | |
| 175 | - | err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", pid)), 0644) | |
| 176 | - | require.NoError(t, err) | |
| 177 | - | ||
| 178 | - | // Mock home directory | |
| 179 | - | originalHome := os.Getenv("HOME") | |
| 180 | - | os.Setenv("HOME", tempDir) | |
| 181 | - | defer os.Setenv("HOME", originalHome) | |
| 182 | - | ||
| 183 | - | // Capture stdout | |
| 184 | - | var output bytes.Buffer | |
| 185 | - | reloadCmd.SetOut(&output) | |
| 186 | - | ||
| 187 | - | // We can't easily test the actual signal sending to another process, | |
| 188 | - | // but we can test the PID file reading logic | |
| 189 | - | ||
| 190 | - | // Test PID file reading | |
| 191 | - | foundPidFile := findGordonPidFile() | |
| 192 | - | assert.Equal(t, pidFile, foundPidFile) | |
| 193 | - | ||
| 194 | - | // Test PID parsing | |
| 195 | - | pidBytes, err := os.ReadFile(pidFile) | |
| 196 | - | require.NoError(t, err) | |
| 197 | - | ||
| 198 | - | var parsedPid int | |
| 199 | - | _, err = fmt.Sscanf(string(pidBytes), "%d", &parsedPid) | |
| 200 | - | require.NoError(t, err) | |
| 201 | - | assert.Equal(t, pid, parsedPid) | |
| 202 | - | } | |
| 203 | - | ||
| 204 | - | func TestRunReload_InvalidPidFile(t *testing.T) { | |
| 205 | - | // Create a PID file with invalid content | |
| 206 | - | tempDir := t.TempDir() | |
| 207 | - | pidFile := filepath.Join(tempDir, ".gordon.pid") | |
| 208 | - | ||
| 209 | - | // Write invalid PID content | |
| 210 | - | err := os.WriteFile(pidFile, []byte("invalid_pid"), 0644) | |
| 211 | - | require.NoError(t, err) | |
| 212 | - | ||
| 213 | - | // Mock home directory | |
| 214 | - | originalHome := os.Getenv("HOME") | |
| 215 | - | os.Setenv("HOME", tempDir) | |
| 216 | - | defer os.Setenv("HOME", originalHome) | |
| 217 | - | ||
| 218 | - | // Capture stdout by redirecting it | |
| 219 | - | oldStdout := os.Stdout | |
| 220 | - | r, w, _ := os.Pipe() | |
| 221 | - | os.Stdout = w | |
| 222 | - | ||
| 223 | - | done := make(chan string) | |
| 224 | - | go func() { | |
| 225 | - | var buf bytes.Buffer | |
| 226 | - | buf.ReadFrom(r) | |
| 227 | - | done <- buf.String() | |
| 228 | - | }() | |
| 229 | - | ||
| 230 | - | // Run reload command | |
| 231 | - | runReload(reloadCmd, []string{}) | |
| 232 | - | ||
| 233 | - | // Restore stdout and get output | |
| 234 | - | w.Close() | |
| 235 | - | os.Stdout = oldStdout | |
| 236 | - | outputStr := <-done | |
| 237 | - | ||
| 238 | - | // Should indicate parsing error | |
| 239 | - | assert.Contains(t, outputStr, "Error parsing Gordon PID") | |
| 240 | - | } | |
| 241 | - | ||
| 242 | - | func TestRunReload_UnreadablePidFile(t *testing.T) { | |
| 243 | - | // Create a PID file that can't be read | |
| 244 | - | tempDir := t.TempDir() | |
| 245 | - | pidFile := filepath.Join(tempDir, ".gordon.pid") | |
| 246 | - | ||
| 247 | - | // Create file but make it unreadable (if possible) | |
| 248 | - | err := os.WriteFile(pidFile, []byte("12345"), 0000) | |
| 249 | - | require.NoError(t, err) | |
| 250 | - | ||
| 251 | - | // Mock home directory | |
| 252 | - | originalHome := os.Getenv("HOME") | |
| 253 | - | os.Setenv("HOME", tempDir) | |
| 254 | - | defer os.Setenv("HOME", originalHome) | |
| 255 | - | ||
| 256 | - | // Capture stdout | |
| 257 | - | var output bytes.Buffer | |
| 258 | - | reloadCmd.SetOut(&output) | |
| 259 | - | ||
| 260 | - | // Run reload command | |
| 261 | - | runReload(reloadCmd, []string{}) | |
| 262 | - | ||
| 263 | - | // Should indicate read error (on systems that enforce permissions) | |
| 264 | - | outputStr := output.String() | |
| 265 | - | // Note: This test might not fail on all systems due to permission handling | |
| 266 | - | if len(outputStr) > 0 { | |
| 267 | - | // If we get output, it should contain an error message | |
| 268 | - | assert.True(t, | |
| 269 | - | contains(outputStr, "Error reading") || | |
| 270 | - | contains(outputStr, "Error parsing") || | |
| 271 | - | contains(outputStr, "Error finding") || | |
| 272 | - | contains(outputStr, "Error sending"), | |
| 273 | - | "Should contain some error message, got: %s", outputStr) | |
| 274 | - | } | |
| 275 | - | } | |
| 276 | - | ||
| 277 | - | func TestRunReload_NonExistentProcess(t *testing.T) { | |
| 278 | - | // Create a PID file with a PID that doesn't exist | |
| 279 | - | tempDir := t.TempDir() | |
| 280 | - | pidFile := filepath.Join(tempDir, ".gordon.pid") | |
| 281 | - | ||
| 282 | - | // Use a PID that's very unlikely to exist (high number) | |
| 283 | - | nonExistentPid := 999999 | |
| 284 | - | err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", nonExistentPid)), 0644) | |
| 285 | - | require.NoError(t, err) | |
| 286 | - | ||
| 287 | - | // Mock home directory | |
| 288 | - | originalHome := os.Getenv("HOME") | |
| 289 | - | os.Setenv("HOME", tempDir) | |
| 290 | - | defer os.Setenv("HOME", originalHome) | |
| 291 | - | ||
| 292 | - | // Capture stdout by redirecting it | |
| 293 | - | oldStdout := os.Stdout | |
| 294 | - | r, w, _ := os.Pipe() | |
| 295 | - | os.Stdout = w | |
| 296 | - | ||
| 297 | - | done := make(chan string) | |
| 298 | - | go func() { | |
| 299 | - | var buf bytes.Buffer | |
| 300 | - | buf.ReadFrom(r) | |
| 301 | - | done <- buf.String() | |
| 302 | - | }() | |
| 303 | - | ||
| 304 | - | // Run reload command | |
| 305 | - | runReload(reloadCmd, []string{}) | |
| 306 | - | ||
| 307 | - | // Restore stdout and get output | |
| 308 | - | w.Close() | |
| 309 | - | os.Stdout = oldStdout | |
| 310 | - | outputStr := <-done | |
| 311 | - | ||
| 312 | - | // Should indicate process not found or signal sending error | |
| 313 | - | assert.True(t, | |
| 314 | - | contains(outputStr, "Error finding") || | |
| 315 | - | contains(outputStr, "Error sending") || | |
| 316 | - | contains(outputStr, "not be running"), | |
| 317 | - | "Should contain process-related error, got: %s", outputStr) | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | func TestFindGordonPidFile_AllLocations(t *testing.T) { | |
| 321 | - | // Test the locations that findGordonPidFile checks | |
| 322 | - | locations := []string{ | |
| 323 | - | "/tmp/gordon.pid", | |
| 324 | - | "/var/run/gordon.pid", | |
| 325 | - | } | |
| 326 | - | ||
| 327 | - | // Add temp dir location | |
| 328 | - | tempDirLocation := filepath.Join(os.TempDir(), "gordon.pid") | |
| 329 | - | locations = append(locations, tempDirLocation) | |
| 330 | - | ||
| 331 | - | // Add home directory location | |
| 332 | - | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 333 | - | homeLocation := filepath.Join(homeDir, ".gordon.pid") | |
| 334 | - | locations = append(locations, homeLocation) | |
| 335 | - | } | |
| 336 | - | ||
| 337 | - | // Verify that the function checks these locations | |
| 338 | - | // We can't easily test all of them without root access, | |
| 339 | - | // but we can verify the function doesn't panic | |
| 340 | - | assert.NotPanics(t, func() { | |
| 341 | - | result := findGordonPidFile() | |
| 342 | - | // Result might be empty if no PID file exists, which is fine | |
| 343 | - | _ = result | |
| 344 | - | }) | |
| 345 | - | } | |
| 346 | - | ||
| 347 | - | func TestSignalSending_Concepts(t *testing.T) { | |
| 348 | - | // Test the signal sending concepts used in runReload | |
| 349 | - | ||
| 350 | - | // Test that we can find our own process | |
| 351 | - | process, err := os.FindProcess(os.Getpid()) | |
| 352 | - | assert.NoError(t, err) | |
| 353 | - | assert.NotNil(t, process) | |
| 354 | - | ||
| 355 | - | // Test signal types | |
| 356 | - | signals := []syscall.Signal{ | |
| 357 | - | syscall.SIGUSR1, | |
| 358 | - | syscall.SIGTERM, | |
| 359 | - | syscall.SIGINT, | |
| 360 | - | } | |
| 361 | - | ||
| 362 | - | for _, sig := range signals { | |
| 363 | - | assert.NotNil(t, sig) | |
| 364 | - | } | |
| 365 | - | } | |
| 366 | - | ||
| 367 | - | func TestPidFileValidation(t *testing.T) { | |
| 368 | - | tests := []struct { | |
| 369 | - | name string | |
| 370 | - | content string | |
| 371 | - | expectErr bool | |
| 372 | - | }{ | |
| 373 | - | { | |
| 374 | - | name: "valid PID", | |
| 375 | - | content: "12345", | |
| 376 | - | expectErr: false, | |
| 377 | - | }, | |
| 378 | - | { | |
| 379 | - | name: "invalid PID - letters", | |
| 380 | - | content: "abc", | |
| 381 | - | expectErr: true, | |
| 382 | - | }, | |
| 383 | - | { | |
| 384 | - | name: "invalid PID - empty", | |
| 385 | - | content: "", | |
| 386 | - | expectErr: true, | |
| 387 | - | }, | |
| 388 | - | { | |
| 389 | - | name: "invalid PID - negative", | |
| 390 | - | content: "-123", | |
| 391 | - | expectErr: false, // Parsing succeeds but PID is invalid | |
| 392 | - | }, | |
| 393 | - | { | |
| 394 | - | name: "valid PID with whitespace", | |
| 395 | - | content: " 12345 \n", | |
| 396 | - | expectErr: false, | |
| 397 | - | }, | |
| 398 | - | } | |
| 399 | - | ||
| 400 | - | for _, tt := range tests { | |
| 401 | - | t.Run(tt.name, func(t *testing.T) { | |
| 402 | - | var pid int | |
| 403 | - | _, err := fmt.Sscanf(tt.content, "%d", &pid) | |
| 404 | - | ||
| 405 | - | if tt.expectErr { | |
| 406 | - | assert.Error(t, err) | |
| 407 | - | } else { | |
| 408 | - | assert.NoError(t, err) | |
| 409 | - | if tt.content != "" && tt.content != " 12345 \n" && tt.content != "-123" { | |
| 410 | - | assert.Greater(t, pid, 0) | |
| 411 | - | } | |
| 412 | - | } | |
| 413 | - | }) | |
| 414 | - | } | |
| 415 | - | } | |
| 416 | - | ||
| 417 | - | // Helper function to check if string contains substring (case-insensitive) | |
| 418 | - | func contains(s, substr string) bool { | |
| 419 | - | return len(s) >= len(substr) && | |
| 420 | - | (s == substr || | |
| 421 | - | len(s) > len(substr) && | |
| 422 | - | (s[:len(substr)] == substr || | |
| 423 | - | s[len(s)-len(substr):] == substr || | |
| 424 | - | searchSubstring(s, substr))) | |
| 425 | - | } | |
| 426 | - | ||
| 427 | - | func searchSubstring(s, substr string) bool { | |
| 428 | - | for i := 0; i <= len(s)-len(substr); i++ { | |
| 429 | - | if s[i:i+len(substr)] == substr { | |
| 430 | - | return true | |
| 431 | - | } | |
| 432 | - | } | |
| 433 | - | return false | |
| 434 | - | }⏎̸ | |
D cmd/root.go +0 -81
| @@ -1,81 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "os" | |
| 6 | - | ||
| 7 | - | "github.com/spf13/cobra" | |
| 8 | - | "github.com/spf13/viper" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | var cfgFile string | |
| 12 | - | ||
| 13 | - | // Version information set by main.go from goreleaser | |
| 14 | - | var ( | |
| 15 | - | BuildVersion = "dev" | |
| 16 | - | BuildCommit = "unknown" | |
| 17 | - | BuildDate = "unknown" | |
| 18 | - | ) | |
| 19 | - | ||
| 20 | - | var rootCmd = &cobra.Command{ | |
| 21 | - | Use: "gordon", | |
| 22 | - | Short: "Gordon - Container Registry & Deployment System", | |
| 23 | - | Long: `Gordon is a single-binary application that combines a container registry | |
| 24 | - | with an intelligent reverse proxy and automated deployment system.`, | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | func Execute() error { | |
| 28 | - | return rootCmd.Execute() | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | // SetVersionInfo sets the build information from main.go | |
| 32 | - | func SetVersionInfo(version, commit, date string) { | |
| 33 | - | BuildVersion = version | |
| 34 | - | BuildCommit = commit | |
| 35 | - | BuildDate = date | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | func init() { | |
| 39 | - | cobra.OnInitialize(initConfig) | |
| 40 | - | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./gordon.toml)") | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | func initConfig() { | |
| 44 | - | if cfgFile != "" { | |
| 45 | - | viper.SetConfigFile(cfgFile) | |
| 46 | - | } else { | |
| 47 | - | // Search for config file in standard locations | |
| 48 | - | viper.SetConfigName("gordon") | |
| 49 | - | viper.SetConfigType("toml") | |
| 50 | - | ||
| 51 | - | // Current directory (highest priority) | |
| 52 | - | viper.AddConfigPath(".") | |
| 53 | - | ||
| 54 | - | // User config directory | |
| 55 | - | if userConfigDir, err := os.UserConfigDir(); err == nil { | |
| 56 | - | viper.AddConfigPath(userConfigDir + "/gordon") | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | // User home directory | |
| 60 | - | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 61 | - | viper.AddConfigPath(homeDir + "/.gordon") | |
| 62 | - | viper.AddConfigPath(homeDir) | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | // System-wide config directories | |
| 66 | - | viper.AddConfigPath("/etc/gordon") | |
| 67 | - | viper.AddConfigPath("/usr/local/etc/gordon") | |
| 68 | - | } | |
| 69 | - | ||
| 70 | - | viper.AutomaticEnv() | |
| 71 | - | ||
| 72 | - | if err := viper.ReadInConfig(); err == nil { | |
| 73 | - | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) | |
| 74 | - | } else { | |
| 75 | - | if cfgFile != "" { | |
| 76 | - | fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) | |
| 77 | - | } | |
| 78 | - | // Don't fatal for version command - config not needed | |
| 79 | - | // Other commands will check for config when needed | |
| 80 | - | } | |
| 81 | - | } | |
D cmd/root_test.go +0 -269
| @@ -1,269 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "os" | |
| 6 | - | "path/filepath" | |
| 7 | - | "testing" | |
| 8 | - | ||
| 9 | - | "github.com/spf13/cobra" | |
| 10 | - | "github.com/spf13/viper" | |
| 11 | - | "github.com/stretchr/testify/assert" | |
| 12 | - | "github.com/stretchr/testify/require" | |
| 13 | - | ) | |
| 14 | - | ||
| 15 | - | func TestRootCommand(t *testing.T) { | |
| 16 | - | tests := []struct { | |
| 17 | - | name string | |
| 18 | - | args []string | |
| 19 | - | }{ | |
| 20 | - | { | |
| 21 | - | name: "no arguments shows help", | |
| 22 | - | args: []string{}, | |
| 23 | - | }, | |
| 24 | - | { | |
| 25 | - | name: "help flag", | |
| 26 | - | args: []string{"--help"}, | |
| 27 | - | }, | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | for _, tt := range tests { | |
| 31 | - | t.Run(tt.name, func(t *testing.T) { | |
| 32 | - | // Create new command to avoid state pollution | |
| 33 | - | cmd := &cobra.Command{ | |
| 34 | - | Use: "gordon", | |
| 35 | - | Short: "Gordon - Container Registry & Deployment System", | |
| 36 | - | Long: `Gordon is a single-binary application that combines a container registry | |
| 37 | - | with an intelligent reverse proxy and automated deployment system.`, | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | cmd.SetArgs(tt.args) | |
| 41 | - | ||
| 42 | - | var output bytes.Buffer | |
| 43 | - | cmd.SetOut(&output) | |
| 44 | - | cmd.SetErr(&output) | |
| 45 | - | ||
| 46 | - | // Should not panic | |
| 47 | - | assert.NotPanics(t, func() { | |
| 48 | - | cmd.Execute() | |
| 49 | - | }) | |
| 50 | - | }) | |
| 51 | - | } | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | func TestExecute(t *testing.T) { | |
| 55 | - | // Create a temporary config file to avoid config errors | |
| 56 | - | tempDir := t.TempDir() | |
| 57 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 58 | - | ||
| 59 | - | configContent := ` | |
| 60 | - | [server] | |
| 61 | - | port = 8080 | |
| 62 | - | registry_port = 5000 | |
| 63 | - | runtime = "docker" | |
| 64 | - | ||
| 65 | - | [logging] | |
| 66 | - | enabled = false | |
| 67 | - | ||
| 68 | - | [routes] | |
| 69 | - | ` | |
| 70 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 71 | - | require.NoError(t, err) | |
| 72 | - | ||
| 73 | - | // Save original values | |
| 74 | - | originalArgs := os.Args | |
| 75 | - | originalCfgFile := cfgFile | |
| 76 | - | ||
| 77 | - | // Test with help flag to avoid starting actual server | |
| 78 | - | os.Args = []string{"gordon", "--help"} | |
| 79 | - | cfgFile = configFile | |
| 80 | - | ||
| 81 | - | // Restore after test | |
| 82 | - | defer func() { | |
| 83 | - | os.Args = originalArgs | |
| 84 | - | cfgFile = originalCfgFile | |
| 85 | - | viper.Reset() | |
| 86 | - | }() | |
| 87 | - | ||
| 88 | - | // Should not panic | |
| 89 | - | assert.NotPanics(t, func() { | |
| 90 | - | Execute() | |
| 91 | - | }) | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | func TestInitConfig(t *testing.T) { | |
| 95 | - | tests := []struct { | |
| 96 | - | name string | |
| 97 | - | setupFunc func(t *testing.T) string // returns temp dir | |
| 98 | - | configFile string | |
| 99 | - | configContent string | |
| 100 | - | expectedError bool | |
| 101 | - | shouldFindFile bool | |
| 102 | - | }{ | |
| 103 | - | { | |
| 104 | - | name: "explicit config file", | |
| 105 | - | setupFunc: func(t *testing.T) string { | |
| 106 | - | return t.TempDir() | |
| 107 | - | }, | |
| 108 | - | configFile: "test-gordon.toml", | |
| 109 | - | configContent: ` | |
| 110 | - | [server] | |
| 111 | - | port = 8080 | |
| 112 | - | registry_port = 5000 | |
| 113 | - | runtime = "docker" | |
| 114 | - | ||
| 115 | - | [logging] | |
| 116 | - | enabled = false | |
| 117 | - | ||
| 118 | - | [routes] | |
| 119 | - | `, | |
| 120 | - | shouldFindFile: true, | |
| 121 | - | }, | |
| 122 | - | { | |
| 123 | - | name: "config file in current directory", | |
| 124 | - | setupFunc: func(t *testing.T) string { | |
| 125 | - | tempDir := t.TempDir() | |
| 126 | - | // Change to temp directory | |
| 127 | - | originalDir, _ := os.Getwd() | |
| 128 | - | os.Chdir(tempDir) | |
| 129 | - | t.Cleanup(func() { | |
| 130 | - | os.Chdir(originalDir) | |
| 131 | - | }) | |
| 132 | - | return tempDir | |
| 133 | - | }, | |
| 134 | - | configFile: "gordon.toml", | |
| 135 | - | configContent: ` | |
| 136 | - | [server] | |
| 137 | - | port = 8080 | |
| 138 | - | registry_port = 5000 | |
| 139 | - | runtime = "docker" | |
| 140 | - | ||
| 141 | - | [logging] | |
| 142 | - | enabled = false | |
| 143 | - | ||
| 144 | - | [routes] | |
| 145 | - | `, | |
| 146 | - | shouldFindFile: true, | |
| 147 | - | }, | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | for _, tt := range tests { | |
| 151 | - | t.Run(tt.name, func(t *testing.T) { | |
| 152 | - | // Reset viper for each test | |
| 153 | - | viper.Reset() | |
| 154 | - | ||
| 155 | - | tempDir := tt.setupFunc(t) | |
| 156 | - | ||
| 157 | - | // Save original values | |
| 158 | - | originalCfgFile := cfgFile | |
| 159 | - | ||
| 160 | - | if tt.configContent != "" { | |
| 161 | - | configPath := tt.configFile | |
| 162 | - | if !filepath.IsAbs(configPath) { | |
| 163 | - | configPath = filepath.Join(tempDir, tt.configFile) | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | err := os.WriteFile(configPath, []byte(tt.configContent), 0644) | |
| 167 | - | require.NoError(t, err) | |
| 168 | - | ||
| 169 | - | if tt.name == "explicit config file" { | |
| 170 | - | cfgFile = configPath | |
| 171 | - | } | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | // Restore after test | |
| 175 | - | t.Cleanup(func() { | |
| 176 | - | cfgFile = originalCfgFile | |
| 177 | - | viper.Reset() | |
| 178 | - | }) | |
| 179 | - | ||
| 180 | - | assert.NotPanics(t, func() { | |
| 181 | - | initConfig() | |
| 182 | - | }) | |
| 183 | - | ||
| 184 | - | if tt.shouldFindFile { | |
| 185 | - | assert.NotEmpty(t, viper.ConfigFileUsed()) | |
| 186 | - | } | |
| 187 | - | }) | |
| 188 | - | } | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | func TestRootCmdStructure(t *testing.T) { | |
| 192 | - | // Test root command basic properties | |
| 193 | - | assert.Equal(t, "gordon", rootCmd.Use) | |
| 194 | - | assert.Contains(t, rootCmd.Short, "Gordon") | |
| 195 | - | assert.Contains(t, rootCmd.Long, "container registry") | |
| 196 | - | ||
| 197 | - | // Test that persistent flags are set | |
| 198 | - | flag := rootCmd.PersistentFlags().Lookup("config") | |
| 199 | - | assert.NotNil(t, flag) | |
| 200 | - | assert.Equal(t, "config", flag.Name) | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | func TestRootCmdSubcommands(t *testing.T) { | |
| 204 | - | // Check that expected subcommands are registered | |
| 205 | - | commands := rootCmd.Commands() | |
| 206 | - | ||
| 207 | - | commandNames := make([]string, len(commands)) | |
| 208 | - | for i, cmd := range commands { | |
| 209 | - | commandNames[i] = cmd.Name() | |
| 210 | - | } | |
| 211 | - | ||
| 212 | - | // Should have start and reload commands | |
| 213 | - | assert.Contains(t, commandNames, "start") | |
| 214 | - | assert.Contains(t, commandNames, "reload") | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | func TestConfigFileFlag(t *testing.T) { | |
| 218 | - | tempDir := t.TempDir() | |
| 219 | - | configFile := filepath.Join(tempDir, "test-config.toml") | |
| 220 | - | ||
| 221 | - | configContent := ` | |
| 222 | - | [server] | |
| 223 | - | port = 9999 | |
| 224 | - | registry_port = 6000 | |
| 225 | - | runtime = "docker" | |
| 226 | - | ||
| 227 | - | [logging] | |
| 228 | - | enabled = false | |
| 229 | - | ||
| 230 | - | [routes] | |
| 231 | - | ` | |
| 232 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 233 | - | require.NoError(t, err) | |
| 234 | - | ||
| 235 | - | // Reset viper and cfgFile | |
| 236 | - | viper.Reset() | |
| 237 | - | originalCfgFile := cfgFile | |
| 238 | - | ||
| 239 | - | defer func() { | |
| 240 | - | cfgFile = originalCfgFile | |
| 241 | - | viper.Reset() | |
| 242 | - | }() | |
| 243 | - | ||
| 244 | - | // Test setting config file via flag | |
| 245 | - | cfgFile = configFile | |
| 246 | - | ||
| 247 | - | assert.NotPanics(t, func() { | |
| 248 | - | initConfig() | |
| 249 | - | }) | |
| 250 | - | ||
| 251 | - | // Should use the specified config file | |
| 252 | - | assert.Equal(t, configFile, viper.ConfigFileUsed()) | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | func TestRootCmdHelp(t *testing.T) { | |
| 256 | - | var output bytes.Buffer | |
| 257 | - | rootCmd.SetOut(&output) | |
| 258 | - | rootCmd.SetArgs([]string{"--help"}) | |
| 259 | - | ||
| 260 | - | err := rootCmd.Execute() | |
| 261 | - | assert.NoError(t, err) | |
| 262 | - | ||
| 263 | - | helpOutput := output.String() | |
| 264 | - | assert.Contains(t, helpOutput, "Gordon") | |
| 265 | - | assert.Contains(t, helpOutput, "container registry") | |
| 266 | - | assert.Contains(t, helpOutput, "Available Commands:") | |
| 267 | - | assert.Contains(t, helpOutput, "start") | |
| 268 | - | assert.Contains(t, helpOutput, "reload") | |
| 269 | - | }⏎̸ | |
D cmd/start.go +0 -327
| @@ -1,327 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "os" | |
| 7 | - | "os/signal" | |
| 8 | - | "path/filepath" | |
| 9 | - | "sync" | |
| 10 | - | "syscall" | |
| 11 | - | "time" | |
| 12 | - | ||
| 13 | - | "gordon/internal/config" | |
| 14 | - | "gordon/internal/container" | |
| 15 | - | "gordon/internal/events" | |
| 16 | - | "gordon/internal/logging" | |
| 17 | - | "gordon/internal/proxy" | |
| 18 | - | "gordon/internal/registry" | |
| 19 | - | ||
| 20 | - | "github.com/fsnotify/fsnotify" | |
| 21 | - | "github.com/rs/zerolog" | |
| 22 | - | "github.com/rs/zerolog/log" | |
| 23 | - | "github.com/spf13/cobra" | |
| 24 | - | "github.com/spf13/viper" | |
| 25 | - | ) | |
| 26 | - | ||
| 27 | - | var startCmd = &cobra.Command{ | |
| 28 | - | Use: "start", | |
| 29 | - | Short: "Start the Gordon server", | |
| 30 | - | Long: `Start the container registry and reverse proxy server`, | |
| 31 | - | Run: runStart, | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | func init() { | |
| 35 | - | rootCmd.AddCommand(startCmd) | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | func runStart(cmd *cobra.Command, args []string) { | |
| 39 | - | // Load configuration first | |
| 40 | - | cfg, err := config.Load() | |
| 41 | - | if err != nil { | |
| 42 | - | // Use basic console logging for config errors | |
| 43 | - | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |
| 44 | - | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |
| 45 | - | log.Fatal().Err(err).Msg("Failed to load config") | |
| 46 | - | } | |
| 47 | - | ||
| 48 | - | // Initialize logging system | |
| 49 | - | if err := logging.Setup(cfg); err != nil { | |
| 50 | - | log.Fatal().Err(err).Msg("Failed to initialize logging") | |
| 51 | - | } | |
| 52 | - | defer logging.Close() | |
| 53 | - | ||
| 54 | - | log.Info().Msg("Starting Gordon server...") | |
| 55 | - | log.Info().Int("registry_port", cfg.Server.RegistryPort).Msg("Registry server") | |
| 56 | - | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | |
| 57 | - | log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime") | |
| 58 | - | ||
| 59 | - | // Create PID file for reload command | |
| 60 | - | pidFile := createPidFile() | |
| 61 | - | if pidFile != "" { | |
| 62 | - | defer removePidFile(pidFile) | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | // Initialize event bus | |
| 66 | - | eventBus := events.NewInMemoryEventBus(100) | |
| 67 | - | if err := eventBus.Start(); err != nil { | |
| 68 | - | log.Fatal().Err(err).Msg("Failed to start event bus") | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | // Initialize container manager | |
| 72 | - | manager, err := container.NewManager(cfg) | |
| 73 | - | if err != nil { | |
| 74 | - | log.Fatal().Err(err).Msg("Failed to initialize container manager") | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | // Create and subscribe container event handler | |
| 78 | - | containerHandler := events.NewContainerEventHandler(manager, cfg) | |
| 79 | - | if err := eventBus.Subscribe(containerHandler); err != nil { | |
| 80 | - | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | // Create and subscribe auto-route event handler | |
| 84 | - | var autoRouteHandler *events.AutoRouteHandler | |
| 85 | - | autoRouteHandler = events.NewAutoRouteHandler(cfg, manager) | |
| 86 | - | if err := eventBus.Subscribe(autoRouteHandler); err != nil { | |
| 87 | - | log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler") | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | // Create and subscribe version event handler | |
| 91 | - | var versionHandler *events.VersionHandler | |
| 92 | - | versionHandler = events.NewVersionHandler(manager, cfg) | |
| 93 | - | if err := eventBus.Subscribe(versionHandler); err != nil { | |
| 94 | - | log.Fatal().Err(err).Msg("Failed to subscribe version event handler") | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | // Start proxy server (needs to be accessible for config reload) | |
| 98 | - | proxyServer := proxy.NewServer(cfg, manager) | |
| 99 | - | ||
| 100 | - | // Create and subscribe proxy event handler | |
| 101 | - | var proxyHandler *proxy.ProxyEventHandler | |
| 102 | - | proxyHandler = proxy.NewProxyEventHandler(proxyServer) | |
| 103 | - | if err := eventBus.Subscribe(proxyHandler); err != nil { | |
| 104 | - | log.Fatal().Err(err).Msg("Failed to subscribe proxy event handler") | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | // Create context for the entire server lifecycle | |
| 108 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 109 | - | ||
| 110 | - | // Setup config file watching for live reload with debouncing | |
| 111 | - | var reloadTimer *time.Timer | |
| 112 | - | var reloadMutex sync.Mutex | |
| 113 | - | ||
| 114 | - | viper.WatchConfig() | |
| 115 | - | viper.OnConfigChange(func(e fsnotify.Event) { | |
| 116 | - | log.Info(). | |
| 117 | - | Str("file", e.Name). | |
| 118 | - | Str("op", e.Op.String()). | |
| 119 | - | Msg("Configuration file changed, reloading...") | |
| 120 | - | ||
| 121 | - | // Debounce config reloads to prevent multiple events from a single logical change | |
| 122 | - | reloadMutex.Lock() | |
| 123 | - | defer reloadMutex.Unlock() | |
| 124 | - | ||
| 125 | - | if reloadTimer != nil { | |
| 126 | - | reloadTimer.Stop() | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | reloadTimer = time.AfterFunc(500*time.Millisecond, func() { | |
| 130 | - | // Reload configuration | |
| 131 | - | newCfg, err := config.Load() | |
| 132 | - | if err != nil { | |
| 133 | - | log.Error().Err(err).Msg("Failed to reload configuration") | |
| 134 | - | return | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | // Update the configuration reference | |
| 138 | - | cfg = newCfg | |
| 139 | - | ||
| 140 | - | // Reinitialize logging with new config | |
| 141 | - | if err := logging.Setup(cfg); err != nil { | |
| 142 | - | log.Error().Err(err).Msg("Failed to reinitialize logging after config reload") | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | // Update proxy server config | |
| 146 | - | proxyServer.UpdateConfig(cfg) | |
| 147 | - | ||
| 148 | - | // Update container manager config (includes env file creation for new routes) | |
| 149 | - | manager.UpdateConfig(cfg) | |
| 150 | - | ||
| 151 | - | // Unsubscribe old handlers before creating new ones | |
| 152 | - | if err := eventBus.Unsubscribe(containerHandler); err != nil { | |
| 153 | - | log.Warn().Err(err).Msg("Failed to unsubscribe old container event handler") | |
| 154 | - | } | |
| 155 | - | if err := eventBus.Unsubscribe(autoRouteHandler); err != nil { | |
| 156 | - | log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler") | |
| 157 | - | } | |
| 158 | - | if err := eventBus.Unsubscribe(versionHandler); err != nil { | |
| 159 | - | log.Warn().Err(err).Msg("Failed to unsubscribe old version event handler") | |
| 160 | - | } | |
| 161 | - | if err := eventBus.Unsubscribe(proxyHandler); err != nil { | |
| 162 | - | log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler") | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | // Update container handler config | |
| 166 | - | containerHandler = events.NewContainerEventHandler(manager, cfg) | |
| 167 | - | if err := eventBus.Subscribe(containerHandler); err != nil { | |
| 168 | - | log.Error().Err(err).Msg("Failed to re-subscribe container event handler") | |
| 169 | - | return | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | // Update auto-route handler config | |
| 173 | - | autoRouteHandler = events.NewAutoRouteHandler(cfg, manager) | |
| 174 | - | if err := eventBus.Subscribe(autoRouteHandler); err != nil { | |
| 175 | - | log.Error().Err(err).Msg("Failed to re-subscribe auto-route event handler") | |
| 176 | - | return | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | // Update version handler config | |
| 180 | - | versionHandler = events.NewVersionHandler(manager, cfg) | |
| 181 | - | if err := eventBus.Subscribe(versionHandler); err != nil { | |
| 182 | - | log.Error().Err(err).Msg("Failed to re-subscribe version event handler") | |
| 183 | - | return | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | // Re-subscribe proxy handler (config ref updated automatically) | |
| 187 | - | proxyHandler = proxy.NewProxyEventHandler(proxyServer) | |
| 188 | - | if err := eventBus.Subscribe(proxyHandler); err != nil { | |
| 189 | - | log.Error().Err(err).Msg("Failed to re-subscribe proxy event handler") | |
| 190 | - | return | |
| 191 | - | } | |
| 192 | - | ||
| 193 | - | // Publish config reload event to handle route changes | |
| 194 | - | if err := eventBus.Publish(events.ConfigReload, nil); err != nil { | |
| 195 | - | log.Error().Err(err).Msg("Failed to publish config reload event") | |
| 196 | - | } else { | |
| 197 | - | log.Info().Msg("Configuration reloaded successfully") | |
| 198 | - | } | |
| 199 | - | }) | |
| 200 | - | }) | |
| 201 | - | ||
| 202 | - | // Sync existing containers | |
| 203 | - | if err := manager.SyncContainers(ctx); err != nil { | |
| 204 | - | log.Warn().Err(err).Msg("Failed to sync existing containers") | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | // Auto-start containers that match config routes | |
| 208 | - | log.Info().Msg("Auto-starting containers for configured routes...") | |
| 209 | - | if err := manager.AutoStartContainers(ctx); err != nil { | |
| 210 | - | log.Warn().Err(err).Msg("Failed to auto-start containers") | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | var wg sync.WaitGroup | |
| 214 | - | ||
| 215 | - | // Start registry server | |
| 216 | - | registryServer, err := registry.NewServer(cfg, eventBus) | |
| 217 | - | if err != nil { | |
| 218 | - | log.Fatal().Err(err).Msg("Failed to initialize registry server") | |
| 219 | - | } | |
| 220 | - | wg.Add(1) | |
| 221 | - | go func() { | |
| 222 | - | defer wg.Done() | |
| 223 | - | if err := registryServer.Start(ctx); err != nil { | |
| 224 | - | log.Error().Err(err).Msg("Registry server error") | |
| 225 | - | } | |
| 226 | - | }() | |
| 227 | - | ||
| 228 | - | // Start proxy server (already created above) | |
| 229 | - | wg.Add(1) | |
| 230 | - | go func() { | |
| 231 | - | defer wg.Done() | |
| 232 | - | if err := proxyServer.Start(ctx); err != nil { | |
| 233 | - | log.Error().Err(err).Msg("Proxy server error") | |
| 234 | - | } | |
| 235 | - | }() | |
| 236 | - | ||
| 237 | - | // Wait for interrupt or reload signals | |
| 238 | - | sigChan := make(chan os.Signal, 1) | |
| 239 | - | reloadChan := make(chan os.Signal, 1) | |
| 240 | - | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | |
| 241 | - | signal.Notify(reloadChan, syscall.SIGUSR1) | |
| 242 | - | ||
| 243 | - | // Handle signals in a loop | |
| 244 | - | for { | |
| 245 | - | select { | |
| 246 | - | case <-sigChan: | |
| 247 | - | log.Info().Msg("Received shutdown signal") | |
| 248 | - | goto shutdown | |
| 249 | - | case <-reloadChan: | |
| 250 | - | log.Info().Msg("Received manual reload signal (SIGUSR1)") | |
| 251 | - | // Trigger manual reload event | |
| 252 | - | if err := eventBus.Publish(events.ManualReload, nil); err != nil { | |
| 253 | - | log.Error().Err(err).Msg("Failed to publish manual reload event") | |
| 254 | - | } else { | |
| 255 | - | log.Info().Msg("Manual reload event published successfully") | |
| 256 | - | } | |
| 257 | - | } | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | shutdown: | |
| 261 | - | ||
| 262 | - | log.Info().Msg("Shutting down servers...") | |
| 263 | - | cancel() | |
| 264 | - | ||
| 265 | - | // Stop all managed containers with a separate context for shutdown | |
| 266 | - | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 267 | - | defer shutdownCancel() | |
| 268 | - | ||
| 269 | - | log.Info().Msg("Shutting down container manager...") | |
| 270 | - | if err := manager.Shutdown(shutdownCtx); err != nil { | |
| 271 | - | log.Error().Err(err).Msg("Failed to shutdown container manager") | |
| 272 | - | } | |
| 273 | - | ||
| 274 | - | // Stop event bus | |
| 275 | - | if err := eventBus.Stop(); err != nil { | |
| 276 | - | log.Error().Err(err).Msg("Failed to stop event bus") | |
| 277 | - | } | |
| 278 | - | ||
| 279 | - | // Give servers time to shutdown gracefully | |
| 280 | - | done := make(chan struct{}) | |
| 281 | - | go func() { | |
| 282 | - | wg.Wait() | |
| 283 | - | close(done) | |
| 284 | - | }() | |
| 285 | - | ||
| 286 | - | select { | |
| 287 | - | case <-done: | |
| 288 | - | log.Info().Msg("Servers stopped gracefully") | |
| 289 | - | case <-time.After(10 * time.Second): | |
| 290 | - | log.Warn().Msg("Force shutdown after timeout") | |
| 291 | - | } | |
| 292 | - | } | |
| 293 | - | ||
| 294 | - | // createPidFile creates a PID file for the Gordon process | |
| 295 | - | func createPidFile() string { | |
| 296 | - | pid := os.Getpid() | |
| 297 | - | ||
| 298 | - | // Try multiple locations for the PID file | |
| 299 | - | locations := []string{ | |
| 300 | - | "/tmp/gordon.pid", | |
| 301 | - | filepath.Join(os.TempDir(), "gordon.pid"), | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | // Also try user's home directory | |
| 305 | - | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 306 | - | locations = append(locations, filepath.Join(homeDir, ".gordon.pid")) | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | for _, location := range locations { | |
| 310 | - | if err := os.WriteFile(location, []byte(fmt.Sprintf("%d", pid)), 0644); err == nil { | |
| 311 | - | log.Debug().Str("pid_file", location).Int("pid", pid).Msg("Created PID file") | |
| 312 | - | return location | |
| 313 | - | } | |
| 314 | - | } | |
| 315 | - | ||
| 316 | - | log.Warn().Int("pid", pid).Msg("Failed to create PID file in any location") | |
| 317 | - | return "" | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | // removePidFile removes the PID file | |
| 321 | - | func removePidFile(pidFile string) { | |
| 322 | - | if err := os.Remove(pidFile); err != nil { | |
| 323 | - | log.Warn().Err(err).Str("pid_file", pidFile).Msg("Failed to remove PID file") | |
| 324 | - | } else { | |
| 325 | - | log.Debug().Str("pid_file", pidFile).Msg("Removed PID file") | |
| 326 | - | } | |
| 327 | - | } | |
D cmd/start_test.go +0 -333
| @@ -1,333 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "context" | |
| 6 | - | "fmt" | |
| 7 | - | "os" | |
| 8 | - | "path/filepath" | |
| 9 | - | "syscall" | |
| 10 | - | "testing" | |
| 11 | - | "time" | |
| 12 | - | ||
| 13 | - | "github.com/spf13/cobra" | |
| 14 | - | "github.com/spf13/viper" | |
| 15 | - | "github.com/stretchr/testify/assert" | |
| 16 | - | "github.com/stretchr/testify/require" | |
| 17 | - | ) | |
| 18 | - | ||
| 19 | - | func TestStartCommand(t *testing.T) { | |
| 20 | - | // Test start command structure | |
| 21 | - | assert.Equal(t, "start", startCmd.Use) | |
| 22 | - | assert.Contains(t, startCmd.Short, "Start") | |
| 23 | - | assert.Contains(t, startCmd.Long, "registry") | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | func TestRunStart_ConfigError(t *testing.T) { | |
| 27 | - | // Save original values | |
| 28 | - | originalCfgFile := cfgFile | |
| 29 | - | defer func() { | |
| 30 | - | cfgFile = originalCfgFile | |
| 31 | - | viper.Reset() | |
| 32 | - | }() | |
| 33 | - | ||
| 34 | - | // Set non-existent config file | |
| 35 | - | cfgFile = "/nonexistent/config.toml" | |
| 36 | - | ||
| 37 | - | // Capture stderr to check config error message | |
| 38 | - | var stderr bytes.Buffer | |
| 39 | - | originalStderr := os.Stderr | |
| 40 | - | r, w, _ := os.Pipe() | |
| 41 | - | os.Stderr = w | |
| 42 | - | ||
| 43 | - | // Initialize config to trigger error handling | |
| 44 | - | done := make(chan bool) | |
| 45 | - | go func() { | |
| 46 | - | defer func() { | |
| 47 | - | if recover() != nil { | |
| 48 | - | // Recovery from log.Fatal is expected in some cases | |
| 49 | - | } | |
| 50 | - | done <- true | |
| 51 | - | }() | |
| 52 | - | ||
| 53 | - | // Call initConfig directly to test config error handling | |
| 54 | - | initConfig() | |
| 55 | - | }() | |
| 56 | - | ||
| 57 | - | // Give it a moment to process | |
| 58 | - | select { | |
| 59 | - | case <-done: | |
| 60 | - | // Expected | |
| 61 | - | case <-time.After(1 * time.Second): | |
| 62 | - | t.Fatal("initConfig should have completed quickly") | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | w.Close() | |
| 66 | - | os.Stderr = originalStderr | |
| 67 | - | stderr.ReadFrom(r) | |
| 68 | - | ||
| 69 | - | // Check that an error message was printed | |
| 70 | - | stderrContent := stderr.String() | |
| 71 | - | assert.Contains(t, stderrContent, "Error reading config file") | |
| 72 | - | } | |
| 73 | - | ||
| 74 | - | func TestCreatePidFile(t *testing.T) { | |
| 75 | - | // Test PID file creation | |
| 76 | - | pidFile := createPidFile() | |
| 77 | - | ||
| 78 | - | if pidFile != "" { | |
| 79 | - | // Verify file exists and contains valid PID | |
| 80 | - | assert.FileExists(t, pidFile) | |
| 81 | - | ||
| 82 | - | content, err := os.ReadFile(pidFile) | |
| 83 | - | require.NoError(t, err) | |
| 84 | - | ||
| 85 | - | var pid int | |
| 86 | - | _, err = fmt.Sscanf(string(content), "%d", &pid) | |
| 87 | - | assert.NoError(t, err) | |
| 88 | - | assert.Equal(t, os.Getpid(), pid) | |
| 89 | - | ||
| 90 | - | // Clean up | |
| 91 | - | removePidFile(pidFile) | |
| 92 | - | assert.NoFileExists(t, pidFile) | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | // Test should pass even if no writable location is found | |
| 96 | - | // (function returns empty string in that case) | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | func TestRemovePidFile(t *testing.T) { | |
| 100 | - | tests := []struct { | |
| 101 | - | name string | |
| 102 | - | setupFunc func(t *testing.T) string | |
| 103 | - | expectError bool | |
| 104 | - | }{ | |
| 105 | - | { | |
| 106 | - | name: "remove existing file", | |
| 107 | - | setupFunc: func(t *testing.T) string { | |
| 108 | - | tempDir := t.TempDir() | |
| 109 | - | pidFile := filepath.Join(tempDir, "test.pid") | |
| 110 | - | err := os.WriteFile(pidFile, []byte("12345"), 0644) | |
| 111 | - | require.NoError(t, err) | |
| 112 | - | return pidFile | |
| 113 | - | }, | |
| 114 | - | expectError: false, | |
| 115 | - | }, | |
| 116 | - | { | |
| 117 | - | name: "remove non-existent file", | |
| 118 | - | setupFunc: func(t *testing.T) string { | |
| 119 | - | tempDir := t.TempDir() | |
| 120 | - | return filepath.Join(tempDir, "nonexistent.pid") | |
| 121 | - | }, | |
| 122 | - | expectError: true, // Warning logged, but no panic | |
| 123 | - | }, | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | for _, tt := range tests { | |
| 127 | - | t.Run(tt.name, func(t *testing.T) { | |
| 128 | - | pidFile := tt.setupFunc(t) | |
| 129 | - | ||
| 130 | - | // Should not panic regardless of whether file exists | |
| 131 | - | assert.NotPanics(t, func() { | |
| 132 | - | removePidFile(pidFile) | |
| 133 | - | }) | |
| 134 | - | ||
| 135 | - | // File should not exist after removal attempt | |
| 136 | - | assert.NoFileExists(t, pidFile) | |
| 137 | - | }) | |
| 138 | - | } | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | func TestCreatePidFile_Locations(t *testing.T) { | |
| 142 | - | // Test that createPidFile tries multiple locations | |
| 143 | - | // We can't easily mock os.WriteFile to test all failure scenarios, | |
| 144 | - | // but we can verify the function doesn't panic | |
| 145 | - | assert.NotPanics(t, func() { | |
| 146 | - | pidFile := createPidFile() | |
| 147 | - | if pidFile != "" { | |
| 148 | - | removePidFile(pidFile) | |
| 149 | - | } | |
| 150 | - | }) | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | func TestCreatePidFile_Content(t *testing.T) { | |
| 154 | - | pidFile := createPidFile() | |
| 155 | - | if pidFile == "" { | |
| 156 | - | t.Skip("No writable location found for PID file") | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | defer removePidFile(pidFile) | |
| 160 | - | ||
| 161 | - | // Verify PID file contains current process ID | |
| 162 | - | content, err := os.ReadFile(pidFile) | |
| 163 | - | require.NoError(t, err) | |
| 164 | - | ||
| 165 | - | var storedPid int | |
| 166 | - | _, err = fmt.Sscanf(string(content), "%d", &storedPid) | |
| 167 | - | require.NoError(t, err) | |
| 168 | - | ||
| 169 | - | assert.Equal(t, os.Getpid(), storedPid) | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | func TestPidFilePermissions(t *testing.T) { | |
| 173 | - | pidFile := createPidFile() | |
| 174 | - | if pidFile == "" { | |
| 175 | - | t.Skip("No writable location found for PID file") | |
| 176 | - | } | |
| 177 | - | ||
| 178 | - | defer removePidFile(pidFile) | |
| 179 | - | ||
| 180 | - | // Check file permissions | |
| 181 | - | info, err := os.Stat(pidFile) | |
| 182 | - | require.NoError(t, err) | |
| 183 | - | ||
| 184 | - | // Should be readable by owner and group | |
| 185 | - | assert.True(t, info.Mode().Perm()&0644 != 0) | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | func TestPidFileLocations(t *testing.T) { | |
| 189 | - | // Test different potential PID file locations | |
| 190 | - | // This mainly tests that the function handles various scenarios | |
| 191 | - | ||
| 192 | - | originalHome := os.Getenv("HOME") | |
| 193 | - | defer os.Setenv("HOME", originalHome) | |
| 194 | - | ||
| 195 | - | // Test with invalid home directory | |
| 196 | - | os.Setenv("HOME", "/nonexistent/invalid/path") | |
| 197 | - | ||
| 198 | - | assert.NotPanics(t, func() { | |
| 199 | - | pidFile := createPidFile() | |
| 200 | - | if pidFile != "" { | |
| 201 | - | removePidFile(pidFile) | |
| 202 | - | } | |
| 203 | - | }) | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | func TestStartCmdStructure(t *testing.T) { | |
| 207 | - | // Verify command is properly registered | |
| 208 | - | found := false | |
| 209 | - | for _, cmd := range rootCmd.Commands() { | |
| 210 | - | if cmd.Name() == "start" { | |
| 211 | - | found = true | |
| 212 | - | break | |
| 213 | - | } | |
| 214 | - | } | |
| 215 | - | assert.True(t, found, "start command should be registered with root command") | |
| 216 | - | } | |
| 217 | - | ||
| 218 | - | func TestStartCmdHelp(t *testing.T) { | |
| 219 | - | var output bytes.Buffer | |
| 220 | - | ||
| 221 | - | // Create a copy of the command to avoid affecting the global command | |
| 222 | - | cmd := &cobra.Command{ | |
| 223 | - | Use: startCmd.Use, | |
| 224 | - | Short: startCmd.Short, | |
| 225 | - | Long: startCmd.Long, | |
| 226 | - | Run: startCmd.Run, | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | cmd.SetOut(&output) | |
| 230 | - | cmd.SetErr(&output) | |
| 231 | - | cmd.SetArgs([]string{"--help"}) | |
| 232 | - | ||
| 233 | - | err := cmd.Execute() | |
| 234 | - | assert.NoError(t, err) | |
| 235 | - | ||
| 236 | - | helpOutput := output.String() | |
| 237 | - | assert.Contains(t, helpOutput, "Start") | |
| 238 | - | assert.Contains(t, helpOutput, "container registry") | |
| 239 | - | assert.Contains(t, helpOutput, "reverse proxy") | |
| 240 | - | assert.Contains(t, helpOutput, "server") | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | // Test helper functions for signal handling scenarios | |
| 244 | - | func TestStartCommand_SignalHandling(t *testing.T) { | |
| 245 | - | // This is a simplified test since we can't easily test the full signal handling | |
| 246 | - | // without actually starting the server. We mainly test that the command structure is correct. | |
| 247 | - | ||
| 248 | - | // Verify the command can be created and has the right properties | |
| 249 | - | assert.NotNil(t, startCmd.Run) | |
| 250 | - | assert.Equal(t, "start", startCmd.Use) | |
| 251 | - | } | |
| 252 | - | ||
| 253 | - | func TestRunStart_ValidConfig(t *testing.T) { | |
| 254 | - | // Create a valid config file | |
| 255 | - | tempDir := t.TempDir() | |
| 256 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 257 | - | ||
| 258 | - | configContent := ` | |
| 259 | - | [server] | |
| 260 | - | port = 8080 | |
| 261 | - | registry_port = 5000 | |
| 262 | - | runtime = "docker" | |
| 263 | - | ||
| 264 | - | [logging] | |
| 265 | - | enabled = false | |
| 266 | - | dir = "` + filepath.Join(tempDir, "logs") + `" | |
| 267 | - | ||
| 268 | - | [registry] | |
| 269 | - | enabled = false | |
| 270 | - | ||
| 271 | - | [routes] | |
| 272 | - | ` | |
| 273 | - | ||
| 274 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 275 | - | require.NoError(t, err) | |
| 276 | - | ||
| 277 | - | // Save original values | |
| 278 | - | originalCfgFile := cfgFile | |
| 279 | - | defer func() { | |
| 280 | - | cfgFile = originalCfgFile | |
| 281 | - | viper.Reset() | |
| 282 | - | }() | |
| 283 | - | ||
| 284 | - | // Set config file | |
| 285 | - | cfgFile = configFile | |
| 286 | - | viper.Reset() | |
| 287 | - | ||
| 288 | - | // We can't actually run the full start command without Docker, | |
| 289 | - | // but we can test that it doesn't immediately fail with config errors | |
| 290 | - | // by mocking the execution | |
| 291 | - | ||
| 292 | - | // This is a limited test since runStart calls log.Fatal on various errors | |
| 293 | - | // and we can't easily test the full server startup without actual dependencies | |
| 294 | - | assert.NotNil(t, startCmd.Run, "Start command should have a run function") | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | func TestSignalHandling_Concepts(t *testing.T) { | |
| 298 | - | // Test the concepts used in signal handling | |
| 299 | - | ||
| 300 | - | // Test that we can create signal channels (used in runStart) | |
| 301 | - | sigChan := make(chan os.Signal, 1) | |
| 302 | - | reloadChan := make(chan os.Signal, 1) | |
| 303 | - | ||
| 304 | - | assert.NotNil(t, sigChan) | |
| 305 | - | assert.NotNil(t, reloadChan) | |
| 306 | - | ||
| 307 | - | // Test signal types that would be used | |
| 308 | - | signals := []os.Signal{ | |
| 309 | - | syscall.SIGINT, | |
| 310 | - | syscall.SIGTERM, | |
| 311 | - | syscall.SIGUSR1, | |
| 312 | - | } | |
| 313 | - | ||
| 314 | - | for _, sig := range signals { | |
| 315 | - | assert.NotNil(t, sig) | |
| 316 | - | } | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | func TestContextCancellation(t *testing.T) { | |
| 320 | - | // Test context cancellation patterns used in runStart | |
| 321 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 322 | - | assert.NotNil(t, ctx) | |
| 323 | - | assert.NotNil(t, cancel) | |
| 324 | - | ||
| 325 | - | // Test timeout context | |
| 326 | - | timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 327 | - | assert.NotNil(t, timeoutCtx) | |
| 328 | - | assert.NotNil(t, timeoutCancel) | |
| 329 | - | ||
| 330 | - | // Clean up | |
| 331 | - | cancel() | |
| 332 | - | timeoutCancel() | |
| 333 | - | }⏎̸ | |
D cmd/version.go +0 -33
| @@ -1,33 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | ||
| 6 | - | "github.com/spf13/cobra" | |
| 7 | - | ) | |
| 8 | - | ||
| 9 | - | var versionCmd = &cobra.Command{ | |
| 10 | - | Use: "version", | |
| 11 | - | Short: "Show version information", | |
| 12 | - | Long: `Display the Gordon version, commit hash, and build date.`, | |
| 13 | - | Run: func(cmd *cobra.Command, args []string) { | |
| 14 | - | fmt.Printf("Gordon %s\n", BuildVersion) | |
| 15 | - | fmt.Printf("Commit: %s\n", BuildCommit) | |
| 16 | - | fmt.Printf("Built: %s\n", BuildDate) | |
| 17 | - | }, | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | func init() { | |
| 21 | - | rootCmd.AddCommand(versionCmd) | |
| 22 | - | versionCmd.Flags().BoolP("short", "s", false, "Show only version number") | |
| 23 | - | ||
| 24 | - | // Override run function to handle short flag | |
| 25 | - | originalRun := versionCmd.Run | |
| 26 | - | versionCmd.Run = func(cmd *cobra.Command, args []string) { | |
| 27 | - | if short, _ := cmd.Flags().GetBool("short"); short { | |
| 28 | - | fmt.Println(BuildVersion) | |
| 29 | - | return | |
| 30 | - | } | |
| 31 | - | originalRun(cmd, args) | |
| 32 | - | } | |
| 33 | - | }⏎̸ | |
A internal/adapters/in/cli/root.go +95 -0
| @@ -0,0 +1,95 @@ | |||
| 1 | + | // Package cli implements the CLI adapter for Gordon. | |
| 2 | + | // This package provides Cobra commands that delegate to the app layer. | |
| 3 | + | package cli | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | ||
| 8 | + | "gordon/internal/app" | |
| 9 | + | ||
| 10 | + | "github.com/spf13/cobra" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | var ( | |
| 14 | + | // Version information (set at build time) | |
| 15 | + | Version = "dev" | |
| 16 | + | Commit = "unknown" | |
| 17 | + | BuildDate = "unknown" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // NewRootCmd creates the root command for Gordon CLI. | |
| 21 | + | func NewRootCmd() *cobra.Command { | |
| 22 | + | rootCmd := &cobra.Command{ | |
| 23 | + | Use: "gordon", | |
| 24 | + | Short: "Gordon - A lightweight container deployment platform", | |
| 25 | + | Long: `Gordon is a self-contained container deployment platform that combines | |
| 26 | + | a Docker registry with automatic container deployment capabilities. | |
| 27 | + | ||
| 28 | + | It listens for image pushes and automatically deploys containers based on | |
| 29 | + | configuration rules, making it ideal for single-server deployments.`, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // Add subcommands | |
| 33 | + | rootCmd.AddCommand(newStartCmd()) | |
| 34 | + | rootCmd.AddCommand(newReloadCmd()) | |
| 35 | + | rootCmd.AddCommand(newVersionCmd()) | |
| 36 | + | ||
| 37 | + | return rootCmd | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // newStartCmd creates the start command. | |
| 41 | + | func newStartCmd() *cobra.Command { | |
| 42 | + | var configPath string | |
| 43 | + | ||
| 44 | + | cmd := &cobra.Command{ | |
| 45 | + | Use: "start", | |
| 46 | + | Short: "Start the Gordon server", | |
| 47 | + | Long: `Start the Gordon server, including the registry and proxy components.`, | |
| 48 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 49 | + | return app.Run(context.Background(), configPath) | |
| 50 | + | }, | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Add flags | |
| 54 | + | cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file") | |
| 55 | + | ||
| 56 | + | return cmd | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // newReloadCmd creates the reload command. | |
| 60 | + | func newReloadCmd() *cobra.Command { | |
| 61 | + | return &cobra.Command{ | |
| 62 | + | Use: "reload", | |
| 63 | + | Short: "Reload containers with updated environment", | |
| 64 | + | Long: `Triggers a reload of all managed containers with updated environment | |
| 65 | + | variables from their respective .env files.`, | |
| 66 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 67 | + | return runReload() | |
| 68 | + | }, | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // newVersionCmd creates the version command. | |
| 73 | + | func newVersionCmd() *cobra.Command { | |
| 74 | + | return &cobra.Command{ | |
| 75 | + | Use: "version", | |
| 76 | + | Short: "Print version information", | |
| 77 | + | Run: func(cmd *cobra.Command, args []string) { | |
| 78 | + | cmd.Printf("Gordon %s\n", Version) | |
| 79 | + | cmd.Printf("Commit: %s\n", Commit) | |
| 80 | + | cmd.Printf("Build Date: %s\n", BuildDate) | |
| 81 | + | }, | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // runReload sends SIGUSR1 to the running Gordon process. | |
| 86 | + | func runReload() error { | |
| 87 | + | return app.SendReloadSignal() | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // SetVersionInfo sets the version information for the CLI. | |
| 91 | + | func SetVersionInfo(version, commit, date string) { | |
| 92 | + | Version = version | |
| 93 | + | Commit = commit | |
| 94 | + | BuildDate = date | |
| 95 | + | } | |
A internal/adapters/in/http/middleware/auth.go +75 -0
| @@ -0,0 +1,75 @@ | |||
| 1 | + | package middleware | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "crypto/subtle" | |
| 5 | + | "net/http" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/zerowrap" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // RegistryAuth middleware provides Docker Registry authentication. | |
| 11 | + | func RegistryAuth(username, password string, log zerowrap.Logger) func(http.Handler) http.Handler { | |
| 12 | + | return func(next http.Handler) http.Handler { | |
| 13 | + | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 14 | + | // Require auth for all registry operations | |
| 15 | + | if !isAuthenticated(r, username, password, log) { | |
| 16 | + | w.Header().Set("WWW-Authenticate", `Basic realm="Gordon Registry"`) | |
| 17 | + | w.Header().Set("Docker-Distribution-API-Version", "registry/2.0") | |
| 18 | + | http.Error(w, "Unauthorized", http.StatusUnauthorized) | |
| 19 | + | ||
| 20 | + | log.Warn(). | |
| 21 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 22 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 23 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 24 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 25 | + | Str(zerowrap.FieldClientIP, r.RemoteAddr). | |
| 26 | + | Msg("unauthorized registry access attempt") | |
| 27 | + | return | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | next.ServeHTTP(w, r) | |
| 31 | + | }) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // isAuthenticated checks basic auth credentials. | |
| 36 | + | func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string, log zerowrap.Logger) bool { | |
| 37 | + | authHeader := r.Header.Get("Authorization") | |
| 38 | + | log.Debug(). | |
| 39 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 40 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 41 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 42 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 43 | + | Bool("has_auth_header", authHeader != ""). | |
| 44 | + | Msg("processing authentication request") | |
| 45 | + | ||
| 46 | + | username, password, ok := r.BasicAuth() | |
| 47 | + | if !ok { | |
| 48 | + | log.Debug(). | |
| 49 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 50 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 51 | + | Msg("no basic auth provided") | |
| 52 | + | return false | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // Use constant-time comparison to prevent timing attacks | |
| 56 | + | usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername)) == 1 | |
| 57 | + | passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1 | |
| 58 | + | ||
| 59 | + | authenticated := usernameMatch && passwordMatch | |
| 60 | + | if !authenticated { | |
| 61 | + | log.Debug(). | |
| 62 | + | Str("provided_username", username). | |
| 63 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 64 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 65 | + | Msg("authentication failed") | |
| 66 | + | } else { | |
| 67 | + | log.Debug(). | |
| 68 | + | Str("username", username). | |
| 69 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 70 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 71 | + | Msg("authentication successful") | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | return authenticated | |
| 75 | + | } | |
R internal/middleware/logging.go → internal/adapters/in/http/middleware/logging.go +71 -96 renamed, 53% similar
| @@ -1,20 +1,21 @@ | |||
| 1 | + | // Package middleware provides HTTP middleware for the adapters layer. | |
| 1 | 2 | package middleware | |
| 2 | 3 | ||
| 3 | 4 | import ( | |
| 4 | 5 | "net/http" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | - | "gordon/internal/logging" | |
| 8 | - | "github.com/rs/zerolog/log" | |
| 8 | + | "github.com/bnema/zerowrap" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | - | // ResponseWriter wraps http.ResponseWriter to capture status code and bytes written | |
| 11 | + | // ResponseWriter wraps http.ResponseWriter to capture status code and bytes written. | |
| 12 | 12 | type ResponseWriter struct { | |
| 13 | 13 | http.ResponseWriter | |
| 14 | 14 | statusCode int | |
| 15 | 15 | bytes int | |
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | + | // NewResponseWriter creates a new wrapped response writer. | |
| 18 | 19 | func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { | |
| 19 | 20 | return &ResponseWriter{ | |
| 20 | 21 | ResponseWriter: w, | |
| @@ -22,60 +23,68 @@ func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { | |||
| 22 | 23 | } | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 26 | + | // WriteHeader captures the status code. | |
| 25 | 27 | func (rw *ResponseWriter) WriteHeader(code int) { | |
| 26 | 28 | rw.statusCode = code | |
| 27 | 29 | rw.ResponseWriter.WriteHeader(code) | |
| 28 | 30 | } | |
| 29 | 31 | ||
| 32 | + | // Write captures bytes written. | |
| 30 | 33 | func (rw *ResponseWriter) Write(b []byte) (int, error) { | |
| 31 | 34 | n, err := rw.ResponseWriter.Write(b) | |
| 32 | 35 | rw.bytes += n | |
| 33 | 36 | return n, err | |
| 34 | 37 | } | |
| 35 | 38 | ||
| 39 | + | // StatusCode returns the captured status code. | |
| 36 | 40 | func (rw *ResponseWriter) StatusCode() int { | |
| 37 | 41 | return rw.statusCode | |
| 38 | 42 | } | |
| 39 | 43 | ||
| 44 | + | // BytesWritten returns the number of bytes written. | |
| 40 | 45 | func (rw *ResponseWriter) BytesWritten() int { | |
| 41 | 46 | return rw.bytes | |
| 42 | 47 | } | |
| 43 | 48 | ||
| 44 | - | // RequestLogger is a middleware that logs HTTP requests | |
| 45 | - | func RequestLogger(next http.Handler) http.Handler { | |
| 46 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 47 | - | start := time.Now() | |
| 48 | - | ||
| 49 | - | // Wrap the response writer to capture status and bytes | |
| 50 | - | rw := NewResponseWriter(w) | |
| 51 | - | ||
| 52 | - | // Get client IP (handle X-Forwarded-For and X-Real-IP headers) | |
| 53 | - | clientIP := getClientIP(r) | |
| 54 | - | ||
| 55 | - | // Call the next handler | |
| 56 | - | next.ServeHTTP(rw, r) | |
| 57 | - | ||
| 58 | - | // Calculate duration | |
| 59 | - | duration := time.Since(start) | |
| 60 | - | ||
| 61 | - | // Log the request using proxy logger | |
| 62 | - | logging.ProxyLogger.Info(). | |
| 63 | - | Str("method", r.Method). | |
| 64 | - | Str("path", r.URL.Path). | |
| 65 | - | Str("query", r.URL.RawQuery). | |
| 66 | - | Str("host", r.Host). | |
| 67 | - | Str("user_agent", r.UserAgent()). | |
| 68 | - | Str("referer", r.Referer()). | |
| 69 | - | Str("client_ip", clientIP). | |
| 70 | - | Int("status", rw.StatusCode()). | |
| 71 | - | Int("bytes", rw.BytesWritten()). | |
| 72 | - | Dur("duration", duration). | |
| 73 | - | Str("proto", r.Proto). | |
| 74 | - | Msg("HTTP Request") | |
| 75 | - | }) | |
| 49 | + | // RequestLogger is a middleware that logs HTTP requests using zerowrap. | |
| 50 | + | func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler { | |
| 51 | + | return func(next http.Handler) http.Handler { | |
| 52 | + | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 53 | + | start := time.Now() | |
| 54 | + | ||
| 55 | + | // Wrap the response writer to capture status and bytes | |
| 56 | + | rw := NewResponseWriter(w) | |
| 57 | + | ||
| 58 | + | // Get client IP (handle X-Forwarded-For and X-Real-IP headers) | |
| 59 | + | clientIP := getClientIP(r) | |
| 60 | + | ||
| 61 | + | // Call the next handler | |
| 62 | + | next.ServeHTTP(rw, r) | |
| 63 | + | ||
| 64 | + | // Calculate duration | |
| 65 | + | duration := time.Since(start) | |
| 66 | + | ||
| 67 | + | // Log the request | |
| 68 | + | log.Info(). | |
| 69 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 70 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 71 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 72 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 73 | + | Str("query", r.URL.RawQuery). | |
| 74 | + | Str(zerowrap.FieldHost, r.Host). | |
| 75 | + | Str("user_agent", r.UserAgent()). | |
| 76 | + | Str("referer", r.Referer()). | |
| 77 | + | Str(zerowrap.FieldClientIP, clientIP). | |
| 78 | + | Int(zerowrap.FieldStatus, rw.StatusCode()). | |
| 79 | + | Int("bytes", rw.BytesWritten()). | |
| 80 | + | Dur(zerowrap.FieldDuration, duration). | |
| 81 | + | Str("proto", r.Proto). | |
| 82 | + | Msg("HTTP request") | |
| 83 | + | }) | |
| 84 | + | } | |
| 76 | 85 | } | |
| 77 | 86 | ||
| 78 | - | // getClientIP extracts the client IP from various headers | |
| 87 | + | // getClientIP extracts the client IP from various headers. | |
| 79 | 88 | func getClientIP(r *http.Request) string { | |
| 80 | 89 | // Check X-Forwarded-For header (comma-separated list, first is original client) | |
| 81 | 90 | if xff := r.Header.Get("X-Forwarded-For"); xff != "" { | |
| @@ -87,97 +96,63 @@ func getClientIP(r *http.Request) string { | |||
| 87 | 96 | } | |
| 88 | 97 | return xff | |
| 89 | 98 | } | |
| 90 | - | ||
| 99 | + | ||
| 91 | 100 | // Check X-Real-IP header | |
| 92 | 101 | if xri := r.Header.Get("X-Real-IP"); xri != "" { | |
| 93 | 102 | return xri | |
| 94 | 103 | } | |
| 95 | - | ||
| 104 | + | ||
| 96 | 105 | // Check X-Forwarded header (less common) | |
| 97 | 106 | if xf := r.Header.Get("X-Forwarded"); xf != "" { | |
| 98 | 107 | return xf | |
| 99 | 108 | } | |
| 100 | - | ||
| 109 | + | ||
| 101 | 110 | // Fallback to RemoteAddr | |
| 102 | 111 | return r.RemoteAddr | |
| 103 | 112 | } | |
| 104 | 113 | ||
| 105 | - | // ContainerRequestLogger logs requests with container-specific information | |
| 106 | - | func ContainerRequestLogger(containerID string, domain string) func(http.Handler) http.Handler { | |
| 114 | + | // PanicRecovery middleware recovers from panics and logs them. | |
| 115 | + | func PanicRecovery(log zerowrap.Logger) func(http.Handler) http.Handler { | |
| 107 | 116 | return func(next http.Handler) http.Handler { | |
| 108 | 117 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 109 | - | start := time.Now() | |
| 110 | - | ||
| 111 | - | // Wrap the response writer to capture status and bytes | |
| 112 | - | rw := NewResponseWriter(w) | |
| 113 | - | ||
| 114 | - | // Get client IP | |
| 115 | - | clientIP := getClientIP(r) | |
| 116 | - | ||
| 117 | - | // Call the next handler | |
| 118 | - | next.ServeHTTP(rw, r) | |
| 119 | - | ||
| 120 | - | // Calculate duration | |
| 121 | - | duration := time.Since(start) | |
| 122 | - | ||
| 123 | - | // Log the request with container information using proxy logger | |
| 124 | - | logging.ProxyLogger.Info(). | |
| 125 | - | Str("method", r.Method). | |
| 126 | - | Str("path", r.URL.Path). | |
| 127 | - | Str("query", r.URL.RawQuery). | |
| 128 | - | Str("host", r.Host). | |
| 129 | - | Str("domain", domain). | |
| 130 | - | Str("container_id", containerID). | |
| 131 | - | Str("user_agent", r.UserAgent()). | |
| 132 | - | Str("referer", r.Referer()). | |
| 133 | - | Str("client_ip", clientIP). | |
| 134 | - | Int("status", rw.StatusCode()). | |
| 135 | - | Int("bytes", rw.BytesWritten()). | |
| 136 | - | Dur("duration", duration). | |
| 137 | - | Str("proto", r.Proto). | |
| 138 | - | Msg("Container Request") | |
| 118 | + | defer func() { | |
| 119 | + | if err := recover(); err != nil { | |
| 120 | + | log.Error(). | |
| 121 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 122 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 123 | + | Interface("panic", err). | |
| 124 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 125 | + | Str(zerowrap.FieldPath, r.URL.Path). | |
| 126 | + | Str(zerowrap.FieldHost, r.Host). | |
| 127 | + | Msg("panic recovered") | |
| 128 | + | ||
| 129 | + | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 130 | + | } | |
| 131 | + | }() | |
| 132 | + | ||
| 133 | + | next.ServeHTTP(w, r) | |
| 139 | 134 | }) | |
| 140 | 135 | } | |
| 141 | 136 | } | |
| 142 | 137 | ||
| 143 | - | // PanicRecovery middleware recovers from panics and logs them | |
| 144 | - | func PanicRecovery(next http.Handler) http.Handler { | |
| 145 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 146 | - | defer func() { | |
| 147 | - | if err := recover(); err != nil { | |
| 148 | - | log.Error(). | |
| 149 | - | Interface("panic", err). | |
| 150 | - | Str("method", r.Method). | |
| 151 | - | Str("path", r.URL.Path). | |
| 152 | - | Str("host", r.Host). | |
| 153 | - | Msg("Panic recovered") | |
| 154 | - | ||
| 155 | - | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 156 | - | } | |
| 157 | - | }() | |
| 158 | - | ||
| 159 | - | next.ServeHTTP(w, r) | |
| 160 | - | }) | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // CORS middleware adds CORS headers | |
| 138 | + | // CORS middleware adds CORS headers. | |
| 164 | 139 | func CORS(next http.Handler) http.Handler { | |
| 165 | 140 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 166 | 141 | w.Header().Set("Access-Control-Allow-Origin", "*") | |
| 167 | 142 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") | |
| 168 | 143 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") | |
| 169 | - | ||
| 144 | + | ||
| 170 | 145 | // Handle preflight OPTIONS request | |
| 171 | 146 | if r.Method == "OPTIONS" { | |
| 172 | 147 | w.WriteHeader(http.StatusOK) | |
| 173 | 148 | return | |
| 174 | 149 | } | |
| 175 | - | ||
| 150 | + | ||
| 176 | 151 | next.ServeHTTP(w, r) | |
| 177 | 152 | }) | |
| 178 | 153 | } | |
| 179 | 154 | ||
| 180 | - | // Chain combines multiple middleware functions | |
| 155 | + | // Chain combines multiple middleware functions. | |
| 181 | 156 | func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { | |
| 182 | 157 | return func(final http.Handler) http.Handler { | |
| 183 | 158 | for i := len(middlewares) - 1; i >= 0; i-- { | |
| @@ -185,4 +160,4 @@ func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) ht | |||
| 185 | 160 | } | |
| 186 | 161 | return final | |
| 187 | 162 | } | |
| 188 | - | }⏎̸ | |
| 163 | + | } | |
A internal/adapters/in/http/proxy/handler.go +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | // Package proxy implements the HTTP adapter for the reverse proxy. | |
| 2 | + | package proxy | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "net/http" | |
| 7 | + | "strconv" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/zerowrap" | |
| 11 | + | ||
| 12 | + | "gordon/internal/boundaries/in" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // Handler wraps the proxy service for HTTP. | |
| 16 | + | // The actual proxying logic is in the usecase layer (ProxyService implements http.Handler). | |
| 17 | + | type Handler struct { | |
| 18 | + | proxySvc in.ProxyService | |
| 19 | + | log zerowrap.Logger | |
| 20 | + | port int | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // NewHandler creates a new proxy HTTP handler. | |
| 24 | + | func NewHandler(proxySvc in.ProxyService, port int, log zerowrap.Logger) *Handler { | |
| 25 | + | return &Handler{ | |
| 26 | + | proxySvc: proxySvc, | |
| 27 | + | port: port, | |
| 28 | + | log: log, | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // ServeHTTP implements http.Handler by delegating to the proxy service. | |
| 33 | + | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 34 | + | h.proxySvc.ServeHTTP(w, r) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Start starts the proxy HTTP server. | |
| 38 | + | func (h *Handler) Start(ctx context.Context, handler http.Handler) error { | |
| 39 | + | addr := ":" + strconv.Itoa(h.port) | |
| 40 | + | ||
| 41 | + | server := &http.Server{ | |
| 42 | + | Addr: addr, | |
| 43 | + | Handler: handler, | |
| 44 | + | ReadHeaderTimeout: 10 * time.Second, | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | h.log.Info(). | |
| 48 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 49 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 50 | + | Str("address", addr). | |
| 51 | + | Msg("proxy server starting") | |
| 52 | + | ||
| 53 | + | // Start server in a goroutine | |
| 54 | + | errChan := make(chan error, 1) | |
| 55 | + | go func() { | |
| 56 | + | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| 57 | + | errChan <- err | |
| 58 | + | } | |
| 59 | + | }() | |
| 60 | + | ||
| 61 | + | // Wait for context cancellation or server error | |
| 62 | + | select { | |
| 63 | + | case err := <-errChan: | |
| 64 | + | return err | |
| 65 | + | case <-ctx.Done(): | |
| 66 | + | h.log.Info(). | |
| 67 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 68 | + | Str(zerowrap.FieldAdapter, "http"). | |
| 69 | + | Msg("proxy server shutting down") | |
| 70 | + | return server.Shutdown(context.Background()) | |
| 71 | + | } | |
| 72 | + | } | |
R internal/registry/server.go → internal/adapters/in/http/registry/handler.go +136 -207 renamed, 50% similar
| @@ -1,7 +1,7 @@ | |||
| 1 | + | // Package registry implements the HTTP adapter for the registry API. | |
| 1 | 2 | package registry | |
| 2 | 3 | ||
| 3 | 4 | import ( | |
| 4 | - | "context" | |
| 5 | 5 | "crypto/sha256" | |
| 6 | 6 | "encoding/json" | |
| 7 | 7 | "fmt" | |
| @@ -10,131 +10,139 @@ import ( | |||
| 10 | 10 | "strconv" | |
| 11 | 11 | "strings" | |
| 12 | 12 | ||
| 13 | - | "gordon/internal/config" | |
| 14 | - | "gordon/internal/events" | |
| 15 | - | "gordon/internal/middleware" | |
| 16 | - | "gordon/pkg/manifest" | |
| 13 | + | "github.com/bnema/zerowrap" | |
| 17 | 14 | ||
| 18 | - | "github.com/rs/zerolog/log" | |
| 15 | + | "gordon/internal/boundaries/in" | |
| 16 | + | "gordon/internal/boundaries/out" | |
| 17 | + | "gordon/internal/domain" | |
| 18 | + | "gordon/pkg/manifest" | |
| 19 | 19 | ) | |
| 20 | 20 | ||
| 21 | - | type Server struct { | |
| 22 | - | config *config.Config | |
| 23 | - | mux *http.ServeMux | |
| 24 | - | storage Storage | |
| 25 | - | eventBus events.EventBus | |
| 21 | + | // Handler implements the HTTP handler for Docker Registry API v2. | |
| 22 | + | type Handler struct { | |
| 23 | + | registrySvc in.RegistryService | |
| 24 | + | blobStorage out.BlobStorage | |
| 25 | + | eventBus out.EventPublisher | |
| 26 | + | log zerowrap.Logger | |
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | - | func NewServer(cfg *config.Config, eventBus events.EventBus) (*Server, error) { | |
| 29 | - | // Initialize storage | |
| 30 | - | registryPath := cfg.Server.DataDir + "/registry" | |
| 31 | - | log.Debug().Str("data_dir", cfg.Server.DataDir).Str("registry_path", registryPath).Msg("Initializing registry storage") | |
| 32 | - | ||
| 33 | - | storage, err := NewFilesystemStorage(registryPath) | |
| 34 | - | if err != nil { | |
| 35 | - | return nil, fmt.Errorf("failed to initialize storage: %w", err) | |
| 29 | + | // NewHandler creates a new registry HTTP handler. | |
| 30 | + | func NewHandler( | |
| 31 | + | registrySvc in.RegistryService, | |
| 32 | + | blobStorage out.BlobStorage, | |
| 33 | + | eventBus out.EventPublisher, | |
| 34 | + | log zerowrap.Logger, | |
| 35 | + | ) *Handler { | |
| 36 | + | return &Handler{ | |
| 37 | + | registrySvc: registrySvc, | |
| 38 | + | blobStorage: blobStorage, | |
| 39 | + | eventBus: eventBus, | |
| 40 | + | log: log, | |
| 36 | 41 | } | |
| 42 | + | } | |
| 37 | 43 | ||
| 38 | - | s := &Server{ | |
| 39 | - | config: cfg, | |
| 40 | - | mux: http.NewServeMux(), | |
| 41 | - | storage: storage, | |
| 42 | - | eventBus: eventBus, | |
| 43 | - | } | |
| 44 | - | s.setupRoutes() | |
| 45 | - | return s, nil | |
| 44 | + | // RegisterRoutes registers the registry routes on the given mux. | |
| 45 | + | func (h *Handler) RegisterRoutes(mux *http.ServeMux) { | |
| 46 | + | mux.HandleFunc("/v2/", h.handleRegistryRoutes) | |
| 46 | 47 | } | |
| 47 | 48 | ||
| 48 | - | func (s *Server) setupRoutes() { | |
| 49 | - | // Docker Registry API v2 endpoints | |
| 50 | - | // Use traditional patterns since repository names can contain slashes | |
| 51 | - | s.mux.HandleFunc("/v2/", s.handleRegistryRoutes) | |
| 49 | + | // ServeHTTP implements http.Handler. | |
| 50 | + | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 51 | + | h.handleRegistryRoutes(w, r) | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | - | func (s *Server) handleRegistryRoutes(w http.ResponseWriter, r *http.Request) { | |
| 54 | + | func (h *Handler) handleRegistryRoutes(w http.ResponseWriter, r *http.Request) { | |
| 55 | + | ctx := zerowrap.CtxWithFields(r.Context(), map[string]any{ | |
| 56 | + | zerowrap.FieldLayer: "adapter", | |
| 57 | + | zerowrap.FieldAdapter: "http", | |
| 58 | + | zerowrap.FieldHandler: "registry", | |
| 59 | + | zerowrap.FieldMethod: r.Method, | |
| 60 | + | zerowrap.FieldPath: r.URL.Path, | |
| 61 | + | }) | |
| 62 | + | r = r.WithContext(ctx) | |
| 63 | + | ||
| 55 | 64 | path := r.URL.Path | |
| 56 | - | ||
| 65 | + | ||
| 57 | 66 | // Route manifest operations: /v2/{name}/manifests/{reference} | |
| 58 | 67 | if strings.Contains(path, "/manifests/") { | |
| 59 | - | s.handleManifestRoutes(w, r) | |
| 68 | + | h.handleManifestRoutes(w, r) | |
| 60 | 69 | return | |
| 61 | 70 | } | |
| 62 | - | ||
| 71 | + | ||
| 63 | 72 | // Route blob operations: /v2/{name}/blobs/{digest} | |
| 64 | 73 | if strings.Contains(path, "/blobs/") && !strings.Contains(path, "/uploads/") { | |
| 65 | - | s.handleBlobRoutes(w, r) | |
| 74 | + | h.handleBlobRoutes(w, r) | |
| 66 | 75 | return | |
| 67 | 76 | } | |
| 68 | - | ||
| 77 | + | ||
| 69 | 78 | // Route blob upload operations: /v2/{name}/blobs/uploads/ | |
| 70 | 79 | if strings.Contains(path, "/blobs/uploads/") { | |
| 71 | - | s.handleBlobUploadRoutes(w, r) | |
| 80 | + | h.handleBlobUploadRoutes(w, r) | |
| 72 | 81 | return | |
| 73 | 82 | } | |
| 74 | - | ||
| 83 | + | ||
| 75 | 84 | // Route tag list operations: /v2/{name}/tags/list | |
| 76 | 85 | if strings.Contains(path, "/tags/list") { | |
| 77 | - | s.handleTagListRoutes(w, r) | |
| 86 | + | h.handleTagListRoutes(w, r) | |
| 78 | 87 | return | |
| 79 | 88 | } | |
| 80 | - | ||
| 89 | + | ||
| 81 | 90 | // Base endpoint: /v2/ | |
| 82 | 91 | if path == "/v2/" { | |
| 83 | - | s.handleBase(w, r) | |
| 92 | + | h.handleBase(w, r) | |
| 84 | 93 | return | |
| 85 | 94 | } | |
| 86 | - | ||
| 95 | + | ||
| 87 | 96 | http.NotFound(w, r) | |
| 88 | 97 | } | |
| 89 | 98 | ||
| 90 | - | func (s *Server) handleManifestRoutes(w http.ResponseWriter, r *http.Request) { | |
| 99 | + | func (h *Handler) handleManifestRoutes(w http.ResponseWriter, r *http.Request) { | |
| 91 | 100 | // Parse path: /v2/{name}/manifests/{reference} | |
| 92 | 101 | parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v2/"), "/") | |
| 93 | 102 | if len(parts) < 3 || parts[len(parts)-2] != "manifests" { | |
| 94 | 103 | http.NotFound(w, r) | |
| 95 | 104 | return | |
| 96 | 105 | } | |
| 97 | - | ||
| 106 | + | ||
| 98 | 107 | reference := parts[len(parts)-1] | |
| 99 | 108 | name := strings.Join(parts[:len(parts)-2], "/") | |
| 100 | - | ||
| 101 | - | // Set path values for compatibility with existing handlers | |
| 109 | + | ||
| 102 | 110 | r.SetPathValue("name", name) | |
| 103 | 111 | r.SetPathValue("reference", reference) | |
| 104 | - | ||
| 112 | + | ||
| 105 | 113 | switch r.Method { | |
| 106 | 114 | case "HEAD", "GET": | |
| 107 | - | s.handleGetManifest(w, r) | |
| 115 | + | h.handleGetManifest(w, r) | |
| 108 | 116 | case "PUT": | |
| 109 | - | s.handlePutManifest(w, r) | |
| 117 | + | h.handlePutManifest(w, r) | |
| 110 | 118 | default: | |
| 111 | 119 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 112 | 120 | } | |
| 113 | 121 | } | |
| 114 | 122 | ||
| 115 | - | func (s *Server) handleBlobRoutes(w http.ResponseWriter, r *http.Request) { | |
| 123 | + | func (h *Handler) handleBlobRoutes(w http.ResponseWriter, r *http.Request) { | |
| 116 | 124 | // Parse path: /v2/{name}/blobs/{digest} | |
| 117 | 125 | parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v2/"), "/") | |
| 118 | 126 | if len(parts) < 3 || parts[len(parts)-2] != "blobs" { | |
| 119 | 127 | http.NotFound(w, r) | |
| 120 | 128 | return | |
| 121 | 129 | } | |
| 122 | - | ||
| 130 | + | ||
| 123 | 131 | digest := parts[len(parts)-1] | |
| 124 | 132 | name := strings.Join(parts[:len(parts)-2], "/") | |
| 125 | - | ||
| 133 | + | ||
| 126 | 134 | r.SetPathValue("name", name) | |
| 127 | 135 | r.SetPathValue("digest", digest) | |
| 128 | - | ||
| 136 | + | ||
| 129 | 137 | switch r.Method { | |
| 130 | 138 | case "HEAD", "GET": | |
| 131 | - | s.handleGetBlob(w, r) | |
| 139 | + | h.handleGetBlob(w, r) | |
| 132 | 140 | default: | |
| 133 | 141 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 134 | 142 | } | |
| 135 | 143 | } | |
| 136 | 144 | ||
| 137 | - | func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) { | |
| 145 | + | func (h *Handler) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) { | |
| 138 | 146 | // Parse path: /v2/{name}/blobs/uploads/{uuid?} | |
| 139 | 147 | path := strings.TrimPrefix(r.URL.Path, "/v2/") | |
| 140 | 148 | uploadIndex := strings.Index(path, "/blobs/uploads/") | |
| @@ -142,17 +150,17 @@ func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) | |||
| 142 | 150 | http.NotFound(w, r) | |
| 143 | 151 | return | |
| 144 | 152 | } | |
| 145 | - | ||
| 153 | + | ||
| 146 | 154 | name := path[:uploadIndex] | |
| 147 | 155 | uploadPart := path[uploadIndex+15:] // len("/blobs/uploads/") = 15 | |
| 148 | - | ||
| 156 | + | ||
| 149 | 157 | r.SetPathValue("name", name) | |
| 150 | - | ||
| 158 | + | ||
| 151 | 159 | if uploadPart == "" { | |
| 152 | 160 | // POST /v2/{name}/blobs/uploads/ | |
| 153 | 161 | switch r.Method { | |
| 154 | 162 | case "POST": | |
| 155 | - | s.handleStartBlobUpload(w, r) | |
| 163 | + | h.handleStartBlobUpload(w, r) | |
| 156 | 164 | default: | |
| 157 | 165 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 158 | 166 | } | |
| @@ -161,73 +169,64 @@ func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) | |||
| 161 | 169 | r.SetPathValue("uuid", uploadPart) | |
| 162 | 170 | switch r.Method { | |
| 163 | 171 | case "PATCH", "PUT": | |
| 164 | - | s.handleBlobUpload(w, r) | |
| 172 | + | h.handleBlobUpload(w, r) | |
| 165 | 173 | default: | |
| 166 | 174 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 167 | 175 | } | |
| 168 | 176 | } | |
| 169 | 177 | } | |
| 170 | 178 | ||
| 171 | - | func (s *Server) handleTagListRoutes(w http.ResponseWriter, r *http.Request) { | |
| 179 | + | func (h *Handler) handleTagListRoutes(w http.ResponseWriter, r *http.Request) { | |
| 172 | 180 | // Parse path: /v2/{name}/tags/list | |
| 173 | 181 | path := strings.TrimPrefix(r.URL.Path, "/v2/") | |
| 174 | 182 | if !strings.HasSuffix(path, "/tags/list") { | |
| 175 | 183 | http.NotFound(w, r) | |
| 176 | 184 | return | |
| 177 | 185 | } | |
| 178 | - | ||
| 186 | + | ||
| 179 | 187 | name := strings.TrimSuffix(path, "/tags/list") | |
| 180 | 188 | r.SetPathValue("name", name) | |
| 181 | - | ||
| 189 | + | ||
| 182 | 190 | switch r.Method { | |
| 183 | 191 | case "GET": | |
| 184 | - | s.handleListTags(w, r) | |
| 192 | + | h.handleListTags(w, r) | |
| 185 | 193 | default: | |
| 186 | 194 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 187 | 195 | } | |
| 188 | 196 | } | |
| 189 | 197 | ||
| 190 | - | func (s *Server) handleBase(w http.ResponseWriter, r *http.Request) { | |
| 191 | - | // For Docker clients, we need to respond with a 200 OK to this endpoint | |
| 192 | - | // even if authentication is required. The client will then check the | |
| 193 | - | // WWW-Authenticate header. | |
| 194 | - | // see: https://docs.docker.com/registry/spec/api/#base | |
| 195 | - | // | |
| 196 | - | // Podman, as of v4.4.1, will not send credentials in the initial /v2/ | |
| 197 | - | // request. It expects a 401 Unauthorized to then send credentials. | |
| 198 | - | // | |
| 199 | - | // To support both, we'll check for the "Docker-Client" user agent. | |
| 200 | - | // For now, we will just return a 200 OK to all clients. | |
| 198 | + | func (h *Handler) handleBase(w http.ResponseWriter, r *http.Request) { | |
| 201 | 199 | w.Header().Set("Docker-Distribution-API-Version", "registry/2.0") | |
| 202 | 200 | w.WriteHeader(http.StatusOK) | |
| 203 | 201 | } | |
| 204 | 202 | ||
| 205 | - | func (s *Server) handleGetManifest(w http.ResponseWriter, r *http.Request) { | |
| 203 | + | func (h *Handler) handleGetManifest(w http.ResponseWriter, r *http.Request) { | |
| 204 | + | ctx := r.Context() | |
| 205 | + | log := zerowrap.FromCtx(ctx) | |
| 206 | 206 | name := r.PathValue("name") | |
| 207 | 207 | reference := r.PathValue("reference") | |
| 208 | 208 | ||
| 209 | 209 | log.Debug().Str("name", name).Str("reference", reference).Msg("GET manifest") | |
| 210 | 210 | ||
| 211 | - | manifest, contentType, err := s.storage.GetManifest(name, reference) | |
| 211 | + | manifestData, err := h.registrySvc.GetManifest(ctx, name, reference) | |
| 212 | 212 | if err != nil { | |
| 213 | - | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Manifest not found") | |
| 213 | + | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("manifest not found") | |
| 214 | 214 | http.NotFound(w, r) | |
| 215 | 215 | return | |
| 216 | 216 | } | |
| 217 | 217 | ||
| 218 | - | log.Debug().Str("name", name).Str("reference", reference).Str("content_type", contentType).Msg("GET manifest - serving") | |
| 219 | - | ||
| 220 | - | // Return the appropriate content type based on what was stored | |
| 221 | - | w.Header().Set("Content-Type", contentType) | |
| 222 | - | w.Header().Set("Content-Length", strconv.Itoa(len(manifest))) | |
| 218 | + | w.Header().Set("Content-Type", manifestData.ContentType) | |
| 219 | + | w.Header().Set("Content-Length", strconv.Itoa(len(manifestData.Data))) | |
| 223 | 220 | w.WriteHeader(http.StatusOK) | |
| 224 | 221 | ||
| 225 | 222 | if r.Method == "GET" { | |
| 226 | - | _, _ = w.Write(manifest) | |
| 223 | + | _, _ = w.Write(manifestData.Data) | |
| 227 | 224 | } | |
| 228 | 225 | } | |
| 229 | 226 | ||
| 230 | - | func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |
| 227 | + | func (h *Handler) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |
| 228 | + | ctx := r.Context() | |
| 229 | + | log := zerowrap.FromCtx(ctx) | |
| 231 | 230 | name := r.PathValue("name") | |
| 232 | 231 | reference := r.PathValue("reference") | |
| 233 | 232 | ||
| @@ -235,7 +234,7 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||
| 235 | 234 | log.Debug().Str("name", name).Str("reference", reference).Str("content_type", contentType).Msg("PUT manifest") | |
| 236 | 235 | ||
| 237 | 236 | if contentType == "" { | |
| 238 | - | log.Warn().Str("name", name).Str("reference", reference).Msg("Content-Type header missing for PUT manifest") | |
| 237 | + | log.Warn().Str("name", name).Str("reference", reference).Msg("Content-Type header missing") | |
| 239 | 238 | http.Error(w, "Content-Type header required", http.StatusBadRequest) | |
| 240 | 239 | return | |
| 241 | 240 | } | |
| @@ -243,58 +242,51 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||
| 243 | 242 | // Read manifest data | |
| 244 | 243 | data, err := io.ReadAll(r.Body) | |
| 245 | 244 | if err != nil { | |
| 246 | - | log.Error().Err(err).Msg("Failed to read manifest data") | |
| 245 | + | log.Error().Err(err).Msg("failed to read manifest data") | |
| 247 | 246 | http.Error(w, "Bad Request", http.StatusBadRequest) | |
| 248 | 247 | return | |
| 249 | 248 | } | |
| 250 | 249 | ||
| 251 | - | // Store manifest | |
| 252 | - | if err := s.storage.PutManifest(name, reference, contentType, data); err != nil { | |
| 253 | - | log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to store manifest") | |
| 254 | - | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 255 | - | return | |
| 256 | - | } | |
| 257 | - | ||
| 258 | - | // Calculate digest for Docker-Content-Digest header | |
| 259 | - | digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data)) | |
| 260 | - | w.Header().Set("Docker-Content-Digest", digest) | |
| 261 | - | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | |
| 262 | - | w.WriteHeader(http.StatusCreated) | |
| 263 | - | ||
| 264 | 250 | // Parse manifest annotations | |
| 265 | 251 | annotations, err := manifest.ParseManifestAnnotations(data, contentType) | |
| 266 | 252 | if err != nil { | |
| 267 | - | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to parse manifest annotations, continuing without them") | |
| 253 | + | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("failed to parse manifest annotations") | |
| 268 | 254 | annotations = map[string]string{} | |
| 269 | 255 | } | |
| 270 | 256 | ||
| 271 | - | // Log discovered annotations | |
| 272 | - | if len(annotations) > 0 { | |
| 273 | - | log.Info(). | |
| 274 | - | Str("name", name). | |
| 275 | - | Str("reference", reference). | |
| 276 | - | Interface("annotations", annotations). | |
| 277 | - | Msg("Manifest annotations discovered") | |
| 278 | - | } | |
| 279 | - | ||
| 280 | - | // Emit image pushed event | |
| 281 | - | s.eventBus.Publish(events.ImagePushed, events.ImagePushedPayload{ | |
| 257 | + | // Store manifest via service | |
| 258 | + | manifestObj := &domain.Manifest{ | |
| 282 | 259 | Name: name, | |
| 283 | 260 | Reference: reference, | |
| 284 | - | Manifest: data, | |
| 261 | + | ContentType: contentType, | |
| 262 | + | Data: data, | |
| 285 | 263 | Annotations: annotations, | |
| 286 | - | }) | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | if err := h.registrySvc.PutManifest(ctx, manifestObj); err != nil { | |
| 267 | + | log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("failed to store manifest") | |
| 268 | + | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 269 | + | return | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // Calculate digest for Docker-Content-Digest header | |
| 273 | + | digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data)) | |
| 274 | + | w.Header().Set("Docker-Content-Digest", digest) | |
| 275 | + | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | |
| 276 | + | w.WriteHeader(http.StatusCreated) | |
| 287 | 277 | } | |
| 288 | 278 | ||
| 289 | - | func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) { | |
| 279 | + | func (h *Handler) handleGetBlob(w http.ResponseWriter, r *http.Request) { | |
| 280 | + | ctx := r.Context() | |
| 281 | + | log := zerowrap.FromCtx(ctx) | |
| 290 | 282 | name := r.PathValue("name") | |
| 291 | 283 | digest := r.PathValue("digest") | |
| 292 | 284 | ||
| 293 | 285 | log.Debug().Str("name", name).Str("digest", digest).Msg("GET blob") | |
| 294 | 286 | ||
| 295 | - | path, err := s.storage.GetBlobPath(digest) | |
| 287 | + | path, err := h.blobStorage.GetBlobPath(digest) | |
| 296 | 288 | if err != nil { | |
| 297 | - | log.Warn().Err(err).Str("name", name).Str("digest", digest).Msg("Blob not found") | |
| 289 | + | log.Warn().Err(err).Str("name", name).Str("digest", digest).Msg("blob not found") | |
| 298 | 290 | http.NotFound(w, r) | |
| 299 | 291 | return | |
| 300 | 292 | } | |
| @@ -303,26 +295,28 @@ func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) { | |||
| 303 | 295 | http.ServeFile(w, r, path) | |
| 304 | 296 | } | |
| 305 | 297 | ||
| 306 | - | func (s *Server) handleStartBlobUpload(w http.ResponseWriter, r *http.Request) { | |
| 298 | + | func (h *Handler) handleStartBlobUpload(w http.ResponseWriter, r *http.Request) { | |
| 299 | + | ctx := r.Context() | |
| 300 | + | log := zerowrap.FromCtx(ctx) | |
| 307 | 301 | name := r.PathValue("name") | |
| 308 | 302 | ||
| 309 | - | log.Debug().Str("name", name).Msg("Starting blob upload") | |
| 303 | + | log.Debug().Str("name", name).Msg("starting blob upload") | |
| 310 | 304 | ||
| 311 | - | // Generate a unique upload ID | |
| 312 | - | uuid, err := s.storage.StartBlobUpload(name) | |
| 305 | + | uuid, err := h.registrySvc.StartUpload(ctx, name) | |
| 313 | 306 | if err != nil { | |
| 314 | - | log.Error().Err(err).Msg("Failed to start blob upload") | |
| 307 | + | log.Error().Err(err).Msg("failed to start blob upload") | |
| 315 | 308 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 316 | 309 | return | |
| 317 | 310 | } | |
| 318 | 311 | ||
| 319 | - | // Set headers for the response | |
| 320 | 312 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid)) | |
| 321 | - | w.Header().Set("Range", "0-0") // Indicate that no bytes have been received yet | |
| 313 | + | w.Header().Set("Range", "0-0") | |
| 322 | 314 | w.WriteHeader(http.StatusAccepted) | |
| 323 | 315 | } | |
| 324 | 316 | ||
| 325 | - | func (s *Server) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |
| 317 | + | func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |
| 318 | + | ctx := r.Context() | |
| 319 | + | log := zerowrap.FromCtx(ctx) | |
| 326 | 320 | name := r.PathValue("name") | |
| 327 | 321 | uuid := r.PathValue("uuid") | |
| 328 | 322 | digest := r.URL.Query().Get("digest") | |
| @@ -331,36 +325,34 @@ func (s *Server) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 331 | 325 | Str("name", name). | |
| 332 | 326 | Str("uuid", uuid). | |
| 333 | 327 | Str("digest", digest). | |
| 334 | - | Str("method", r.Method). | |
| 335 | - | Msg("Handling blob upload chunk") | |
| 328 | + | Str(zerowrap.FieldMethod, r.Method). | |
| 329 | + | Msg("handling blob upload chunk") | |
| 336 | 330 | ||
| 337 | 331 | // Read the chunk from the request body | |
| 338 | 332 | chunk, err := io.ReadAll(r.Body) | |
| 339 | 333 | if err != nil { | |
| 340 | - | log.Error().Err(err).Msg("Failed to read blob chunk") | |
| 334 | + | log.Error().Err(err).Msg("failed to read blob chunk") | |
| 341 | 335 | http.Error(w, "Bad Request", http.StatusBadRequest) | |
| 342 | 336 | return | |
| 343 | 337 | } | |
| 344 | 338 | ||
| 345 | 339 | // Append the chunk to the upload | |
| 346 | - | length, err := s.storage.AppendBlobChunk(name, uuid, chunk) | |
| 340 | + | length, err := h.blobStorage.AppendBlobChunk(name, uuid, chunk) | |
| 347 | 341 | if err != nil { | |
| 348 | - | log.Error().Err(err).Msg("Failed to append blob chunk") | |
| 342 | + | log.Error().Err(err).Msg("failed to append blob chunk") | |
| 349 | 343 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 350 | 344 | return | |
| 351 | 345 | } | |
| 352 | 346 | ||
| 353 | 347 | // If this is the final chunk (PUT request with digest), finalize the upload | |
| 354 | 348 | if r.Method == "PUT" && digest != "" { | |
| 355 | - | if err := s.storage.FinishBlobUpload(uuid, digest); err != nil { | |
| 356 | - | log.Error().Err(err).Str("digest", digest).Msg("Failed to finalize blob upload") | |
| 357 | - | // The upload is invalid, delete it | |
| 358 | - | _ = s.storage.CancelBlobUpload(uuid) | |
| 349 | + | if err := h.registrySvc.FinishUpload(ctx, uuid, digest); err != nil { | |
| 350 | + | log.Error().Err(err).Str("digest", digest).Msg("failed to finalize blob upload") | |
| 351 | + | _ = h.registrySvc.CancelUpload(ctx, uuid) | |
| 359 | 352 | http.Error(w, "Bad Request: digest mismatch", http.StatusBadRequest) | |
| 360 | 353 | return | |
| 361 | 354 | } | |
| 362 | 355 | ||
| 363 | - | // Respond with 201 Created | |
| 364 | 356 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) | |
| 365 | 357 | w.Header().Set("Docker-Content-Digest", digest) | |
| 366 | 358 | w.WriteHeader(http.StatusCreated) | |
| @@ -373,26 +365,27 @@ func (s *Server) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 373 | 365 | w.WriteHeader(http.StatusAccepted) | |
| 374 | 366 | } | |
| 375 | 367 | ||
| 376 | - | func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) { | |
| 368 | + | func (h *Handler) handleListTags(w http.ResponseWriter, r *http.Request) { | |
| 369 | + | ctx := r.Context() | |
| 370 | + | log := zerowrap.FromCtx(ctx) | |
| 377 | 371 | name := r.PathValue("name") | |
| 378 | 372 | ||
| 379 | - | log.Debug().Str("name", name).Msg("Listing tags") | |
| 373 | + | log.Debug().Str("name", name).Msg("listing tags") | |
| 380 | 374 | ||
| 381 | - | tags, err := s.storage.ListTags(name) | |
| 375 | + | tags, err := h.registrySvc.ListTags(ctx, name) | |
| 382 | 376 | if err != nil { | |
| 383 | - | log.Warn().Err(err).Str("name", name).Msg("Tag not found") | |
| 377 | + | log.Warn().Err(err).Str("name", name).Msg("tags not found") | |
| 384 | 378 | w.WriteHeader(http.StatusNotFound) | |
| 385 | 379 | return | |
| 386 | 380 | } | |
| 387 | 381 | ||
| 388 | - | // Construct the response JSON properly | |
| 389 | 382 | tagsJSON, err := json.Marshal(tags) | |
| 390 | 383 | if err != nil { | |
| 391 | - | log.Error().Err(err).Str("name", name).Msg("Failed to marshal tags to JSON") | |
| 384 | + | log.Error().Err(err).Str("name", name).Msg("failed to marshal tags") | |
| 392 | 385 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 393 | 386 | return | |
| 394 | 387 | } | |
| 395 | - | ||
| 388 | + | ||
| 396 | 389 | response := fmt.Sprintf(`{"name":"%s","tags":%s}`, name, string(tagsJSON)) | |
| 397 | 390 | ||
| 398 | 391 | w.Header().Set("Content-Type", "application/json") | |
| @@ -400,67 +393,3 @@ func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) { | |||
| 400 | 393 | w.WriteHeader(http.StatusOK) | |
| 401 | 394 | w.Write([]byte(response)) | |
| 402 | 395 | } | |
| 403 | - | ||
| 404 | - | func (s *Server) Start(ctx context.Context) error { | |
| 405 | - | addr := ":" + strconv.Itoa(s.config.Server.RegistryPort) | |
| 406 | - | ||
| 407 | - | // Build middleware chain | |
| 408 | - | middlewares := []func(http.Handler) http.Handler{ | |
| 409 | - | middleware.PanicRecovery, | |
| 410 | - | middleware.RequestLogger, | |
| 411 | - | } | |
| 412 | - | ||
| 413 | - | // Add registry authentication if enabled | |
| 414 | - | if s.config.RegistryAuth.Enabled { | |
| 415 | - | middlewares = append(middlewares, middleware.RegistryAuth(s.config.RegistryAuth.Username, s.config.RegistryAuth.Password)) | |
| 416 | - | } | |
| 417 | - | ||
| 418 | - | // Wrap the mux with middleware | |
| 419 | - | handler := middleware.Chain(middlewares...)(s.mux) | |
| 420 | - | ||
| 421 | - | server := &http.Server{ | |
| 422 | - | Addr: addr, | |
| 423 | - | Handler: handler, | |
| 424 | - | } | |
| 425 | - | ||
| 426 | - | log.Info().Str("address", addr).Msg("Registry server starting") | |
| 427 | - | ||
| 428 | - | // Start server in a goroutine | |
| 429 | - | errChan := make(chan error, 1) | |
| 430 | - | go func() { | |
| 431 | - | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| 432 | - | errChan <- err | |
| 433 | - | } | |
| 434 | - | }() | |
| 435 | - | ||
| 436 | - | // Wait for context cancellation or server error | |
| 437 | - | select { | |
| 438 | - | case err := <-errChan: | |
| 439 | - | return err | |
| 440 | - | case <-ctx.Done(): | |
| 441 | - | log.Info().Msg("Registry server shutting down...") | |
| 442 | - | return server.Shutdown(context.Background()) | |
| 443 | - | } | |
| 444 | - | } | |
| 445 | - | ||
| 446 | - | // ServeHTTP implements http.Handler interface | |
| 447 | - | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 448 | - | s.mux.ServeHTTP(w, r) | |
| 449 | - | } | |
| 450 | - | ||
| 451 | - | // GetHandler returns the handler with middleware applied (for testing) | |
| 452 | - | func (s *Server) GetHandler() http.Handler { | |
| 453 | - | // Build middleware chain | |
| 454 | - | middlewares := []func(http.Handler) http.Handler{ | |
| 455 | - | middleware.PanicRecovery, | |
| 456 | - | middleware.RequestLogger, | |
| 457 | - | } | |
| 458 | - | ||
| 459 | - | // Add registry authentication if enabled | |
| 460 | - | if s.config.RegistryAuth.Enabled { | |
| 461 | - | middlewares = append(middlewares, middleware.RegistryAuth(s.config.RegistryAuth.Username, s.config.RegistryAuth.Password)) | |
| 462 | - | } | |
| 463 | - | ||
| 464 | - | // Wrap the mux with middleware | |
| 465 | - | return middleware.Chain(middlewares...)(s.mux) | |
| 466 | - | } | |
A internal/adapters/out/docker/runtime.go +918 -0
| @@ -0,0 +1,918 @@ | |||
| 1 | + | // Package docker implements the container runtime adapter using Docker API. | |
| 2 | + | package docker | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "encoding/base64" | |
| 7 | + | "encoding/json" | |
| 8 | + | "fmt" | |
| 9 | + | "io" | |
| 10 | + | "strconv" | |
| 11 | + | "strings" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/zerowrap" | |
| 14 | + | cerrdefs "github.com/containerd/errdefs" | |
| 15 | + | "github.com/docker/docker/api/types/container" | |
| 16 | + | "github.com/docker/docker/api/types/image" | |
| 17 | + | "github.com/docker/docker/api/types/network" | |
| 18 | + | "github.com/docker/docker/api/types/registry" | |
| 19 | + | "github.com/docker/docker/api/types/volume" | |
| 20 | + | "github.com/docker/docker/client" | |
| 21 | + | "github.com/docker/go-connections/nat" | |
| 22 | + | ||
| 23 | + | "gordon/internal/domain" | |
| 24 | + | ) | |
| 25 | + | ||
| 26 | + | // Runtime implements the ContainerRuntime interface using Docker API. | |
| 27 | + | type Runtime struct { | |
| 28 | + | client *client.Client | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // NewRuntime creates a new Docker runtime instance. | |
| 32 | + | func NewRuntime() (*Runtime, error) { | |
| 33 | + | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 34 | + | if err != nil { | |
| 35 | + | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | return &Runtime{ | |
| 39 | + | client: cli, | |
| 40 | + | }, nil | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // NewRuntimeWithClient creates a new Docker runtime instance with a custom client (for testing). | |
| 44 | + | func NewRuntimeWithClient(cli *client.Client) *Runtime { | |
| 45 | + | return &Runtime{ | |
| 46 | + | client: cli, | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // CreateContainer creates a new container. | |
| 51 | + | func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error) { | |
| 52 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 53 | + | zerowrap.FieldLayer: "adapter", | |
| 54 | + | zerowrap.FieldAdapter: "docker", | |
| 55 | + | zerowrap.FieldAction: "CreateContainer", | |
| 56 | + | "container_name": config.Name, | |
| 57 | + | "image": config.Image, | |
| 58 | + | }) | |
| 59 | + | log := zerowrap.FromCtx(ctx) | |
| 60 | + | ||
| 61 | + | // Convert ports to Docker format | |
| 62 | + | exposedPorts := make(nat.PortSet) | |
| 63 | + | portBindings := make(nat.PortMap) | |
| 64 | + | ||
| 65 | + | for _, port := range config.Ports { | |
| 66 | + | containerPort := nat.Port(fmt.Sprintf("%d/tcp", port)) | |
| 67 | + | exposedPorts[containerPort] = struct{}{} | |
| 68 | + | ||
| 69 | + | // Bind to random available port on host | |
| 70 | + | portBindings[containerPort] = []nat.PortBinding{ | |
| 71 | + | { | |
| 72 | + | HostIP: "0.0.0.0", | |
| 73 | + | HostPort: "0", // Docker will assign a random available port | |
| 74 | + | }, | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // Convert volumes to Docker format | |
| 79 | + | var binds []string | |
| 80 | + | if config.Volumes != nil { | |
| 81 | + | for containerPath, volumeName := range config.Volumes { | |
| 82 | + | bind := fmt.Sprintf("%s:%s", volumeName, containerPath) | |
| 83 | + | binds = append(binds, bind) | |
| 84 | + | log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount") | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // Create container configuration | |
| 89 | + | containerConfig := &container.Config{ | |
| 90 | + | Image: config.Image, | |
| 91 | + | Env: config.Env, | |
| 92 | + | ExposedPorts: exposedPorts, | |
| 93 | + | WorkingDir: config.WorkingDir, | |
| 94 | + | Cmd: config.Cmd, | |
| 95 | + | Labels: config.Labels, | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | hostConfig := &container.HostConfig{ | |
| 99 | + | PortBindings: portBindings, | |
| 100 | + | AutoRemove: config.AutoRemove, | |
| 101 | + | Binds: binds, | |
| 102 | + | NetworkMode: container.NetworkMode(config.NetworkMode), | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // Create network configuration for container | |
| 106 | + | var networkConfig *network.NetworkingConfig | |
| 107 | + | if config.NetworkMode != "" && config.NetworkMode != "default" && config.NetworkMode != "bridge" { | |
| 108 | + | networkConfig = &network.NetworkingConfig{ | |
| 109 | + | EndpointsConfig: map[string]*network.EndpointSettings{ | |
| 110 | + | config.NetworkMode: { | |
| 111 | + | Aliases: config.Aliases, | |
| 112 | + | }, | |
| 113 | + | }, | |
| 114 | + | } | |
| 115 | + | if config.Hostname != "" { | |
| 116 | + | networkConfig.EndpointsConfig[config.NetworkMode].NetworkID = config.NetworkMode | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // Create the container | |
| 121 | + | resp, err := r.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, config.Name) | |
| 122 | + | if err != nil { | |
| 123 | + | return nil, log.WrapErr(err, "failed to create container") | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | log.Info().Str(zerowrap.FieldEntityID, resp.ID).Msg("container created") | |
| 127 | + | ||
| 128 | + | // Inspect the created container to get full details | |
| 129 | + | return r.InspectContainer(ctx, resp.ID) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // StartContainer starts a container. | |
| 133 | + | func (r *Runtime) StartContainer(ctx context.Context, containerID string) error { | |
| 134 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 135 | + | zerowrap.FieldLayer: "adapter", | |
| 136 | + | zerowrap.FieldAdapter: "docker", | |
| 137 | + | zerowrap.FieldAction: "StartContainer", | |
| 138 | + | zerowrap.FieldEntityID: containerID, | |
| 139 | + | }) | |
| 140 | + | log := zerowrap.FromCtx(ctx) | |
| 141 | + | ||
| 142 | + | err := r.client.ContainerStart(ctx, containerID, container.StartOptions{}) | |
| 143 | + | if err != nil { | |
| 144 | + | return log.WrapErr(err, "failed to start container") | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | log.Info().Msg("container started") | |
| 148 | + | return nil | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // StopContainer stops a container. | |
| 152 | + | func (r *Runtime) StopContainer(ctx context.Context, containerID string) error { | |
| 153 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 154 | + | zerowrap.FieldLayer: "adapter", | |
| 155 | + | zerowrap.FieldAdapter: "docker", | |
| 156 | + | zerowrap.FieldAction: "StopContainer", | |
| 157 | + | zerowrap.FieldEntityID: containerID, | |
| 158 | + | }) | |
| 159 | + | log := zerowrap.FromCtx(ctx) | |
| 160 | + | ||
| 161 | + | timeout := 30 // 30 seconds timeout | |
| 162 | + | err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 163 | + | if err != nil { | |
| 164 | + | return log.WrapErr(err, "failed to stop container") | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | log.Info().Msg("container stopped") | |
| 168 | + | return nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // RestartContainer restarts a container. | |
| 172 | + | func (r *Runtime) RestartContainer(ctx context.Context, containerID string) error { | |
| 173 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 174 | + | zerowrap.FieldLayer: "adapter", | |
| 175 | + | zerowrap.FieldAdapter: "docker", | |
| 176 | + | zerowrap.FieldAction: "RestartContainer", | |
| 177 | + | zerowrap.FieldEntityID: containerID, | |
| 178 | + | }) | |
| 179 | + | log := zerowrap.FromCtx(ctx) | |
| 180 | + | ||
| 181 | + | timeout := 30 // 30 seconds timeout | |
| 182 | + | err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 183 | + | if err != nil { | |
| 184 | + | return log.WrapErr(err, "failed to restart container") | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | log.Info().Msg("container restarted") | |
| 188 | + | return nil | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // RemoveContainer removes a container. | |
| 192 | + | func (r *Runtime) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |
| 193 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 194 | + | zerowrap.FieldLayer: "adapter", | |
| 195 | + | zerowrap.FieldAdapter: "docker", | |
| 196 | + | zerowrap.FieldAction: "RemoveContainer", | |
| 197 | + | zerowrap.FieldEntityID: containerID, | |
| 198 | + | "force": force, | |
| 199 | + | }) | |
| 200 | + | log := zerowrap.FromCtx(ctx) | |
| 201 | + | ||
| 202 | + | err := r.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) | |
| 203 | + | if err != nil { | |
| 204 | + | return log.WrapErr(err, "failed to remove container") | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | log.Info().Msg("container removed") | |
| 208 | + | return nil | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // ListContainers lists containers. | |
| 212 | + | func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) { | |
| 213 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 214 | + | zerowrap.FieldLayer: "adapter", | |
| 215 | + | zerowrap.FieldAdapter: "docker", | |
| 216 | + | zerowrap.FieldAction: "ListContainers", | |
| 217 | + | "all": all, | |
| 218 | + | }) | |
| 219 | + | log := zerowrap.FromCtx(ctx) | |
| 220 | + | ||
| 221 | + | containers, err := r.client.ContainerList(ctx, container.ListOptions{All: all}) | |
| 222 | + | if err != nil { | |
| 223 | + | return nil, log.WrapErr(err, "failed to list containers") | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | var result []*domain.Container | |
| 227 | + | for _, c := range containers { | |
| 228 | + | // Extract ports | |
| 229 | + | var ports []int | |
| 230 | + | for _, port := range c.Ports { | |
| 231 | + | if port.PublicPort > 0 { | |
| 232 | + | ports = append(ports, int(port.PublicPort)) | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // Get the primary name (remove leading slash) | |
| 237 | + | name := "" | |
| 238 | + | if len(c.Names) > 0 { | |
| 239 | + | name = strings.TrimPrefix(c.Names[0], "/") | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | result = append(result, &domain.Container{ | |
| 243 | + | ID: c.ID, | |
| 244 | + | Image: c.Image, | |
| 245 | + | Name: name, | |
| 246 | + | Status: c.Status, | |
| 247 | + | Ports: ports, | |
| 248 | + | Labels: c.Labels, | |
| 249 | + | }) | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | return result, nil | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // InspectContainer inspects a container. | |
| 256 | + | func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) { | |
| 257 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 258 | + | zerowrap.FieldLayer: "adapter", | |
| 259 | + | zerowrap.FieldAdapter: "docker", | |
| 260 | + | zerowrap.FieldAction: "InspectContainer", | |
| 261 | + | zerowrap.FieldEntityID: containerID, | |
| 262 | + | }) | |
| 263 | + | log := zerowrap.FromCtx(ctx) | |
| 264 | + | ||
| 265 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 266 | + | if err != nil { | |
| 267 | + | return nil, log.WrapErr(err, "failed to inspect container") | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // Extract published ports | |
| 271 | + | var ports []int | |
| 272 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 273 | + | for _, bindings := range resp.NetworkSettings.Ports { | |
| 274 | + | for _, binding := range bindings { | |
| 275 | + | if binding.HostPort != "" { | |
| 276 | + | if port, err := strconv.Atoi(binding.HostPort); err == nil { | |
| 277 | + | ports = append(ports, port) | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // Get container name (remove leading slash) | |
| 285 | + | name := strings.TrimPrefix(resp.Name, "/") | |
| 286 | + | ||
| 287 | + | return &domain.Container{ | |
| 288 | + | ID: resp.ID, | |
| 289 | + | Image: resp.Config.Image, | |
| 290 | + | Name: name, | |
| 291 | + | Status: resp.State.Status, | |
| 292 | + | Ports: ports, | |
| 293 | + | Labels: resp.Config.Labels, | |
| 294 | + | }, nil | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // GetContainerLogs gets container logs. | |
| 298 | + | func (r *Runtime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 299 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 300 | + | zerowrap.FieldLayer: "adapter", | |
| 301 | + | zerowrap.FieldAdapter: "docker", | |
| 302 | + | zerowrap.FieldAction: "GetContainerLogs", | |
| 303 | + | zerowrap.FieldEntityID: containerID, | |
| 304 | + | }) | |
| 305 | + | log := zerowrap.FromCtx(ctx) | |
| 306 | + | ||
| 307 | + | logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{ | |
| 308 | + | ShowStdout: true, | |
| 309 | + | ShowStderr: true, | |
| 310 | + | Follow: follow, | |
| 311 | + | Timestamps: true, | |
| 312 | + | }) | |
| 313 | + | if err != nil { | |
| 314 | + | return nil, log.WrapErr(err, "failed to get container logs") | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | return logs, nil | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | // PullImage pulls an image. | |
| 321 | + | func (r *Runtime) PullImage(ctx context.Context, imageRef string) error { | |
| 322 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 323 | + | zerowrap.FieldLayer: "adapter", | |
| 324 | + | zerowrap.FieldAdapter: "docker", | |
| 325 | + | zerowrap.FieldAction: "PullImage", | |
| 326 | + | "image": imageRef, | |
| 327 | + | }) | |
| 328 | + | log := zerowrap.FromCtx(ctx) | |
| 329 | + | ||
| 330 | + | log.Info().Msg("pulling image") | |
| 331 | + | ||
| 332 | + | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{}) | |
| 333 | + | if err != nil { | |
| 334 | + | return log.WrapErr(err, "failed to pull image") | |
| 335 | + | } | |
| 336 | + | defer reader.Close() | |
| 337 | + | ||
| 338 | + | // Read the response to completion (this is required for the pull to complete) | |
| 339 | + | _, err = io.Copy(io.Discard, reader) | |
| 340 | + | if err != nil { | |
| 341 | + | return log.WrapErr(err, "failed to read pull response") | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | log.Info().Msg("image pulled successfully") | |
| 345 | + | return nil | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | // PullImageWithAuth pulls an image with authentication. | |
| 349 | + | func (r *Runtime) PullImageWithAuth(ctx context.Context, imageRef, username, password string) error { | |
| 350 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 351 | + | zerowrap.FieldLayer: "adapter", | |
| 352 | + | zerowrap.FieldAdapter: "docker", | |
| 353 | + | zerowrap.FieldAction: "PullImageWithAuth", | |
| 354 | + | "image": imageRef, | |
| 355 | + | "username": username, | |
| 356 | + | }) | |
| 357 | + | log := zerowrap.FromCtx(ctx) | |
| 358 | + | ||
| 359 | + | log.Info().Msg("pulling image with authentication") | |
| 360 | + | ||
| 361 | + | // Create authentication configuration | |
| 362 | + | authConfig := registry.AuthConfig{ | |
| 363 | + | Username: username, | |
| 364 | + | Password: password, | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | // Encode authentication to base64 JSON | |
| 368 | + | authConfigBytes, err := json.Marshal(authConfig) | |
| 369 | + | if err != nil { | |
| 370 | + | return log.WrapErr(err, "failed to marshal auth config") | |
| 371 | + | } | |
| 372 | + | authStr := base64.URLEncoding.EncodeToString(authConfigBytes) | |
| 373 | + | ||
| 374 | + | // Pull with authentication | |
| 375 | + | reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{ | |
| 376 | + | RegistryAuth: authStr, | |
| 377 | + | }) | |
| 378 | + | if err != nil { | |
| 379 | + | return log.WrapErr(err, "failed to pull image with auth") | |
| 380 | + | } | |
| 381 | + | defer reader.Close() | |
| 382 | + | ||
| 383 | + | // Read the response to completion (this is required for the pull to complete) | |
| 384 | + | _, err = io.Copy(io.Discard, reader) | |
| 385 | + | if err != nil { | |
| 386 | + | return log.WrapErr(err, "failed to read pull response") | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | log.Info().Msg("image pulled successfully with authentication") | |
| 390 | + | return nil | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | // RemoveImage removes an image. | |
| 394 | + | func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) error { | |
| 395 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 396 | + | zerowrap.FieldLayer: "adapter", | |
| 397 | + | zerowrap.FieldAdapter: "docker", | |
| 398 | + | zerowrap.FieldAction: "RemoveImage", | |
| 399 | + | "image": imageRef, | |
| 400 | + | "force": force, | |
| 401 | + | }) | |
| 402 | + | log := zerowrap.FromCtx(ctx) | |
| 403 | + | ||
| 404 | + | _, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force}) | |
| 405 | + | if err != nil { | |
| 406 | + | return log.WrapErr(err, "failed to remove image") | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | log.Info().Msg("image removed") | |
| 410 | + | return nil | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | // ListImages lists images. | |
| 414 | + | func (r *Runtime) ListImages(ctx context.Context) ([]string, error) { | |
| 415 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 416 | + | zerowrap.FieldLayer: "adapter", | |
| 417 | + | zerowrap.FieldAdapter: "docker", | |
| 418 | + | zerowrap.FieldAction: "ListImages", | |
| 419 | + | }) | |
| 420 | + | log := zerowrap.FromCtx(ctx) | |
| 421 | + | ||
| 422 | + | images, err := r.client.ImageList(ctx, image.ListOptions{}) | |
| 423 | + | if err != nil { | |
| 424 | + | return nil, log.WrapErr(err, "failed to list images") | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | var result []string | |
| 428 | + | for _, img := range images { | |
| 429 | + | for _, tag := range img.RepoTags { | |
| 430 | + | if tag != "<none>:<none>" { | |
| 431 | + | result = append(result, tag) | |
| 432 | + | } | |
| 433 | + | } | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | return result, nil | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | // Ping checks if Docker is responsive. | |
| 440 | + | func (r *Runtime) Ping(ctx context.Context) error { | |
| 441 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 442 | + | zerowrap.FieldLayer: "adapter", | |
| 443 | + | zerowrap.FieldAdapter: "docker", | |
| 444 | + | zerowrap.FieldAction: "Ping", | |
| 445 | + | }) | |
| 446 | + | log := zerowrap.FromCtx(ctx) | |
| 447 | + | ||
| 448 | + | _, err := r.client.Ping(ctx) | |
| 449 | + | if err != nil { | |
| 450 | + | return log.WrapErr(err, "Docker ping failed") | |
| 451 | + | } | |
| 452 | + | return nil | |
| 453 | + | } | |
| 454 | + | ||
| 455 | + | // Version returns Docker version. | |
| 456 | + | func (r *Runtime) Version(ctx context.Context) (string, error) { | |
| 457 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 458 | + | zerowrap.FieldLayer: "adapter", | |
| 459 | + | zerowrap.FieldAdapter: "docker", | |
| 460 | + | zerowrap.FieldAction: "Version", | |
| 461 | + | }) | |
| 462 | + | log := zerowrap.FromCtx(ctx) | |
| 463 | + | ||
| 464 | + | version, err := r.client.ServerVersion(ctx) | |
| 465 | + | if err != nil { | |
| 466 | + | return "", log.WrapErr(err, "failed to get Docker version") | |
| 467 | + | } | |
| 468 | + | return version.Version, nil | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | // IsContainerRunning checks if a container is running. | |
| 472 | + | func (r *Runtime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) { | |
| 473 | + | ctr, err := r.InspectContainer(ctx, containerID) | |
| 474 | + | if err != nil { | |
| 475 | + | return false, err | |
| 476 | + | } | |
| 477 | + | return ctr.Status == string(domain.ContainerStatusRunning), nil | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // GetContainerPort gets the host port for a container's internal port. | |
| 481 | + | func (r *Runtime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) { | |
| 482 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 483 | + | zerowrap.FieldLayer: "adapter", | |
| 484 | + | zerowrap.FieldAdapter: "docker", | |
| 485 | + | zerowrap.FieldAction: "GetContainerPort", | |
| 486 | + | zerowrap.FieldEntityID: containerID, | |
| 487 | + | "internal_port": internalPort, | |
| 488 | + | }) | |
| 489 | + | log := zerowrap.FromCtx(ctx) | |
| 490 | + | ||
| 491 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 492 | + | if err != nil { | |
| 493 | + | return 0, log.WrapErr(err, "failed to inspect container") | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { | |
| 497 | + | return 0, fmt.Errorf("no port mappings found for container %s", containerID) | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort)) | |
| 501 | + | bindings, exists := resp.NetworkSettings.Ports[containerPort] | |
| 502 | + | if !exists || len(bindings) == 0 { | |
| 503 | + | return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | hostPort, err := strconv.Atoi(bindings[0].HostPort) | |
| 507 | + | if err != nil { | |
| 508 | + | return 0, fmt.Errorf("invalid host port for container %s: %w", containerID, err) | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | return hostPort, nil | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | // GetImageExposedPorts gets the exposed ports from an image. | |
| 515 | + | func (r *Runtime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { | |
| 516 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 517 | + | zerowrap.FieldLayer: "adapter", | |
| 518 | + | zerowrap.FieldAdapter: "docker", | |
| 519 | + | zerowrap.FieldAction: "GetImageExposedPorts", | |
| 520 | + | "image": imageRef, | |
| 521 | + | }) | |
| 522 | + | log := zerowrap.FromCtx(ctx) | |
| 523 | + | ||
| 524 | + | imageInspect, err := r.client.ImageInspect(ctx, imageRef) | |
| 525 | + | if err != nil { | |
| 526 | + | return nil, log.WrapErr(err, "failed to inspect image") | |
| 527 | + | } | |
| 528 | + | ||
| 529 | + | var ports []int | |
| 530 | + | if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil { | |
| 531 | + | for portSpec := range imageInspect.Config.ExposedPorts { | |
| 532 | + | // Parse port from format like "80/tcp" | |
| 533 | + | portStr := strings.Split(portSpec, "/")[0] | |
| 534 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 535 | + | ports = append(ports, port) | |
| 536 | + | } | |
| 537 | + | } | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | if len(ports) == 0 { | |
| 541 | + | return nil, fmt.Errorf("no EXPOSE directives found in image %s - please add EXPOSE directive to Dockerfile", imageRef) | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | log.Info().Ints("exposed_ports", ports).Msg("found exposed ports in image") | |
| 545 | + | return ports, nil | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | // GetContainerExposedPorts gets all exposed ports from a running container. | |
| 549 | + | func (r *Runtime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { | |
| 550 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 551 | + | zerowrap.FieldLayer: "adapter", | |
| 552 | + | zerowrap.FieldAdapter: "docker", | |
| 553 | + | zerowrap.FieldAction: "GetContainerExposedPorts", | |
| 554 | + | zerowrap.FieldEntityID: containerID, | |
| 555 | + | }) | |
| 556 | + | log := zerowrap.FromCtx(ctx) | |
| 557 | + | ||
| 558 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 559 | + | if err != nil { | |
| 560 | + | return nil, log.WrapErr(err, "failed to inspect container") | |
| 561 | + | } | |
| 562 | + | ||
| 563 | + | var ports []int | |
| 564 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 565 | + | for portSpec := range resp.NetworkSettings.Ports { | |
| 566 | + | // Parse port from format like "80/tcp" | |
| 567 | + | portStr := strings.Split(string(portSpec), "/")[0] | |
| 568 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 569 | + | ports = append(ports, port) | |
| 570 | + | } | |
| 571 | + | } | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | return ports, nil | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | // GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use. | |
| 578 | + | func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |
| 579 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 580 | + | zerowrap.FieldLayer: "adapter", | |
| 581 | + | zerowrap.FieldAdapter: "docker", | |
| 582 | + | zerowrap.FieldAction: "GetContainerNetworkInfo", | |
| 583 | + | zerowrap.FieldEntityID: containerID, | |
| 584 | + | }) | |
| 585 | + | log := zerowrap.FromCtx(ctx) | |
| 586 | + | ||
| 587 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 588 | + | if err != nil { | |
| 589 | + | return "", 0, log.WrapErr(err, "failed to inspect container") | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | // Get container's internal IP address | |
| 593 | + | var containerIP string | |
| 594 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | |
| 595 | + | // Use the first available network (usually bridge or custom network) | |
| 596 | + | for _, net := range resp.NetworkSettings.Networks { | |
| 597 | + | if net.IPAddress != "" { | |
| 598 | + | containerIP = net.IPAddress | |
| 599 | + | break | |
| 600 | + | } | |
| 601 | + | } | |
| 602 | + | } | |
| 603 | + | ||
| 604 | + | if containerIP == "" { | |
| 605 | + | return "", 0, fmt.Errorf("no IP address found for container %s", containerID) | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | // Get exposed ports from container | |
| 609 | + | var exposedPorts []int | |
| 610 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 611 | + | for portSpec := range resp.NetworkSettings.Ports { | |
| 612 | + | // Parse port from format like "80/tcp" | |
| 613 | + | portStr := strings.Split(string(portSpec), "/")[0] | |
| 614 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 615 | + | exposedPorts = append(exposedPorts, port) | |
| 616 | + | } | |
| 617 | + | } | |
| 618 | + | } | |
| 619 | + | ||
| 620 | + | if len(exposedPorts) == 0 { | |
| 621 | + | return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID) | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | // Use the first exposed port | |
| 625 | + | selectedPort := exposedPorts[0] | |
| 626 | + | ||
| 627 | + | log.Info(). | |
| 628 | + | Str("ip", containerIP). | |
| 629 | + | Int("selected_port", selectedPort). | |
| 630 | + | Ints("exposed_ports", exposedPorts). | |
| 631 | + | Msg("container network info detected") | |
| 632 | + | ||
| 633 | + | return containerIP, selectedPort, nil | |
| 634 | + | } | |
| 635 | + | ||
| 636 | + | // InspectImageVolumes gets the volume mount points declared in the image. | |
| 637 | + | func (r *Runtime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | |
| 638 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 639 | + | zerowrap.FieldLayer: "adapter", | |
| 640 | + | zerowrap.FieldAdapter: "docker", | |
| 641 | + | zerowrap.FieldAction: "InspectImageVolumes", | |
| 642 | + | "image": imageRef, | |
| 643 | + | }) | |
| 644 | + | log := zerowrap.FromCtx(ctx) | |
| 645 | + | ||
| 646 | + | imageInspect, err := r.client.ImageInspect(ctx, imageRef) | |
| 647 | + | if err != nil { | |
| 648 | + | return nil, log.WrapErr(err, "failed to inspect image") | |
| 649 | + | } | |
| 650 | + | ||
| 651 | + | var volumes []string | |
| 652 | + | if imageInspect.Config != nil && imageInspect.Config.Volumes != nil { | |
| 653 | + | for volumePath := range imageInspect.Config.Volumes { | |
| 654 | + | volumes = append(volumes, volumePath) | |
| 655 | + | } | |
| 656 | + | } | |
| 657 | + | ||
| 658 | + | if len(volumes) > 0 { | |
| 659 | + | log.Info().Strs("volumes", volumes).Msg("found VOLUME directives in image") | |
| 660 | + | } else { | |
| 661 | + | log.Debug().Msg("no VOLUME directives found in image") | |
| 662 | + | } | |
| 663 | + | ||
| 664 | + | return volumes, nil | |
| 665 | + | } | |
| 666 | + | ||
| 667 | + | // VolumeExists checks if a Docker volume exists. | |
| 668 | + | func (r *Runtime) VolumeExists(ctx context.Context, volumeName string) (bool, error) { | |
| 669 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 670 | + | zerowrap.FieldLayer: "adapter", | |
| 671 | + | zerowrap.FieldAdapter: "docker", | |
| 672 | + | zerowrap.FieldAction: "VolumeExists", | |
| 673 | + | "volume": volumeName, | |
| 674 | + | }) | |
| 675 | + | log := zerowrap.FromCtx(ctx) | |
| 676 | + | ||
| 677 | + | _, err := r.client.VolumeInspect(ctx, volumeName) | |
| 678 | + | if err != nil { | |
| 679 | + | if cerrdefs.IsNotFound(err) { | |
| 680 | + | return false, nil | |
| 681 | + | } | |
| 682 | + | return false, log.WrapErr(err, "failed to inspect volume") | |
| 683 | + | } | |
| 684 | + | return true, nil | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | // CreateVolume creates a new Docker volume. | |
| 688 | + | func (r *Runtime) CreateVolume(ctx context.Context, volumeName string) error { | |
| 689 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 690 | + | zerowrap.FieldLayer: "adapter", | |
| 691 | + | zerowrap.FieldAdapter: "docker", | |
| 692 | + | zerowrap.FieldAction: "CreateVolume", | |
| 693 | + | "volume": volumeName, | |
| 694 | + | }) | |
| 695 | + | log := zerowrap.FromCtx(ctx) | |
| 696 | + | ||
| 697 | + | _, err := r.client.VolumeCreate(ctx, volume.CreateOptions{ | |
| 698 | + | Name: volumeName, | |
| 699 | + | Labels: map[string]string{ | |
| 700 | + | "gordon.managed": "true", | |
| 701 | + | "gordon.created": "auto", | |
| 702 | + | }, | |
| 703 | + | }) | |
| 704 | + | if err != nil { | |
| 705 | + | return log.WrapErr(err, "failed to create volume") | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | log.Info().Msg("volume created") | |
| 709 | + | return nil | |
| 710 | + | } | |
| 711 | + | ||
| 712 | + | // RemoveVolume removes a Docker volume. | |
| 713 | + | func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force bool) error { | |
| 714 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 715 | + | zerowrap.FieldLayer: "adapter", | |
| 716 | + | zerowrap.FieldAdapter: "docker", | |
| 717 | + | zerowrap.FieldAction: "RemoveVolume", | |
| 718 | + | "volume": volumeName, | |
| 719 | + | "force": force, | |
| 720 | + | }) | |
| 721 | + | log := zerowrap.FromCtx(ctx) | |
| 722 | + | ||
| 723 | + | err := r.client.VolumeRemove(ctx, volumeName, force) | |
| 724 | + | if err != nil { | |
| 725 | + | if cerrdefs.IsNotFound(err) { | |
| 726 | + | log.Debug().Msg("volume not found, already removed") | |
| 727 | + | return nil | |
| 728 | + | } | |
| 729 | + | return log.WrapErr(err, "failed to remove volume") | |
| 730 | + | } | |
| 731 | + | ||
| 732 | + | log.Info().Msg("volume removed") | |
| 733 | + | return nil | |
| 734 | + | } | |
| 735 | + | ||
| 736 | + | // InspectImageEnv gets the environment variables declared in the image. | |
| 737 | + | func (r *Runtime) InspectImageEnv(ctx context.Context, imageRef string) ([]string, error) { | |
| 738 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 739 | + | zerowrap.FieldLayer: "adapter", | |
| 740 | + | zerowrap.FieldAdapter: "docker", | |
| 741 | + | zerowrap.FieldAction: "InspectImageEnv", | |
| 742 | + | "image": imageRef, | |
| 743 | + | }) | |
| 744 | + | log := zerowrap.FromCtx(ctx) | |
| 745 | + | ||
| 746 | + | imageInspect, err := r.client.ImageInspect(ctx, imageRef) | |
| 747 | + | if err != nil { | |
| 748 | + | return nil, log.WrapErr(err, "failed to inspect image") | |
| 749 | + | } | |
| 750 | + | ||
| 751 | + | var envVars []string | |
| 752 | + | if imageInspect.Config != nil && imageInspect.Config.Env != nil { | |
| 753 | + | envVars = imageInspect.Config.Env | |
| 754 | + | } | |
| 755 | + | ||
| 756 | + | if len(envVars) > 0 { | |
| 757 | + | log.Debug().Strs("env_vars", envVars).Msg("found ENV directives in image") | |
| 758 | + | } else { | |
| 759 | + | log.Debug().Msg("no ENV directives found in image") | |
| 760 | + | } | |
| 761 | + | ||
| 762 | + | return envVars, nil | |
| 763 | + | } | |
| 764 | + | ||
| 765 | + | // CreateNetwork creates a new Docker network. | |
| 766 | + | func (r *Runtime) CreateNetwork(ctx context.Context, name string, options map[string]string) error { | |
| 767 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 768 | + | zerowrap.FieldLayer: "adapter", | |
| 769 | + | zerowrap.FieldAdapter: "docker", | |
| 770 | + | zerowrap.FieldAction: "CreateNetwork", | |
| 771 | + | "network": name, | |
| 772 | + | }) | |
| 773 | + | log := zerowrap.FromCtx(ctx) | |
| 774 | + | ||
| 775 | + | // Set default driver to bridge if not specified | |
| 776 | + | driver := "bridge" | |
| 777 | + | if driverOption, exists := options["driver"]; exists { | |
| 778 | + | driver = driverOption | |
| 779 | + | } | |
| 780 | + | ||
| 781 | + | createOptions := network.CreateOptions{ | |
| 782 | + | Driver: driver, | |
| 783 | + | Labels: map[string]string{ | |
| 784 | + | "gordon.managed": "true", | |
| 785 | + | }, | |
| 786 | + | } | |
| 787 | + | ||
| 788 | + | // Add any additional options to labels | |
| 789 | + | for key, value := range options { | |
| 790 | + | if key != "driver" { | |
| 791 | + | createOptions.Labels["gordon."+key] = value | |
| 792 | + | } | |
| 793 | + | } | |
| 794 | + | ||
| 795 | + | _, err := r.client.NetworkCreate(ctx, name, createOptions) | |
| 796 | + | if err != nil { | |
| 797 | + | return log.WrapErr(err, "failed to create network") | |
| 798 | + | } | |
| 799 | + | ||
| 800 | + | log.Info().Str("driver", driver).Msg("network created") | |
| 801 | + | return nil | |
| 802 | + | } | |
| 803 | + | ||
| 804 | + | // RemoveNetwork removes a Docker network. | |
| 805 | + | func (r *Runtime) RemoveNetwork(ctx context.Context, name string) error { | |
| 806 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 807 | + | zerowrap.FieldLayer: "adapter", | |
| 808 | + | zerowrap.FieldAdapter: "docker", | |
| 809 | + | zerowrap.FieldAction: "RemoveNetwork", | |
| 810 | + | "network": name, | |
| 811 | + | }) | |
| 812 | + | log := zerowrap.FromCtx(ctx) | |
| 813 | + | ||
| 814 | + | err := r.client.NetworkRemove(ctx, name) | |
| 815 | + | if err != nil { | |
| 816 | + | if cerrdefs.IsNotFound(err) { | |
| 817 | + | log.Debug().Msg("network not found, already removed") | |
| 818 | + | return nil | |
| 819 | + | } | |
| 820 | + | return log.WrapErr(err, "failed to remove network") | |
| 821 | + | } | |
| 822 | + | ||
| 823 | + | log.Info().Msg("network removed") | |
| 824 | + | return nil | |
| 825 | + | } | |
| 826 | + | ||
| 827 | + | // ListNetworks lists all Docker networks. | |
| 828 | + | func (r *Runtime) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 829 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 830 | + | zerowrap.FieldLayer: "adapter", | |
| 831 | + | zerowrap.FieldAdapter: "docker", | |
| 832 | + | zerowrap.FieldAction: "ListNetworks", | |
| 833 | + | }) | |
| 834 | + | log := zerowrap.FromCtx(ctx) | |
| 835 | + | ||
| 836 | + | networks, err := r.client.NetworkList(ctx, network.ListOptions{}) | |
| 837 | + | if err != nil { | |
| 838 | + | return nil, log.WrapErr(err, "failed to list networks") | |
| 839 | + | } | |
| 840 | + | ||
| 841 | + | var result []*domain.NetworkInfo | |
| 842 | + | for _, net := range networks { | |
| 843 | + | var containers []string | |
| 844 | + | for containerID := range net.Containers { | |
| 845 | + | containers = append(containers, containerID) | |
| 846 | + | } | |
| 847 | + | ||
| 848 | + | result = append(result, &domain.NetworkInfo{ | |
| 849 | + | ID: net.ID, | |
| 850 | + | Name: net.Name, | |
| 851 | + | Driver: net.Driver, | |
| 852 | + | Containers: containers, | |
| 853 | + | Labels: net.Labels, | |
| 854 | + | }) | |
| 855 | + | } | |
| 856 | + | ||
| 857 | + | return result, nil | |
| 858 | + | } | |
| 859 | + | ||
| 860 | + | // NetworkExists checks if a Docker network exists. | |
| 861 | + | func (r *Runtime) NetworkExists(ctx context.Context, name string) (bool, error) { | |
| 862 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 863 | + | zerowrap.FieldLayer: "adapter", | |
| 864 | + | zerowrap.FieldAdapter: "docker", | |
| 865 | + | zerowrap.FieldAction: "NetworkExists", | |
| 866 | + | "network": name, | |
| 867 | + | }) | |
| 868 | + | log := zerowrap.FromCtx(ctx) | |
| 869 | + | ||
| 870 | + | _, err := r.client.NetworkInspect(ctx, name, network.InspectOptions{}) | |
| 871 | + | if err != nil { | |
| 872 | + | if cerrdefs.IsNotFound(err) { | |
| 873 | + | return false, nil | |
| 874 | + | } | |
| 875 | + | return false, log.WrapErr(err, "failed to inspect network") | |
| 876 | + | } | |
| 877 | + | return true, nil | |
| 878 | + | } | |
| 879 | + | ||
| 880 | + | // ConnectContainerToNetwork connects a container to a network. | |
| 881 | + | func (r *Runtime) ConnectContainerToNetwork(ctx context.Context, containerName, networkName string) error { | |
| 882 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 883 | + | zerowrap.FieldLayer: "adapter", | |
| 884 | + | zerowrap.FieldAdapter: "docker", | |
| 885 | + | zerowrap.FieldAction: "ConnectContainerToNetwork", | |
| 886 | + | "container": containerName, | |
| 887 | + | "network": networkName, | |
| 888 | + | }) | |
| 889 | + | log := zerowrap.FromCtx(ctx) | |
| 890 | + | ||
| 891 | + | err := r.client.NetworkConnect(ctx, networkName, containerName, &network.EndpointSettings{}) | |
| 892 | + | if err != nil { | |
| 893 | + | return log.WrapErr(err, "failed to connect container to network") | |
| 894 | + | } | |
| 895 | + | ||
| 896 | + | log.Info().Msg("container connected to network") | |
| 897 | + | return nil | |
| 898 | + | } | |
| 899 | + | ||
| 900 | + | // DisconnectContainerFromNetwork disconnects a container from a network. | |
| 901 | + | func (r *Runtime) DisconnectContainerFromNetwork(ctx context.Context, containerName, networkName string) error { | |
| 902 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 903 | + | zerowrap.FieldLayer: "adapter", | |
| 904 | + | zerowrap.FieldAdapter: "docker", | |
| 905 | + | zerowrap.FieldAction: "DisconnectContainerFromNetwork", | |
| 906 | + | "container": containerName, | |
| 907 | + | "network": networkName, | |
| 908 | + | }) | |
| 909 | + | log := zerowrap.FromCtx(ctx) | |
| 910 | + | ||
| 911 | + | err := r.client.NetworkDisconnect(ctx, networkName, containerName, false) | |
| 912 | + | if err != nil { | |
| 913 | + | return log.WrapErr(err, "failed to disconnect container from network") | |
| 914 | + | } | |
| 915 | + | ||
| 916 | + | log.Info().Msg("container disconnected from network") | |
| 917 | + | return nil | |
| 918 | + | } | |
A internal/adapters/out/envloader/file.go +231 -0
| @@ -0,0 +1,231 @@ | |||
| 1 | + | // Package envloader implements the environment variable loader adapter. | |
| 2 | + | package envloader | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "bufio" | |
| 6 | + | "context" | |
| 7 | + | "fmt" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "strings" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | ||
| 14 | + | "gordon/internal/boundaries/out" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // FileLoader implements the EnvLoader interface using filesystem-based env files. | |
| 18 | + | type FileLoader struct { | |
| 19 | + | envDir string | |
| 20 | + | secretProviders map[string]out.SecretProvider | |
| 21 | + | log zerowrap.Logger | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // NewFileLoader creates a new file-based environment loader. | |
| 25 | + | func NewFileLoader(envDir string, log zerowrap.Logger) (*FileLoader, error) { | |
| 26 | + | // Ensure env directory exists | |
| 27 | + | if err := os.MkdirAll(envDir, 0700); err != nil { | |
| 28 | + | return nil, fmt.Errorf("failed to create env directory %s: %w", envDir, err) | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | log.Debug(). | |
| 32 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 33 | + | Str(zerowrap.FieldAdapter, "envloader"). | |
| 34 | + | Str("env_dir", envDir). | |
| 35 | + | Msg("env loader initialized") | |
| 36 | + | ||
| 37 | + | return &FileLoader{ | |
| 38 | + | envDir: envDir, | |
| 39 | + | secretProviders: make(map[string]out.SecretProvider), | |
| 40 | + | log: log, | |
| 41 | + | }, nil | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // RegisterSecretProvider registers a secret provider for resolving secrets. | |
| 45 | + | func (l *FileLoader) RegisterSecretProvider(provider out.SecretProvider) { | |
| 46 | + | l.secretProviders[provider.Name()] = provider | |
| 47 | + | l.log.Debug(). | |
| 48 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 49 | + | Str(zerowrap.FieldAdapter, "envloader"). | |
| 50 | + | Str("provider", provider.Name()). | |
| 51 | + | Msg("secret provider registered") | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // LoadEnv loads environment variables for a given domain. | |
| 55 | + | func (l *FileLoader) LoadEnv(ctx context.Context, domain string) ([]string, error) { | |
| 56 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 57 | + | zerowrap.FieldLayer: "adapter", | |
| 58 | + | zerowrap.FieldAdapter: "envloader", | |
| 59 | + | zerowrap.FieldAction: "LoadEnv", | |
| 60 | + | "domain": domain, | |
| 61 | + | }) | |
| 62 | + | log := zerowrap.FromCtx(ctx) | |
| 63 | + | ||
| 64 | + | envVars := []string{} | |
| 65 | + | envFile := l.getEnvFilePath(domain) | |
| 66 | + | ||
| 67 | + | // Check if env file exists | |
| 68 | + | if _, err := os.Stat(envFile); os.IsNotExist(err) { | |
| 69 | + | log.Debug().Str("env_file", envFile).Msg("no env file found for route") | |
| 70 | + | return envVars, nil | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | log.Debug().Str("env_file", envFile).Msg("loading env file for route") | |
| 74 | + | ||
| 75 | + | file, err := os.Open(envFile) | |
| 76 | + | if err != nil { | |
| 77 | + | return nil, log.WrapErr(err, "failed to open env file") | |
| 78 | + | } | |
| 79 | + | defer file.Close() | |
| 80 | + | ||
| 81 | + | scanner := bufio.NewScanner(file) | |
| 82 | + | lineNum := 0 | |
| 83 | + | ||
| 84 | + | for scanner.Scan() { | |
| 85 | + | lineNum++ | |
| 86 | + | line := strings.TrimSpace(scanner.Text()) | |
| 87 | + | ||
| 88 | + | // Skip empty lines and comments | |
| 89 | + | if line == "" || strings.HasPrefix(line, "#") { | |
| 90 | + | continue | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Parse KEY=VALUE format | |
| 94 | + | parts := strings.SplitN(line, "=", 2) | |
| 95 | + | if len(parts) != 2 { | |
| 96 | + | log.Warn(). | |
| 97 | + | Int("line", lineNum). | |
| 98 | + | Str("content", line). | |
| 99 | + | Msg("invalid env file line format, skipping") | |
| 100 | + | continue | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | key := strings.TrimSpace(parts[0]) | |
| 104 | + | value := strings.TrimSpace(parts[1]) | |
| 105 | + | ||
| 106 | + | // Remove quotes if present | |
| 107 | + | if len(value) >= 2 { | |
| 108 | + | if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || | |
| 109 | + | (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) { | |
| 110 | + | value = value[1 : len(value)-1] | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Resolve secrets if value contains secret syntax | |
| 115 | + | resolvedValue, err := l.resolveSecrets(ctx, value) | |
| 116 | + | if err != nil { | |
| 117 | + | return nil, log.WrapErrWithFields(err, "failed to resolve secret", map[string]any{ | |
| 118 | + | "key": key, | |
| 119 | + | "line": lineNum, | |
| 120 | + | }) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | envVars = append(envVars, fmt.Sprintf("%s=%s", key, resolvedValue)) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | if err := scanner.Err(); err != nil { | |
| 127 | + | return nil, log.WrapErr(err, "error reading env file") | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | log.Info().Int(zerowrap.FieldCount, len(envVars)).Msg("loaded environment variables for route") | |
| 131 | + | ||
| 132 | + | return envVars, nil | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // CreateEnvFile creates an empty environment file for a new domain. | |
| 136 | + | func (l *FileLoader) CreateEnvFile(ctx context.Context, domain string) error { | |
| 137 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 138 | + | zerowrap.FieldLayer: "adapter", | |
| 139 | + | zerowrap.FieldAdapter: "envloader", | |
| 140 | + | zerowrap.FieldAction: "CreateEnvFile", | |
| 141 | + | "domain": domain, | |
| 142 | + | }) | |
| 143 | + | log := zerowrap.FromCtx(ctx) | |
| 144 | + | ||
| 145 | + | envFile := l.getEnvFilePath(domain) | |
| 146 | + | ||
| 147 | + | // Check if file already exists | |
| 148 | + | if _, err := os.Stat(envFile); err == nil { | |
| 149 | + | log.Debug().Str("env_file", envFile).Msg("env file already exists, skipping creation") | |
| 150 | + | return nil | |
| 151 | + | } else if !os.IsNotExist(err) { | |
| 152 | + | return log.WrapErr(err, "error checking env file") | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // Create empty env file with helpful comments | |
| 156 | + | content := fmt.Sprintf(`# Environment variables for route: %s | |
| 157 | + | # Add KEY=VALUE pairs, one per line | |
| 158 | + | # Secrets can be referenced with ${provider:path} syntax | |
| 159 | + | ||
| 160 | + | `, domain) | |
| 161 | + | ||
| 162 | + | if err := os.WriteFile(envFile, []byte(content), 0600); err != nil { | |
| 163 | + | return log.WrapErr(err, "failed to create env file") | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | log.Info().Str("env_file", envFile).Msg("created empty env file for route") | |
| 167 | + | ||
| 168 | + | return nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // EnvFileExists checks if an environment file exists for a domain. | |
| 172 | + | func (l *FileLoader) EnvFileExists(domain string) bool { | |
| 173 | + | envFile := l.getEnvFilePath(domain) | |
| 174 | + | _, err := os.Stat(envFile) | |
| 175 | + | return err == nil | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // resolveSecrets resolves any secret references in the value. | |
| 179 | + | // Secret syntax: ${provider:path} | |
| 180 | + | func (l *FileLoader) resolveSecrets(ctx context.Context, value string) (string, error) { | |
| 181 | + | // Check if value contains secret syntax: ${provider:path} | |
| 182 | + | if !strings.Contains(value, "${") { | |
| 183 | + | return value, nil | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | result := value | |
| 187 | + | for { | |
| 188 | + | start := strings.Index(result, "${") | |
| 189 | + | if start == -1 { | |
| 190 | + | break | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | end := strings.Index(result[start:], "}") | |
| 194 | + | if end == -1 { | |
| 195 | + | return "", fmt.Errorf("unclosed secret syntax in value: %s", value) | |
| 196 | + | } | |
| 197 | + | end += start | |
| 198 | + | ||
| 199 | + | secretRef := result[start+2 : end] | |
| 200 | + | parts := strings.SplitN(secretRef, ":", 2) | |
| 201 | + | if len(parts) != 2 { | |
| 202 | + | return "", fmt.Errorf("invalid secret syntax: expected 'provider:path', got '%s'", secretRef) | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | providerName := parts[0] | |
| 206 | + | secretPath := parts[1] | |
| 207 | + | ||
| 208 | + | provider, exists := l.secretProviders[providerName] | |
| 209 | + | if !exists { | |
| 210 | + | return "", fmt.Errorf("unknown secret provider: %s", providerName) | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | secretValue, err := provider.GetSecret(ctx, secretPath) | |
| 214 | + | if err != nil { | |
| 215 | + | return "", fmt.Errorf("failed to get secret from provider %s: %w", providerName, err) | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // Replace the secret reference with the actual value | |
| 219 | + | result = result[:start] + secretValue + result[end+1:] | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | return result, nil | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (l *FileLoader) getEnvFilePath(domain string) string { | |
| 226 | + | // Create domain-safe filename (replace dots and other chars with underscores) | |
| 227 | + | safeDomain := strings.ReplaceAll(domain, ".", "_") | |
| 228 | + | safeDomain = strings.ReplaceAll(safeDomain, ":", "_") | |
| 229 | + | safeDomain = strings.ReplaceAll(safeDomain, "/", "_") | |
| 230 | + | return filepath.Join(l.envDir, safeDomain+".env") | |
| 231 | + | } | |
A internal/adapters/out/eventbus/inmemory.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | // Package eventbus implements the event bus adapter. | |
| 2 | + | package eventbus | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "sync" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/zerowrap" | |
| 11 | + | "github.com/google/uuid" | |
| 12 | + | ||
| 13 | + | "gordon/internal/boundaries/out" | |
| 14 | + | "gordon/internal/domain" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // InMemory implements the EventBus interface using in-memory channels. | |
| 18 | + | type InMemory struct { | |
| 19 | + | handlers []out.EventHandler | |
| 20 | + | eventChan chan domain.Event | |
| 21 | + | done chan struct{} | |
| 22 | + | mu sync.RWMutex | |
| 23 | + | ctx context.Context | |
| 24 | + | cancel context.CancelFunc | |
| 25 | + | bufferSize int | |
| 26 | + | log zerowrap.Logger | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // NewInMemory creates a new in-memory event bus. | |
| 30 | + | func NewInMemory(bufferSize int, log zerowrap.Logger) *InMemory { | |
| 31 | + | if bufferSize <= 0 { | |
| 32 | + | bufferSize = 100 | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 36 | + | ||
| 37 | + | return &InMemory{ | |
| 38 | + | handlers: make([]out.EventHandler, 0), | |
| 39 | + | eventChan: make(chan domain.Event, bufferSize), | |
| 40 | + | done: make(chan struct{}), | |
| 41 | + | ctx: ctx, | |
| 42 | + | cancel: cancel, | |
| 43 | + | bufferSize: bufferSize, | |
| 44 | + | log: log, | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // Publish publishes an event to the bus. | |
| 49 | + | func (bus *InMemory) Publish(eventType domain.EventType, payload any) error { | |
| 50 | + | event := domain.Event{ | |
| 51 | + | ID: uuid.New().String(), | |
| 52 | + | Type: eventType, | |
| 53 | + | Timestamp: time.Now(), | |
| 54 | + | Data: payload, | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // Add specific fields for known event types | |
| 58 | + | switch p := payload.(type) { | |
| 59 | + | case domain.ImagePushedPayload: | |
| 60 | + | event.ImageName = p.Name | |
| 61 | + | event.Tag = p.Reference | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | select { | |
| 65 | + | case bus.eventChan <- event: | |
| 66 | + | bus.log.Debug(). | |
| 67 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 68 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 69 | + | Str("event_id", event.ID). | |
| 70 | + | Str(zerowrap.FieldEvent, string(event.Type)). | |
| 71 | + | Str("image_name", event.ImageName). | |
| 72 | + | Str("tag", event.Tag). | |
| 73 | + | Msg("event published") | |
| 74 | + | return nil | |
| 75 | + | case <-bus.ctx.Done(): | |
| 76 | + | return fmt.Errorf("event bus is stopped") | |
| 77 | + | default: | |
| 78 | + | return fmt.Errorf("event channel is full, dropping event %s", event.ID) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Subscribe adds an event handler to the bus. | |
| 83 | + | func (bus *InMemory) Subscribe(handler out.EventHandler) error { | |
| 84 | + | bus.mu.Lock() | |
| 85 | + | defer bus.mu.Unlock() | |
| 86 | + | ||
| 87 | + | bus.handlers = append(bus.handlers, handler) | |
| 88 | + | bus.log.Debug(). | |
| 89 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 90 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 91 | + | Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)). | |
| 92 | + | Int("total_handlers", len(bus.handlers)). | |
| 93 | + | Msg("event handler subscribed") | |
| 94 | + | ||
| 95 | + | return nil | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Unsubscribe removes an event handler from the bus. | |
| 99 | + | func (bus *InMemory) Unsubscribe(handler out.EventHandler) error { | |
| 100 | + | bus.mu.Lock() | |
| 101 | + | defer bus.mu.Unlock() | |
| 102 | + | ||
| 103 | + | for i, h := range bus.handlers { | |
| 104 | + | if h == handler { | |
| 105 | + | bus.handlers = append(bus.handlers[:i], bus.handlers[i+1:]...) | |
| 106 | + | bus.log.Debug(). | |
| 107 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 108 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 109 | + | Str(zerowrap.FieldHandler, fmt.Sprintf("%T", handler)). | |
| 110 | + | Int("total_handlers", len(bus.handlers)). | |
| 111 | + | Msg("event handler unsubscribed") | |
| 112 | + | return nil | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | return fmt.Errorf("handler not found") | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // Start starts the event bus processing loop. | |
| 120 | + | func (bus *InMemory) Start() error { | |
| 121 | + | bus.log.Info(). | |
| 122 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 123 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 124 | + | Int("buffer_size", bus.bufferSize). | |
| 125 | + | Msg("starting event bus") | |
| 126 | + | ||
| 127 | + | go bus.processEvents() | |
| 128 | + | return nil | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // Stop stops the event bus. | |
| 132 | + | func (bus *InMemory) Stop() error { | |
| 133 | + | bus.log.Info(). | |
| 134 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 135 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 136 | + | Msg("stopping event bus") | |
| 137 | + | ||
| 138 | + | bus.cancel() | |
| 139 | + | ||
| 140 | + | select { | |
| 141 | + | case <-bus.done: | |
| 142 | + | bus.log.Info(). | |
| 143 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 144 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 145 | + | Msg("event bus stopped") | |
| 146 | + | return nil | |
| 147 | + | case <-time.After(5 * time.Second): | |
| 148 | + | bus.log.Warn(). | |
| 149 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 150 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 151 | + | Msg("event bus stop timeout") | |
| 152 | + | return fmt.Errorf("timeout waiting for event bus to stop") | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (bus *InMemory) processEvents() { | |
| 157 | + | defer close(bus.done) | |
| 158 | + | ||
| 159 | + | for { | |
| 160 | + | select { | |
| 161 | + | case event := <-bus.eventChan: | |
| 162 | + | bus.handleEvent(event) | |
| 163 | + | case <-bus.ctx.Done(): | |
| 164 | + | bus.log.Debug(). | |
| 165 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 166 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 167 | + | Msg("event bus processing stopped") | |
| 168 | + | return | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func (bus *InMemory) handleEvent(event domain.Event) { | |
| 174 | + | bus.mu.RLock() | |
| 175 | + | handlers := make([]out.EventHandler, len(bus.handlers)) | |
| 176 | + | copy(handlers, bus.handlers) | |
| 177 | + | bus.mu.RUnlock() | |
| 178 | + | ||
| 179 | + | for _, handler := range handlers { | |
| 180 | + | if handler.CanHandle(event.Type) { | |
| 181 | + | go func(h out.EventHandler) { | |
| 182 | + | start := time.Now() | |
| 183 | + | ||
| 184 | + | if err := h.Handle(event); err != nil { | |
| 185 | + | bus.log.Error(). | |
| 186 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 187 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 188 | + | Err(err). | |
| 189 | + | Str("event_id", event.ID). | |
| 190 | + | Str(zerowrap.FieldEvent, string(event.Type)). | |
| 191 | + | Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)). | |
| 192 | + | Msg("error handling event") | |
| 193 | + | } else { | |
| 194 | + | bus.log.Debug(). | |
| 195 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 196 | + | Str(zerowrap.FieldAdapter, "eventbus"). | |
| 197 | + | Str("event_id", event.ID). | |
| 198 | + | Str(zerowrap.FieldEvent, string(event.Type)). | |
| 199 | + | Str(zerowrap.FieldHandler, fmt.Sprintf("%T", h)). | |
| 200 | + | Dur(zerowrap.FieldDuration, time.Since(start)). | |
| 201 | + | Msg("event handled successfully") | |
| 202 | + | } | |
| 203 | + | }(handler) | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
A internal/adapters/out/filesystem/blob.go +294 -0
| @@ -0,0 +1,294 @@ | |||
| 1 | + | // Package filesystem implements storage adapters using the local filesystem. | |
| 2 | + | package filesystem | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "fmt" | |
| 6 | + | "io" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "strings" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // BlobStorage implements the BlobStorage interface using the local filesystem. | |
| 16 | + | type BlobStorage struct { | |
| 17 | + | rootDir string | |
| 18 | + | log zerowrap.Logger | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // NewBlobStorage creates a new filesystem blob storage instance. | |
| 22 | + | func NewBlobStorage(rootDir string, log zerowrap.Logger) (*BlobStorage, error) { | |
| 23 | + | // Create directory structure if it doesn't exist | |
| 24 | + | dirs := []string{ | |
| 25 | + | filepath.Join(rootDir, "blobs"), | |
| 26 | + | filepath.Join(rootDir, "uploads"), | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | for _, dir := range dirs { | |
| 30 | + | if err := os.MkdirAll(dir, 0750); err != nil { | |
| 31 | + | return nil, fmt.Errorf("failed to create directory %s: %w", dir, err) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | log.Info(). | |
| 36 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 37 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 38 | + | Str("root_dir", rootDir). | |
| 39 | + | Msg("blob storage initialized") | |
| 40 | + | ||
| 41 | + | return &BlobStorage{ | |
| 42 | + | rootDir: rootDir, | |
| 43 | + | log: log, | |
| 44 | + | }, nil | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // GetBlob retrieves a blob by digest. | |
| 48 | + | func (s *BlobStorage) GetBlob(digest string) (io.ReadCloser, error) { | |
| 49 | + | blobPath := s.getBlobPath(digest) | |
| 50 | + | ||
| 51 | + | file, err := os.Open(blobPath) | |
| 52 | + | if err != nil { | |
| 53 | + | if os.IsNotExist(err) { | |
| 54 | + | return nil, fmt.Errorf("blob not found: %s", digest) | |
| 55 | + | } | |
| 56 | + | return nil, fmt.Errorf("failed to open blob: %w", err) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | return file, nil | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // GetBlobPath returns the filesystem path to a blob. | |
| 63 | + | func (s *BlobStorage) GetBlobPath(digest string) (string, error) { | |
| 64 | + | path := s.getBlobPath(digest) | |
| 65 | + | if _, err := os.Stat(path); err != nil { | |
| 66 | + | if os.IsNotExist(err) { | |
| 67 | + | return "", fmt.Errorf("blob not found: %s", digest) | |
| 68 | + | } | |
| 69 | + | return "", err | |
| 70 | + | } | |
| 71 | + | return path, nil | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // PutBlob stores a blob with the given digest. | |
| 75 | + | func (s *BlobStorage) PutBlob(digest string, data io.Reader, size int64) error { | |
| 76 | + | blobPath := s.getBlobPath(digest) | |
| 77 | + | ||
| 78 | + | // Create directory if it doesn't exist | |
| 79 | + | if err := os.MkdirAll(filepath.Dir(blobPath), 0750); err != nil { | |
| 80 | + | return fmt.Errorf("failed to create blob directory: %w", err) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Create temporary file first | |
| 84 | + | tmpPath := blobPath + ".tmp" | |
| 85 | + | file, err := os.Create(tmpPath) | |
| 86 | + | if err != nil { | |
| 87 | + | return fmt.Errorf("failed to create temporary blob file: %w", err) | |
| 88 | + | } | |
| 89 | + | defer file.Close() | |
| 90 | + | ||
| 91 | + | // Copy data to file | |
| 92 | + | written, err := io.Copy(file, data) | |
| 93 | + | if err != nil { | |
| 94 | + | os.Remove(tmpPath) | |
| 95 | + | return fmt.Errorf("failed to write blob data: %w", err) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Verify size if provided | |
| 99 | + | if size > 0 && written != size { | |
| 100 | + | os.Remove(tmpPath) | |
| 101 | + | return fmt.Errorf("blob size mismatch: expected %d, got %d", size, written) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // Move to final location | |
| 105 | + | if err := os.Rename(tmpPath, blobPath); err != nil { | |
| 106 | + | os.Remove(tmpPath) | |
| 107 | + | return fmt.Errorf("failed to move blob to final location: %w", err) | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | s.log.Info(). | |
| 111 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 112 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 113 | + | Str("digest", digest). | |
| 114 | + | Int64(zerowrap.FieldSize, written). | |
| 115 | + | Msg("blob stored") | |
| 116 | + | ||
| 117 | + | return nil | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // DeleteBlob removes a blob by digest. | |
| 121 | + | func (s *BlobStorage) DeleteBlob(digest string) error { | |
| 122 | + | blobPath := s.getBlobPath(digest) | |
| 123 | + | ||
| 124 | + | if err := os.Remove(blobPath); err != nil { | |
| 125 | + | if os.IsNotExist(err) { | |
| 126 | + | return fmt.Errorf("blob not found: %s", digest) | |
| 127 | + | } | |
| 128 | + | return fmt.Errorf("failed to delete blob: %w", err) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | s.log.Info(). | |
| 132 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 133 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 134 | + | Str("digest", digest). | |
| 135 | + | Msg("blob deleted") | |
| 136 | + | ||
| 137 | + | return nil | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // BlobExists checks if a blob exists. | |
| 141 | + | func (s *BlobStorage) BlobExists(digest string) bool { | |
| 142 | + | blobPath := s.getBlobPath(digest) | |
| 143 | + | _, err := os.Stat(blobPath) | |
| 144 | + | return err == nil | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // StartBlobUpload starts a new blob upload and returns the upload UUID. | |
| 148 | + | func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |
| 149 | + | // Generate UUID-like upload ID using timestamp | |
| 150 | + | uuid := fmt.Sprintf("%d-%s", time.Now().UnixNano(), strings.ReplaceAll(name, "/", "_")) | |
| 151 | + | ||
| 152 | + | uploadPath := s.getUploadPath(uuid) | |
| 153 | + | ||
| 154 | + | // Create directory if it doesn't exist | |
| 155 | + | if err := os.MkdirAll(filepath.Dir(uploadPath), 0750); err != nil { | |
| 156 | + | return "", fmt.Errorf("failed to create upload directory: %w", err) | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // Create empty upload file | |
| 160 | + | file, err := os.Create(uploadPath) | |
| 161 | + | if err != nil { | |
| 162 | + | return "", fmt.Errorf("failed to create upload file: %w", err) | |
| 163 | + | } | |
| 164 | + | file.Close() | |
| 165 | + | ||
| 166 | + | s.log.Info(). | |
| 167 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 168 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 169 | + | Str("uuid", uuid). | |
| 170 | + | Str("name", name). | |
| 171 | + | Msg("blob upload started") | |
| 172 | + | ||
| 173 | + | return uuid, nil | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // AppendBlobChunk appends data to an in-progress upload. | |
| 177 | + | func (s *BlobStorage) AppendBlobChunk(name, uuid string, chunk []byte) (int64, error) { | |
| 178 | + | uploadPath := s.getUploadPath(uuid) | |
| 179 | + | ||
| 180 | + | file, err := os.OpenFile(uploadPath, os.O_WRONLY|os.O_APPEND, 0600) | |
| 181 | + | if err != nil { | |
| 182 | + | if os.IsNotExist(err) { | |
| 183 | + | return 0, fmt.Errorf("upload not found: %s", uuid) | |
| 184 | + | } | |
| 185 | + | return 0, fmt.Errorf("failed to open upload file: %w", err) | |
| 186 | + | } | |
| 187 | + | defer file.Close() | |
| 188 | + | ||
| 189 | + | written, err := file.Write(chunk) | |
| 190 | + | if err != nil { | |
| 191 | + | return 0, fmt.Errorf("failed to write chunk to upload file: %w", err) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | fi, err := file.Stat() | |
| 195 | + | if err != nil { | |
| 196 | + | return 0, fmt.Errorf("failed to get file info: %w", err) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | s.log.Debug(). | |
| 200 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 201 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 202 | + | Str("uuid", uuid). | |
| 203 | + | Str("name", name). | |
| 204 | + | Int("chunk_size", written). | |
| 205 | + | Int64("total_size", fi.Size()). | |
| 206 | + | Msg("appended chunk to blob upload") | |
| 207 | + | ||
| 208 | + | return fi.Size(), nil | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // GetBlobUpload returns a writer for the upload. | |
| 212 | + | func (s *BlobStorage) GetBlobUpload(uuid string) (io.WriteCloser, error) { | |
| 213 | + | uploadPath := s.getUploadPath(uuid) | |
| 214 | + | ||
| 215 | + | file, err := os.OpenFile(uploadPath, os.O_WRONLY|os.O_APPEND, 0600) | |
| 216 | + | if err != nil { | |
| 217 | + | if os.IsNotExist(err) { | |
| 218 | + | return nil, fmt.Errorf("upload not found: %s", uuid) | |
| 219 | + | } | |
| 220 | + | return nil, fmt.Errorf("failed to open upload file: %w", err) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | return file, nil | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | // FinishBlobUpload completes an upload and moves it to blob storage. | |
| 227 | + | func (s *BlobStorage) FinishBlobUpload(uuid, digest string) error { | |
| 228 | + | uploadPath := s.getUploadPath(uuid) | |
| 229 | + | blobPath := s.getBlobPath(digest) | |
| 230 | + | ||
| 231 | + | // Create blob directory if it doesn't exist | |
| 232 | + | if err := os.MkdirAll(filepath.Dir(blobPath), 0750); err != nil { | |
| 233 | + | return fmt.Errorf("failed to create blob directory: %w", err) | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // Move upload file to blob location | |
| 237 | + | if err := os.Rename(uploadPath, blobPath); err != nil { | |
| 238 | + | return fmt.Errorf("failed to move upload to blob location: %w", err) | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | s.log.Info(). | |
| 242 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 243 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 244 | + | Str("uuid", uuid). | |
| 245 | + | Str("digest", digest). | |
| 246 | + | Msg("blob upload finished") | |
| 247 | + | ||
| 248 | + | return nil | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // CancelBlobUpload cancels an in-progress upload. | |
| 252 | + | func (s *BlobStorage) CancelBlobUpload(uuid string) error { | |
| 253 | + | uploadPath := s.getUploadPath(uuid) | |
| 254 | + | ||
| 255 | + | if err := os.Remove(uploadPath); err != nil { | |
| 256 | + | if os.IsNotExist(err) { | |
| 257 | + | return fmt.Errorf("upload not found: %s", uuid) | |
| 258 | + | } | |
| 259 | + | return fmt.Errorf("failed to cancel upload: %w", err) | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | s.log.Info(). | |
| 263 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 264 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 265 | + | Str("uuid", uuid). | |
| 266 | + | Msg("blob upload cancelled") | |
| 267 | + | ||
| 268 | + | return nil | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // Helper methods for path generation | |
| 272 | + | ||
| 273 | + | func (s *BlobStorage) getBlobPath(digest string) string { | |
| 274 | + | // Split digest into directory structure (e.g., sha256:abc123... -> sha256/ab/abc123...) | |
| 275 | + | parts := strings.SplitN(digest, ":", 2) | |
| 276 | + | if len(parts) != 2 { | |
| 277 | + | // Fallback if digest format is unexpected | |
| 278 | + | return filepath.Join(s.rootDir, "blobs", digest) | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | algorithm := parts[0] | |
| 282 | + | hash := parts[1] | |
| 283 | + | ||
| 284 | + | // Create two-level directory structure for better performance | |
| 285 | + | if len(hash) >= 2 { | |
| 286 | + | return filepath.Join(s.rootDir, "blobs", algorithm, hash[:2], hash) | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | return filepath.Join(s.rootDir, "blobs", algorithm, hash) | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | func (s *BlobStorage) getUploadPath(uuid string) string { | |
| 293 | + | return filepath.Join(s.rootDir, "uploads", uuid) | |
| 294 | + | } | |
A internal/adapters/out/filesystem/manifest.go +304 -0
| @@ -0,0 +1,304 @@ | |||
| 1 | + | package filesystem | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/zerowrap" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // ManifestStorage implements the ManifestStorage interface using the local filesystem. | |
| 13 | + | type ManifestStorage struct { | |
| 14 | + | rootDir string | |
| 15 | + | log zerowrap.Logger | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // NewManifestStorage creates a new filesystem manifest storage instance. | |
| 19 | + | func NewManifestStorage(rootDir string, log zerowrap.Logger) (*ManifestStorage, error) { | |
| 20 | + | // Create directory structure if it doesn't exist | |
| 21 | + | reposDir := filepath.Join(rootDir, "repositories") | |
| 22 | + | if err := os.MkdirAll(reposDir, 0750); err != nil { | |
| 23 | + | return nil, fmt.Errorf("failed to create repositories directory: %w", err) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | log.Info(). | |
| 27 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 28 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 29 | + | Str("root_dir", rootDir). | |
| 30 | + | Msg("manifest storage initialized") | |
| 31 | + | ||
| 32 | + | return &ManifestStorage{ | |
| 33 | + | rootDir: rootDir, | |
| 34 | + | log: log, | |
| 35 | + | }, nil | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // GetManifest retrieves a manifest by name and reference. | |
| 39 | + | // Returns the manifest data and content type. | |
| 40 | + | func (s *ManifestStorage) GetManifest(name, reference string) ([]byte, string, error) { | |
| 41 | + | manifestPath := s.getManifestPath(name, reference) | |
| 42 | + | ||
| 43 | + | data, err := os.ReadFile(manifestPath) | |
| 44 | + | if err != nil { | |
| 45 | + | if os.IsNotExist(err) { | |
| 46 | + | return nil, "", fmt.Errorf("manifest not found: %s/%s", name, reference) | |
| 47 | + | } | |
| 48 | + | return nil, "", fmt.Errorf("failed to read manifest: %w", err) | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | contentType, err := s.getManifestContentType(name, reference) | |
| 52 | + | if err != nil { | |
| 53 | + | // Fallback for manifests stored before content type tracking | |
| 54 | + | s.log.Warn(). | |
| 55 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 56 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 57 | + | Err(err). | |
| 58 | + | Str("name", name). | |
| 59 | + | Str("reference", reference). | |
| 60 | + | Msg("could not get manifest content type, falling back to default") | |
| 61 | + | contentType = "application/vnd.docker.distribution.manifest.v2+json" | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | return data, contentType, nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // PutManifest stores a manifest. | |
| 68 | + | func (s *ManifestStorage) PutManifest(name, reference, contentType string, data []byte) error { | |
| 69 | + | manifestPath := s.getManifestPath(name, reference) | |
| 70 | + | ||
| 71 | + | // Create directory if it doesn't exist | |
| 72 | + | if err := os.MkdirAll(filepath.Dir(manifestPath), 0750); err != nil { | |
| 73 | + | return fmt.Errorf("failed to create manifest directory: %w", err) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // Write manifest file | |
| 77 | + | if err := os.WriteFile(manifestPath, data, 0600); err != nil { | |
| 78 | + | return fmt.Errorf("failed to write manifest: %w", err) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // Store content type | |
| 82 | + | if err := s.putManifestContentType(name, reference, contentType); err != nil { | |
| 83 | + | // Don't fail the whole operation, but log a warning | |
| 84 | + | s.log.Warn(). | |
| 85 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 86 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 87 | + | Err(err). | |
| 88 | + | Str("name", name). | |
| 89 | + | Str("reference", reference). | |
| 90 | + | Msg("failed to store manifest content type") | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Update tags list | |
| 94 | + | if err := s.updateTagsList(name, reference); err != nil { | |
| 95 | + | s.log.Warn(). | |
| 96 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 97 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 98 | + | Err(err). | |
| 99 | + | Str("name", name). | |
| 100 | + | Str("reference", reference). | |
| 101 | + | Msg("failed to update tags list") | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | s.log.Info(). | |
| 105 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 106 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 107 | + | Str("name", name). | |
| 108 | + | Str("reference", reference). | |
| 109 | + | Msg("manifest stored") | |
| 110 | + | ||
| 111 | + | return nil | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // DeleteManifest removes a manifest. | |
| 115 | + | func (s *ManifestStorage) DeleteManifest(name, reference string) error { | |
| 116 | + | manifestPath := s.getManifestPath(name, reference) | |
| 117 | + | ||
| 118 | + | if err := os.Remove(manifestPath); err != nil { | |
| 119 | + | if os.IsNotExist(err) { | |
| 120 | + | return fmt.Errorf("manifest not found: %s/%s", name, reference) | |
| 121 | + | } | |
| 122 | + | return fmt.Errorf("failed to delete manifest: %w", err) | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // Delete content type file | |
| 126 | + | if err := s.deleteManifestContentType(name, reference); err != nil { | |
| 127 | + | s.log.Warn(). | |
| 128 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 129 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 130 | + | Err(err). | |
| 131 | + | Str("name", name). | |
| 132 | + | Str("reference", reference). | |
| 133 | + | Msg("failed to delete manifest content type") | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // Remove from tags list | |
| 137 | + | if err := s.removeFromTagsList(name, reference); err != nil { | |
| 138 | + | s.log.Warn(). | |
| 139 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 140 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 141 | + | Err(err). | |
| 142 | + | Str("name", name). | |
| 143 | + | Str("reference", reference). | |
| 144 | + | Msg("failed to update tags list") | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | s.log.Info(). | |
| 148 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 149 | + | Str(zerowrap.FieldAdapter, "filesystem"). | |
| 150 | + | Str("name", name). | |
| 151 | + | Str("reference", reference). | |
| 152 | + | Msg("manifest deleted") | |
| 153 | + | ||
| 154 | + | return nil | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // ListTags returns all tags for a repository. | |
| 158 | + | func (s *ManifestStorage) ListTags(name string) ([]string, error) { | |
| 159 | + | tagsPath := s.getTagsPath(name) | |
| 160 | + | ||
| 161 | + | data, err := os.ReadFile(tagsPath) | |
| 162 | + | if err != nil { | |
| 163 | + | if os.IsNotExist(err) { | |
| 164 | + | return []string{}, nil // No tags file means no tags | |
| 165 | + | } | |
| 166 | + | return nil, fmt.Errorf("failed to read tags file: %w", err) | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | var tags []string | |
| 170 | + | if err := json.Unmarshal(data, &tags); err != nil { | |
| 171 | + | return nil, fmt.Errorf("failed to parse tags file: %w", err) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | return tags, nil | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // ListRepositories returns all repository names. | |
| 178 | + | func (s *ManifestStorage) ListRepositories() ([]string, error) { | |
| 179 | + | reposDir := filepath.Join(s.rootDir, "repositories") | |
| 180 | + | ||
| 181 | + | var repositories []string | |
| 182 | + | err := filepath.Walk(reposDir, func(path string, info os.FileInfo, err error) error { | |
| 183 | + | if err != nil { | |
| 184 | + | return err | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | if info.IsDir() && path != reposDir { | |
| 188 | + | // Get relative path from repositories directory | |
| 189 | + | relPath, err := filepath.Rel(reposDir, path) | |
| 190 | + | if err != nil { | |
| 191 | + | return err | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // Check if this directory contains manifests | |
| 195 | + | manifestsDir := filepath.Join(path, "manifests") | |
| 196 | + | if _, err := os.Stat(manifestsDir); err == nil { | |
| 197 | + | repositories = append(repositories, relPath) | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | return nil | |
| 202 | + | }) | |
| 203 | + | ||
| 204 | + | if err != nil { | |
| 205 | + | return nil, fmt.Errorf("failed to list repositories: %w", err) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | return repositories, nil | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // Helper methods for path generation | |
| 212 | + | ||
| 213 | + | func (s *ManifestStorage) getManifestPath(name, reference string) string { | |
| 214 | + | return filepath.Join(s.rootDir, "repositories", name, "manifests", reference) | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | func (s *ManifestStorage) getManifestContentTypePath(name, reference string) string { | |
| 218 | + | return s.getManifestPath(name, reference) + ".contenttype" | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | func (s *ManifestStorage) getTagsPath(name string) string { | |
| 222 | + | return filepath.Join(s.rootDir, "repositories", name, "tags.json") | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // Content type helpers | |
| 226 | + | ||
| 227 | + | func (s *ManifestStorage) getManifestContentType(name, reference string) (string, error) { | |
| 228 | + | contentTypePath := s.getManifestContentTypePath(name, reference) | |
| 229 | + | data, err := os.ReadFile(contentTypePath) | |
| 230 | + | if err != nil { | |
| 231 | + | return "", err | |
| 232 | + | } | |
| 233 | + | return string(data), nil | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | func (s *ManifestStorage) putManifestContentType(name, reference, contentType string) error { | |
| 237 | + | contentTypePath := s.getManifestContentTypePath(name, reference) | |
| 238 | + | return os.WriteFile(contentTypePath, []byte(contentType), 0600) | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func (s *ManifestStorage) deleteManifestContentType(name, reference string) error { | |
| 242 | + | contentTypePath := s.getManifestContentTypePath(name, reference) | |
| 243 | + | err := os.Remove(contentTypePath) | |
| 244 | + | if err != nil && !os.IsNotExist(err) { | |
| 245 | + | return err | |
| 246 | + | } | |
| 247 | + | return nil | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // Tags management helpers | |
| 251 | + | ||
| 252 | + | func (s *ManifestStorage) updateTagsList(name, reference string) error { | |
| 253 | + | tags, err := s.ListTags(name) | |
| 254 | + | if err != nil { | |
| 255 | + | return err | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // Add tag if not already present | |
| 259 | + | for _, tag := range tags { | |
| 260 | + | if tag == reference { | |
| 261 | + | return nil // Already exists | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | tags = append(tags, reference) | |
| 266 | + | return s.saveTagsList(name, tags) | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | func (s *ManifestStorage) removeFromTagsList(name, reference string) error { | |
| 270 | + | tags, err := s.ListTags(name) | |
| 271 | + | if err != nil { | |
| 272 | + | return err | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | // Remove tag if present | |
| 276 | + | var newTags []string | |
| 277 | + | for _, tag := range tags { | |
| 278 | + | if tag != reference { | |
| 279 | + | newTags = append(newTags, tag) | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | return s.saveTagsList(name, newTags) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | func (s *ManifestStorage) saveTagsList(name string, tags []string) error { | |
| 287 | + | tagsPath := s.getTagsPath(name) | |
| 288 | + | ||
| 289 | + | // Create directory if it doesn't exist | |
| 290 | + | if err := os.MkdirAll(filepath.Dir(tagsPath), 0750); err != nil { | |
| 291 | + | return fmt.Errorf("failed to create tags directory: %w", err) | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | data, err := json.Marshal(tags) | |
| 295 | + | if err != nil { | |
| 296 | + | return fmt.Errorf("failed to marshal tags: %w", err) | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | if err := os.WriteFile(tagsPath, data, 0600); err != nil { | |
| 300 | + | return fmt.Errorf("failed to write tags file: %w", err) | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | return nil | |
| 304 | + | } | |
A internal/adapters/out/secrets/pass.go +67 -0
| @@ -0,0 +1,67 @@ | |||
| 1 | + | // Package secrets implements secret provider adapters. | |
| 2 | + | package secrets | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "os/exec" | |
| 8 | + | "strings" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/zerowrap" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // PassProvider implements the SecretProvider interface using the pass password manager. | |
| 15 | + | type PassProvider struct { | |
| 16 | + | timeout time.Duration | |
| 17 | + | log zerowrap.Logger | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // NewPassProvider creates a new pass provider. | |
| 21 | + | func NewPassProvider(log zerowrap.Logger) *PassProvider { | |
| 22 | + | return &PassProvider{ | |
| 23 | + | timeout: 10 * time.Second, | |
| 24 | + | log: log, | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Name returns the provider name. | |
| 29 | + | func (p *PassProvider) Name() string { | |
| 30 | + | return "pass" | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // GetSecret retrieves a secret from pass by path. | |
| 34 | + | func (p *PassProvider) GetSecret(ctx context.Context, path string) (string, error) { | |
| 35 | + | ctx, cancel := context.WithTimeout(ctx, p.timeout) | |
| 36 | + | defer cancel() | |
| 37 | + | ||
| 38 | + | cmd := exec.CommandContext(ctx, "pass", "show", path) | |
| 39 | + | output, err := cmd.Output() | |
| 40 | + | if err != nil { | |
| 41 | + | if exitError, ok := err.(*exec.ExitError); ok { | |
| 42 | + | return "", fmt.Errorf("pass command failed: %s", string(exitError.Stderr)) | |
| 43 | + | } | |
| 44 | + | return "", fmt.Errorf("failed to execute pass command: %w", err) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | secret := strings.TrimSpace(string(output)) | |
| 48 | + | if secret == "" { | |
| 49 | + | return "", fmt.Errorf("empty secret returned from pass for path: %s", path) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | p.log.Debug(). | |
| 53 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 54 | + | Str(zerowrap.FieldAdapter, "secrets"). | |
| 55 | + | Str("provider", "pass"). | |
| 56 | + | Str("path", path). | |
| 57 | + | Msg("successfully retrieved secret from pass") | |
| 58 | + | ||
| 59 | + | return secret, nil | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // IsAvailable checks if pass is available in the system. | |
| 63 | + | func (p *PassProvider) IsAvailable() bool { | |
| 64 | + | cmd := exec.Command("pass", "version") | |
| 65 | + | err := cmd.Run() | |
| 66 | + | return err == nil | |
| 67 | + | } | |
A internal/adapters/out/secrets/sops.go +94 -0
| @@ -0,0 +1,94 @@ | |||
| 1 | + | package secrets | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "os/exec" | |
| 7 | + | "path/filepath" | |
| 8 | + | "regexp" | |
| 9 | + | "strings" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/zerowrap" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // sopsKeyPathRegex validates sops key paths (alphanumeric, dots, underscores, hyphens, brackets). | |
| 16 | + | var sopsKeyPathRegex = regexp.MustCompile(`^[a-zA-Z0-9._\-\[\]]+$`) | |
| 17 | + | ||
| 18 | + | // SopsProvider implements the SecretProvider interface using sops. | |
| 19 | + | type SopsProvider struct { | |
| 20 | + | timeout time.Duration | |
| 21 | + | log zerowrap.Logger | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // NewSopsProvider creates a new sops provider. | |
| 25 | + | func NewSopsProvider(log zerowrap.Logger) *SopsProvider { | |
| 26 | + | return &SopsProvider{ | |
| 27 | + | timeout: 10 * time.Second, | |
| 28 | + | log: log, | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // Name returns the provider name. | |
| 33 | + | func (s *SopsProvider) Name() string { | |
| 34 | + | return "sops" | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // GetSecret retrieves a secret from a sops-encrypted file. | |
| 38 | + | // The path format is "file.yaml:key.nested.path". | |
| 39 | + | func (s *SopsProvider) GetSecret(ctx context.Context, path string) (string, error) { | |
| 40 | + | ctx, cancel := context.WithTimeout(ctx, s.timeout) | |
| 41 | + | defer cancel() | |
| 42 | + | ||
| 43 | + | // Extract key from path format: file.yaml:key.nested.path | |
| 44 | + | parts := strings.SplitN(path, ":", 2) | |
| 45 | + | if len(parts) != 2 { | |
| 46 | + | return "", fmt.Errorf("sops path must be in format 'file:key', got: %s", path) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | filePath := parts[0] | |
| 50 | + | keyPath := parts[1] | |
| 51 | + | ||
| 52 | + | // Validate file path to prevent command injection | |
| 53 | + | cleanPath := filepath.Clean(filePath) | |
| 54 | + | if strings.Contains(cleanPath, "..") { | |
| 55 | + | return "", fmt.Errorf("invalid file path: path traversal not allowed") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Validate key path to prevent command injection | |
| 59 | + | if !sopsKeyPathRegex.MatchString(keyPath) { | |
| 60 | + | return "", fmt.Errorf("invalid key path: must contain only alphanumeric characters, dots, underscores, hyphens, and brackets") | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Use sops -d --extract to get specific key | |
| 64 | + | // #nosec G204 - filePath and keyPath are validated above | |
| 65 | + | cmd := exec.CommandContext(ctx, "sops", "-d", "--extract", fmt.Sprintf("[\"%s\"]", keyPath), cleanPath) | |
| 66 | + | output, err := cmd.Output() | |
| 67 | + | if err != nil { | |
| 68 | + | if exitError, ok := err.(*exec.ExitError); ok { | |
| 69 | + | return "", fmt.Errorf("sops command failed: %s", string(exitError.Stderr)) | |
| 70 | + | } | |
| 71 | + | return "", fmt.Errorf("failed to execute sops command: %w", err) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | secret := strings.TrimSpace(string(output)) | |
| 75 | + | if secret == "" { | |
| 76 | + | return "", fmt.Errorf("empty secret returned from sops for path: %s", path) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | s.log.Debug(). | |
| 80 | + | Str(zerowrap.FieldLayer, "adapter"). | |
| 81 | + | Str(zerowrap.FieldAdapter, "secrets"). | |
| 82 | + | Str("provider", "sops"). | |
| 83 | + | Str("path", path). | |
| 84 | + | Msg("successfully retrieved secret from sops") | |
| 85 | + | ||
| 86 | + | return secret, nil | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // IsAvailable checks if sops is available in the system. | |
| 90 | + | func (s *SopsProvider) IsAvailable() bool { | |
| 91 | + | cmd := exec.Command("sops", "--version") | |
| 92 | + | err := cmd.Run() | |
| 93 | + | return err == nil | |
| 94 | + | } | |
A internal/app/run.go +547 -0
| @@ -0,0 +1,547 @@ | |||
| 1 | + | // Package app provides the application initialization and wiring. | |
| 2 | + | package app | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "net/http" | |
| 8 | + | "os" | |
| 9 | + | "os/signal" | |
| 10 | + | "path/filepath" | |
| 11 | + | "syscall" | |
| 12 | + | "time" | |
| 13 | + | ||
| 14 | + | "github.com/bnema/zerowrap" | |
| 15 | + | "github.com/fsnotify/fsnotify" | |
| 16 | + | "github.com/spf13/viper" | |
| 17 | + | ||
| 18 | + | // Adapters - Output | |
| 19 | + | "gordon/internal/adapters/out/docker" | |
| 20 | + | "gordon/internal/adapters/out/envloader" | |
| 21 | + | "gordon/internal/adapters/out/eventbus" | |
| 22 | + | "gordon/internal/adapters/out/filesystem" | |
| 23 | + | "gordon/internal/adapters/out/secrets" | |
| 24 | + | ||
| 25 | + | // Adapters - Input | |
| 26 | + | "gordon/internal/adapters/in/http/middleware" | |
| 27 | + | "gordon/internal/adapters/in/http/registry" | |
| 28 | + | ||
| 29 | + | // Use cases | |
| 30 | + | "gordon/internal/usecase/config" | |
| 31 | + | "gordon/internal/usecase/container" | |
| 32 | + | "gordon/internal/usecase/proxy" | |
| 33 | + | registrySvc "gordon/internal/usecase/registry" | |
| 34 | + | ) | |
| 35 | + | ||
| 36 | + | // Config holds the application configuration. | |
| 37 | + | type Config struct { | |
| 38 | + | Server struct { | |
| 39 | + | Port int `mapstructure:"port"` | |
| 40 | + | RegistryPort int `mapstructure:"registry_port"` | |
| 41 | + | RegistryDomain string `mapstructure:"registry_domain"` | |
| 42 | + | DataDir string `mapstructure:"data_dir"` | |
| 43 | + | } `mapstructure:"server"` | |
| 44 | + | ||
| 45 | + | Logging struct { | |
| 46 | + | Level string `mapstructure:"level"` | |
| 47 | + | Format string `mapstructure:"format"` | |
| 48 | + | File struct { | |
| 49 | + | Enabled bool `mapstructure:"enabled"` | |
| 50 | + | Path string `mapstructure:"path"` | |
| 51 | + | MaxSize int `mapstructure:"max_size"` | |
| 52 | + | MaxBackups int `mapstructure:"max_backups"` | |
| 53 | + | MaxAge int `mapstructure:"max_age"` | |
| 54 | + | } `mapstructure:"file"` | |
| 55 | + | } `mapstructure:"logging"` | |
| 56 | + | ||
| 57 | + | Env struct { | |
| 58 | + | Dir string `mapstructure:"dir"` | |
| 59 | + | } `mapstructure:"env"` | |
| 60 | + | ||
| 61 | + | RegistryAuth struct { | |
| 62 | + | Enabled bool `mapstructure:"enabled"` | |
| 63 | + | Username string `mapstructure:"username"` | |
| 64 | + | Password string `mapstructure:"password"` | |
| 65 | + | } `mapstructure:"registry_auth"` | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // services holds all the services used by the application. | |
| 69 | + | type services struct { | |
| 70 | + | runtime *docker.Runtime | |
| 71 | + | eventBus *eventbus.InMemory | |
| 72 | + | blobStorage *filesystem.BlobStorage | |
| 73 | + | manifestStorage *filesystem.ManifestStorage | |
| 74 | + | envLoader *envloader.FileLoader | |
| 75 | + | configSvc *config.Service | |
| 76 | + | containerSvc *container.Service | |
| 77 | + | registrySvc *registrySvc.Service | |
| 78 | + | proxySvc *proxy.Service | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // Run initializes and starts the Gordon application. | |
| 82 | + | func Run(ctx context.Context, configPath string) error { | |
| 83 | + | // Load configuration | |
| 84 | + | v, cfg, err := initConfig(configPath) | |
| 85 | + | if err != nil { | |
| 86 | + | return err | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Initialize logger | |
| 90 | + | log, cleanup, err := initLogger(cfg) | |
| 91 | + | if err != nil { | |
| 92 | + | return err | |
| 93 | + | } | |
| 94 | + | if cleanup != nil { | |
| 95 | + | defer cleanup() | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | ctx = zerowrap.WithCtx(ctx, log) | |
| 99 | + | log.Info().Msg("Gordon starting") | |
| 100 | + | ||
| 101 | + | // Create PID file | |
| 102 | + | pidFile := createPidFile(log) | |
| 103 | + | if pidFile != "" { | |
| 104 | + | defer removePidFile(pidFile, log) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // Create all services | |
| 108 | + | svc, err := createServices(ctx, v, cfg, log) | |
| 109 | + | if err != nil { | |
| 110 | + | return err | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Register event handlers | |
| 114 | + | if err := registerEventHandlers(ctx, svc); err != nil { | |
| 115 | + | return err | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // Set up config hot reload | |
| 119 | + | setupConfigHotReload(ctx, v, svc, log) | |
| 120 | + | ||
| 121 | + | // Start event bus | |
| 122 | + | if err := svc.eventBus.Start(); err != nil { | |
| 123 | + | return log.WrapErr(err, "failed to start event bus") | |
| 124 | + | } | |
| 125 | + | defer svc.eventBus.Stop() | |
| 126 | + | ||
| 127 | + | // Sync and auto-start containers | |
| 128 | + | syncAndAutoStart(ctx, svc, log) | |
| 129 | + | ||
| 130 | + | // Create HTTP handlers | |
| 131 | + | registryHandler, proxyHandler := createHTTPHandlers(svc, cfg, log) | |
| 132 | + | ||
| 133 | + | // Start servers and wait for shutdown | |
| 134 | + | return runServers(ctx, cfg, registryHandler, proxyHandler, svc.containerSvc, log) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // initConfig loads configuration from file. | |
| 138 | + | func initConfig(configPath string) (*viper.Viper, Config, error) { | |
| 139 | + | v := viper.New() | |
| 140 | + | if err := loadConfig(v, configPath); err != nil { | |
| 141 | + | return nil, Config{}, fmt.Errorf("failed to load config: %w", err) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | var cfg Config | |
| 145 | + | if err := v.Unmarshal(&cfg); err != nil { | |
| 146 | + | return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | return v, cfg, nil | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // initLogger initializes the zerowrap logger. | |
| 153 | + | func initLogger(cfg Config) (zerowrap.Logger, func(), error) { | |
| 154 | + | logConfig := zerowrap.Config{ | |
| 155 | + | Level: cfg.Logging.Level, | |
| 156 | + | Format: cfg.Logging.Format, | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | if cfg.Logging.File.Enabled { | |
| 160 | + | log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{ | |
| 161 | + | Enabled: true, | |
| 162 | + | Path: cfg.Logging.File.Path, | |
| 163 | + | MaxSize: cfg.Logging.File.MaxSize, | |
| 164 | + | MaxBackups: cfg.Logging.File.MaxBackups, | |
| 165 | + | MaxAge: cfg.Logging.File.MaxAge, | |
| 166 | + | Compress: true, | |
| 167 | + | }) | |
| 168 | + | if err != nil { | |
| 169 | + | return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err) | |
| 170 | + | } | |
| 171 | + | return log, cleanup, nil | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | return zerowrap.New(logConfig), nil, nil | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // createServices creates all the application services. | |
| 178 | + | func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowrap.Logger) (*services, error) { | |
| 179 | + | svc := &services{} | |
| 180 | + | var err error | |
| 181 | + | ||
| 182 | + | // Create output adapters | |
| 183 | + | if svc.runtime, svc.eventBus, err = createOutputAdapters(ctx, log); err != nil { | |
| 184 | + | return nil, err | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // Create storage | |
| 188 | + | if svc.blobStorage, svc.manifestStorage, err = createStorage(cfg, log); err != nil { | |
| 189 | + | return nil, err | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | // Create env loader | |
| 193 | + | if svc.envLoader, err = createEnvLoader(cfg, log); err != nil { | |
| 194 | + | return nil, err | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | // Create use case services | |
| 198 | + | svc.configSvc = config.NewService(v, svc.eventBus) | |
| 199 | + | if err := svc.configSvc.Load(ctx); err != nil { | |
| 200 | + | return nil, log.WrapErr(err, "failed to load configuration") | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | svc.containerSvc = createContainerService(v, cfg, svc) | |
| 204 | + | svc.registrySvc = registrySvc.NewService(svc.blobStorage, svc.manifestStorage, svc.eventBus) | |
| 205 | + | svc.proxySvc = proxy.NewService(svc.runtime, svc.containerSvc, svc.configSvc, proxy.Config{ | |
| 206 | + | RegistryDomain: cfg.Server.RegistryDomain, | |
| 207 | + | RegistryPort: cfg.Server.RegistryPort, | |
| 208 | + | }) | |
| 209 | + | ||
| 210 | + | return svc, nil | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // createOutputAdapters creates the Docker runtime and event bus. | |
| 214 | + | func createOutputAdapters(ctx context.Context, log zerowrap.Logger) (*docker.Runtime, *eventbus.InMemory, error) { | |
| 215 | + | runtime, err := docker.NewRuntime() | |
| 216 | + | if err != nil { | |
| 217 | + | return nil, nil, log.WrapErr(err, "failed to create Docker runtime") | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | if err := runtime.Ping(ctx); err != nil { | |
| 221 | + | return nil, nil, log.WrapErr(err, "Docker is not available") | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | dockerVersion, _ := runtime.Version(ctx) | |
| 225 | + | log.Info().Str("docker_version", dockerVersion).Msg("Docker runtime initialized") | |
| 226 | + | ||
| 227 | + | eventBus := eventbus.NewInMemory(100, log) | |
| 228 | + | ||
| 229 | + | return runtime, eventBus, nil | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // createStorage creates blob and manifest storage. | |
| 233 | + | func createStorage(cfg Config, log zerowrap.Logger) (*filesystem.BlobStorage, *filesystem.ManifestStorage, error) { | |
| 234 | + | dataDir := cfg.Server.DataDir | |
| 235 | + | if dataDir == "" { | |
| 236 | + | dataDir = "/var/lib/gordon" | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | registryDir := filepath.Join(dataDir, "registry") | |
| 240 | + | ||
| 241 | + | blobStorage, err := filesystem.NewBlobStorage(registryDir, log) | |
| 242 | + | if err != nil { | |
| 243 | + | return nil, nil, log.WrapErr(err, "failed to create blob storage") | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | manifestStorage, err := filesystem.NewManifestStorage(registryDir, log) | |
| 247 | + | if err != nil { | |
| 248 | + | return nil, nil, log.WrapErr(err, "failed to create manifest storage") | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | return blobStorage, manifestStorage, nil | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // createEnvLoader creates the environment loader with secret providers. | |
| 255 | + | func createEnvLoader(cfg Config, log zerowrap.Logger) (*envloader.FileLoader, error) { | |
| 256 | + | dataDir := cfg.Server.DataDir | |
| 257 | + | if dataDir == "" { | |
| 258 | + | dataDir = "/var/lib/gordon" | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | envDir := cfg.Env.Dir | |
| 262 | + | if envDir == "" { | |
| 263 | + | envDir = filepath.Join(dataDir, "env") | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | envLoader, err := envloader.NewFileLoader(envDir, log) | |
| 267 | + | if err != nil { | |
| 268 | + | return nil, log.WrapErr(err, "failed to create env loader") | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // Register secret providers | |
| 272 | + | passProvider := secrets.NewPassProvider(log) | |
| 273 | + | if passProvider.IsAvailable() { | |
| 274 | + | envLoader.RegisterSecretProvider(passProvider) | |
| 275 | + | log.Debug().Msg("pass secret provider registered") | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | sopsProvider := secrets.NewSopsProvider(log) | |
| 279 | + | if sopsProvider.IsAvailable() { | |
| 280 | + | envLoader.RegisterSecretProvider(sopsProvider) | |
| 281 | + | log.Debug().Msg("sops secret provider registered") | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | return envLoader, nil | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // createContainerService creates the container service with configuration. | |
| 288 | + | func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service { | |
| 289 | + | containerConfig := container.Config{ | |
| 290 | + | RegistryAuthEnabled: cfg.RegistryAuth.Enabled, | |
| 291 | + | RegistryDomain: cfg.Server.RegistryDomain, | |
| 292 | + | RegistryUsername: cfg.RegistryAuth.Username, | |
| 293 | + | RegistryPassword: cfg.RegistryAuth.Password, | |
| 294 | + | VolumeAutoCreate: v.GetBool("volumes.auto_create"), | |
| 295 | + | VolumePrefix: v.GetString("volumes.prefix"), | |
| 296 | + | VolumePreserve: v.GetBool("volumes.preserve"), | |
| 297 | + | NetworkIsolation: v.GetBool("network_isolation.enabled"), | |
| 298 | + | NetworkPrefix: v.GetString("network_isolation.network_prefix"), | |
| 299 | + | DNSSuffix: v.GetString("network_isolation.dns_suffix"), | |
| 300 | + | NetworkGroups: svc.configSvc.GetNetworkGroups(), | |
| 301 | + | Attachments: svc.configSvc.GetAttachments(), | |
| 302 | + | } | |
| 303 | + | return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, containerConfig) | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | // registerEventHandlers registers all event handlers. | |
| 307 | + | func registerEventHandlers(ctx context.Context, svc *services) error { | |
| 308 | + | imagePushedHandler := container.NewImagePushedHandler(ctx, svc.containerSvc, svc.configSvc) | |
| 309 | + | if err := svc.eventBus.Subscribe(imagePushedHandler); err != nil { | |
| 310 | + | return fmt.Errorf("failed to subscribe image pushed handler: %w", err) | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc) | |
| 314 | + | if err := svc.eventBus.Subscribe(configReloadHandler); err != nil { | |
| 315 | + | return fmt.Errorf("failed to subscribe config reload handler: %w", err) | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | return nil | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | // setupConfigHotReload sets up Viper config hot reload. | |
| 322 | + | func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, log zerowrap.Logger) { | |
| 323 | + | v.OnConfigChange(func(e fsnotify.Event) { | |
| 324 | + | log.Info().Str("file", e.Name).Msg("config file changed") | |
| 325 | + | ||
| 326 | + | if err := v.ReadInConfig(); err != nil { | |
| 327 | + | log.Error().Err(err).Msg("failed to reload config") | |
| 328 | + | return | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | if err := svc.configSvc.Load(ctx); err != nil { | |
| 332 | + | log.Error().Err(err).Msg("failed to reload configuration") | |
| 333 | + | return | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | svc.proxySvc.UpdateConfig(proxy.Config{ | |
| 337 | + | RegistryDomain: v.GetString("server.registry_domain"), | |
| 338 | + | RegistryPort: v.GetInt("server.registry_port"), | |
| 339 | + | }) | |
| 340 | + | }) | |
| 341 | + | v.WatchConfig() | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // syncAndAutoStart syncs existing containers and auto-starts if configured. | |
| 345 | + | func syncAndAutoStart(ctx context.Context, svc *services, log zerowrap.Logger) { | |
| 346 | + | if err := svc.containerSvc.SyncContainers(ctx); err != nil { | |
| 347 | + | log.Warn().Err(err).Msg("failed to sync existing containers") | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | if svc.configSvc.IsAutoRouteEnabled() { | |
| 351 | + | if err := svc.containerSvc.AutoStart(ctx); err != nil { | |
| 352 | + | log.Warn().Err(err).Msg("failed to auto-start containers") | |
| 353 | + | } | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | // createHTTPHandlers creates HTTP handlers with middleware. | |
| 358 | + | func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler) { | |
| 359 | + | registryHandler := registry.NewHandler(svc.registrySvc, svc.blobStorage, svc.eventBus, log) | |
| 360 | + | ||
| 361 | + | registryMiddlewares := []func(http.Handler) http.Handler{ | |
| 362 | + | middleware.PanicRecovery(log), | |
| 363 | + | middleware.RequestLogger(log), | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | if cfg.RegistryAuth.Enabled { | |
| 367 | + | registryMiddlewares = append(registryMiddlewares, | |
| 368 | + | middleware.RegistryAuth(cfg.RegistryAuth.Username, cfg.RegistryAuth.Password, log)) | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler) | |
| 372 | + | ||
| 373 | + | proxyMiddlewares := []func(http.Handler) http.Handler{ | |
| 374 | + | middleware.PanicRecovery(log), | |
| 375 | + | middleware.RequestLogger(log), | |
| 376 | + | middleware.CORS, | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | proxyWithMiddleware := middleware.Chain(proxyMiddlewares...)(svc.proxySvc) | |
| 380 | + | ||
| 381 | + | return registryWithMiddleware, proxyWithMiddleware | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // runServers starts the HTTP servers and waits for shutdown. | |
| 385 | + | func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler http.Handler, containerSvc *container.Service, log zerowrap.Logger) error { | |
| 386 | + | ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) | |
| 387 | + | defer cancel() | |
| 388 | + | ||
| 389 | + | errChan := make(chan error, 2) | |
| 390 | + | ||
| 391 | + | go startServer(fmt.Sprintf(":%d", cfg.Server.RegistryPort), registryHandler, "registry", errChan, log) | |
| 392 | + | go startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", errChan, log) | |
| 393 | + | ||
| 394 | + | log.Info(). | |
| 395 | + | Int("proxy_port", cfg.Server.Port). | |
| 396 | + | Int("registry_port", cfg.Server.RegistryPort). | |
| 397 | + | Msg("Gordon is running") | |
| 398 | + | ||
| 399 | + | select { | |
| 400 | + | case err := <-errChan: | |
| 401 | + | return err | |
| 402 | + | case <-ctx.Done(): | |
| 403 | + | log.Info().Msg("shutdown signal received") | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | log.Info().Msg("shutting down Gordon...") | |
| 407 | + | ||
| 408 | + | if err := containerSvc.Shutdown(ctx); err != nil { | |
| 409 | + | log.Warn().Err(err).Msg("error during container shutdown") | |
| 410 | + | } | |
| 411 | + | ||
| 412 | + | log.Info().Msg("Gordon stopped") | |
| 413 | + | return nil | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | // startServer starts an HTTP server. | |
| 417 | + | func startServer(addr string, handler http.Handler, name string, errChan chan<- error, log zerowrap.Logger) { | |
| 418 | + | log.Info().Str("address", addr).Msgf("%s server starting", name) | |
| 419 | + | ||
| 420 | + | server := &http.Server{ | |
| 421 | + | Addr: addr, | |
| 422 | + | Handler: handler, | |
| 423 | + | ReadHeaderTimeout: 10 * time.Second, | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| 427 | + | errChan <- fmt.Errorf("%s server error: %w", name, err) | |
| 428 | + | } | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | // SendReloadSignal sends SIGUSR1 to the running Gordon process. | |
| 432 | + | func SendReloadSignal() error { | |
| 433 | + | pidFile := findPidFile() | |
| 434 | + | if pidFile == "" { | |
| 435 | + | return fmt.Errorf("gordon PID file not found, is Gordon running?") | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | pidBytes, err := os.ReadFile(pidFile) | |
| 439 | + | if err != nil { | |
| 440 | + | return fmt.Errorf("failed to read PID file: %w", err) | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | var pid int | |
| 444 | + | if _, err := fmt.Sscanf(string(pidBytes), "%d", &pid); err != nil { | |
| 445 | + | return fmt.Errorf("failed to parse PID: %w", err) | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | process, err := os.FindProcess(pid) | |
| 449 | + | if err != nil { | |
| 450 | + | return fmt.Errorf("failed to find process: %w", err) | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | if err := process.Signal(syscall.SIGUSR1); err != nil { | |
| 454 | + | return fmt.Errorf("failed to send reload signal: %w", err) | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | return nil | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | // createPidFile creates a PID file for the Gordon process. | |
| 461 | + | func createPidFile(log zerowrap.Logger) string { | |
| 462 | + | pid := os.Getpid() | |
| 463 | + | ||
| 464 | + | locations := []string{ | |
| 465 | + | "/tmp/gordon.pid", | |
| 466 | + | filepath.Join(os.TempDir(), "gordon.pid"), | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 470 | + | locations = append(locations, filepath.Join(homeDir, ".gordon.pid")) | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | for _, location := range locations { | |
| 474 | + | if err := os.WriteFile(location, []byte(fmt.Sprintf("%d", pid)), 0600); err == nil { | |
| 475 | + | log.Debug().Str("pid_file", location).Int("pid", pid).Msg("created PID file") | |
| 476 | + | return location | |
| 477 | + | } | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | log.Warn().Int("pid", pid).Msg("failed to create PID file in any location") | |
| 481 | + | return "" | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | // removePidFile removes the PID file. | |
| 485 | + | func removePidFile(pidFile string, log zerowrap.Logger) { | |
| 486 | + | if err := os.Remove(pidFile); err != nil { | |
| 487 | + | log.Warn().Err(err).Str("pid_file", pidFile).Msg("failed to remove PID file") | |
| 488 | + | } else { | |
| 489 | + | log.Debug().Str("pid_file", pidFile).Msg("removed PID file") | |
| 490 | + | } | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | // findPidFile finds the Gordon PID file. | |
| 494 | + | func findPidFile() string { | |
| 495 | + | locations := []string{ | |
| 496 | + | "/tmp/gordon.pid", | |
| 497 | + | filepath.Join(os.TempDir(), "gordon.pid"), | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 501 | + | locations = append(locations, filepath.Join(homeDir, ".gordon.pid")) | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | for _, location := range locations { | |
| 505 | + | if _, err := os.Stat(location); err == nil { | |
| 506 | + | return location | |
| 507 | + | } | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | return "" | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | // loadConfig loads configuration from file and sets defaults. | |
| 514 | + | func loadConfig(v *viper.Viper, configPath string) error { | |
| 515 | + | v.SetDefault("server.port", 80) | |
| 516 | + | v.SetDefault("server.registry_port", 5000) | |
| 517 | + | v.SetDefault("server.data_dir", "/var/lib/gordon") | |
| 518 | + | v.SetDefault("logging.level", "info") | |
| 519 | + | v.SetDefault("logging.format", "console") | |
| 520 | + | v.SetDefault("logging.file.enabled", false) | |
| 521 | + | v.SetDefault("logging.file.max_size", 100) | |
| 522 | + | v.SetDefault("logging.file.max_backups", 3) | |
| 523 | + | v.SetDefault("logging.file.max_age", 28) | |
| 524 | + | v.SetDefault("env.dir", "/var/lib/gordon/env") | |
| 525 | + | v.SetDefault("registry_auth.enabled", false) | |
| 526 | + | ||
| 527 | + | if configPath != "" { | |
| 528 | + | v.SetConfigFile(configPath) | |
| 529 | + | } else { | |
| 530 | + | v.SetConfigName("gordon") | |
| 531 | + | v.SetConfigType("yaml") | |
| 532 | + | v.AddConfigPath("/etc/gordon") | |
| 533 | + | v.AddConfigPath("$HOME/.gordon") | |
| 534 | + | v.AddConfigPath(".") | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | if err := v.ReadInConfig(); err != nil { | |
| 538 | + | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { | |
| 539 | + | return fmt.Errorf("failed to read config file: %w", err) | |
| 540 | + | } | |
| 541 | + | } | |
| 542 | + | ||
| 543 | + | v.SetEnvPrefix("GORDON") | |
| 544 | + | v.AutomaticEnv() | |
| 545 | + | ||
| 546 | + | return nil | |
| 547 | + | } | |
A internal/boundaries/in/config.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package in | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "gordon/internal/domain" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // ConfigService defines the contract for configuration management. | |
| 10 | + | type ConfigService interface { | |
| 11 | + | // Load loads the configuration from the configured source. | |
| 12 | + | Load(ctx context.Context) error | |
| 13 | + | ||
| 14 | + | // GetRoutes returns all configured routes. | |
| 15 | + | GetRoutes(ctx context.Context) []domain.Route | |
| 16 | + | ||
| 17 | + | // AddRoute adds a new route to the configuration. | |
| 18 | + | AddRoute(ctx context.Context, route domain.Route) error | |
| 19 | + | ||
| 20 | + | // UpdateRoute updates an existing route. | |
| 21 | + | UpdateRoute(ctx context.Context, route domain.Route) error | |
| 22 | + | ||
| 23 | + | // RemoveRoute removes a route from the configuration. | |
| 24 | + | RemoveRoute(ctx context.Context, domain string) error | |
| 25 | + | ||
| 26 | + | // Watch starts watching for configuration changes. | |
| 27 | + | // The onChange callback is called when configuration changes are detected. | |
| 28 | + | Watch(ctx context.Context, onChange func()) error | |
| 29 | + | ||
| 30 | + | // GetServerPort returns the configured server port. | |
| 31 | + | GetServerPort() int | |
| 32 | + | ||
| 33 | + | // GetRegistryPort returns the configured registry port. | |
| 34 | + | GetRegistryPort() int | |
| 35 | + | ||
| 36 | + | // GetRegistryDomain returns the configured registry domain. | |
| 37 | + | GetRegistryDomain() string | |
| 38 | + | ||
| 39 | + | // GetDataDir returns the configured data directory. | |
| 40 | + | GetDataDir() string | |
| 41 | + | ||
| 42 | + | // IsAutoRouteEnabled returns whether auto-route is enabled. | |
| 43 | + | IsAutoRouteEnabled() bool | |
| 44 | + | ||
| 45 | + | // IsNetworkIsolationEnabled returns whether network isolation is enabled. | |
| 46 | + | IsNetworkIsolationEnabled() bool | |
| 47 | + | ||
| 48 | + | // GetNetworkPrefix returns the prefix for created networks. | |
| 49 | + | GetNetworkPrefix() string | |
| 50 | + | } | |
A internal/boundaries/in/container.go +40 -0
| @@ -0,0 +1,40 @@ | |||
| 1 | + | // Package in defines input ports (interfaces) for use cases. | |
| 2 | + | // These interfaces define the contract between driving adapters (HTTP, CLI) | |
| 3 | + | // and the business logic (use cases). | |
| 4 | + | package in | |
| 5 | + | ||
| 6 | + | import ( | |
| 7 | + | "context" | |
| 8 | + | ||
| 9 | + | "gordon/internal/domain" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // ContainerService defines the contract for container management operations. | |
| 13 | + | type ContainerService interface { | |
| 14 | + | // Deploy creates and starts a container for the given route. | |
| 15 | + | Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) | |
| 16 | + | ||
| 17 | + | // Stop stops a running container. | |
| 18 | + | Stop(ctx context.Context, containerID string) error | |
| 19 | + | ||
| 20 | + | // Remove removes a container, optionally forcing removal. | |
| 21 | + | Remove(ctx context.Context, containerID string, force bool) error | |
| 22 | + | ||
| 23 | + | // Get retrieves a container by domain name. | |
| 24 | + | Get(ctx context.Context, domain string) (*domain.Container, bool) | |
| 25 | + | ||
| 26 | + | // List returns all managed containers. | |
| 27 | + | List(ctx context.Context) map[string]*domain.Container | |
| 28 | + | ||
| 29 | + | // HealthCheck performs health checks on all containers. | |
| 30 | + | HealthCheck(ctx context.Context) map[string]bool | |
| 31 | + | ||
| 32 | + | // SyncContainers synchronizes containers with configured routes. | |
| 33 | + | SyncContainers(ctx context.Context) error | |
| 34 | + | ||
| 35 | + | // AutoStart starts containers for all configured routes. | |
| 36 | + | AutoStart(ctx context.Context) error | |
| 37 | + | ||
| 38 | + | // Shutdown gracefully shuts down all managed containers. | |
| 39 | + | Shutdown(ctx context.Context) error | |
| 40 | + | } | |
A internal/boundaries/in/mocks/mock_config_service.go +679 -0
| @@ -0,0 +1,679 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | ||
| 11 | + | mock "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // NewMockConfigService creates a new instance of MockConfigService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 15 | + | // The first argument is typically a *testing.T value. | |
| 16 | + | func NewMockConfigService(t interface { | |
| 17 | + | mock.TestingT | |
| 18 | + | Cleanup(func()) | |
| 19 | + | }) *MockConfigService { | |
| 20 | + | mock := &MockConfigService{} | |
| 21 | + | mock.Mock.Test(t) | |
| 22 | + | ||
| 23 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 24 | + | ||
| 25 | + | return mock | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // MockConfigService is an autogenerated mock type for the ConfigService type | |
| 29 | + | type MockConfigService struct { | |
| 30 | + | mock.Mock | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | type MockConfigService_Expecter struct { | |
| 34 | + | mock *mock.Mock | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (_m *MockConfigService) EXPECT() *MockConfigService_Expecter { | |
| 38 | + | return &MockConfigService_Expecter{mock: &_m.Mock} | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // AddRoute provides a mock function for the type MockConfigService | |
| 42 | + | func (_mock *MockConfigService) AddRoute(ctx context.Context, route domain.Route) error { | |
| 43 | + | ret := _mock.Called(ctx, route) | |
| 44 | + | ||
| 45 | + | if len(ret) == 0 { | |
| 46 | + | panic("no return value specified for AddRoute") | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | var r0 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) error); ok { | |
| 51 | + | r0 = returnFunc(ctx, route) | |
| 52 | + | } else { | |
| 53 | + | r0 = ret.Error(0) | |
| 54 | + | } | |
| 55 | + | return r0 | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // MockConfigService_AddRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddRoute' | |
| 59 | + | type MockConfigService_AddRoute_Call struct { | |
| 60 | + | *mock.Call | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // AddRoute is a helper method to define mock.On call | |
| 64 | + | // - ctx context.Context | |
| 65 | + | // - route domain.Route | |
| 66 | + | func (_e *MockConfigService_Expecter) AddRoute(ctx interface{}, route interface{}) *MockConfigService_AddRoute_Call { | |
| 67 | + | return &MockConfigService_AddRoute_Call{Call: _e.mock.On("AddRoute", ctx, route)} | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *MockConfigService_AddRoute_Call) Run(run func(ctx context.Context, route domain.Route)) *MockConfigService_AddRoute_Call { | |
| 71 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 72 | + | var arg0 context.Context | |
| 73 | + | if args[0] != nil { | |
| 74 | + | arg0 = args[0].(context.Context) | |
| 75 | + | } | |
| 76 | + | var arg1 domain.Route | |
| 77 | + | if args[1] != nil { | |
| 78 | + | arg1 = args[1].(domain.Route) | |
| 79 | + | } | |
| 80 | + | run( | |
| 81 | + | arg0, | |
| 82 | + | arg1, | |
| 83 | + | ) | |
| 84 | + | }) | |
| 85 | + | return _c | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func (_c *MockConfigService_AddRoute_Call) Return(err error) *MockConfigService_AddRoute_Call { | |
| 89 | + | _c.Call.Return(err) | |
| 90 | + | return _c | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Context, route domain.Route) error) *MockConfigService_AddRoute_Call { | |
| 94 | + | _c.Call.Return(run) | |
| 95 | + | return _c | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // GetDataDir provides a mock function for the type MockConfigService | |
| 99 | + | func (_mock *MockConfigService) GetDataDir() string { | |
| 100 | + | ret := _mock.Called() | |
| 101 | + | ||
| 102 | + | if len(ret) == 0 { | |
| 103 | + | panic("no return value specified for GetDataDir") | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | var r0 string | |
| 107 | + | if returnFunc, ok := ret.Get(0).(func() string); ok { | |
| 108 | + | r0 = returnFunc() | |
| 109 | + | } else { | |
| 110 | + | r0 = ret.Get(0).(string) | |
| 111 | + | } | |
| 112 | + | return r0 | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // MockConfigService_GetDataDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDataDir' | |
| 116 | + | type MockConfigService_GetDataDir_Call struct { | |
| 117 | + | *mock.Call | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // GetDataDir is a helper method to define mock.On call | |
| 121 | + | func (_e *MockConfigService_Expecter) GetDataDir() *MockConfigService_GetDataDir_Call { | |
| 122 | + | return &MockConfigService_GetDataDir_Call{Call: _e.mock.On("GetDataDir")} | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func (_c *MockConfigService_GetDataDir_Call) Run(run func()) *MockConfigService_GetDataDir_Call { | |
| 126 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 127 | + | run() | |
| 128 | + | }) | |
| 129 | + | return _c | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (_c *MockConfigService_GetDataDir_Call) Return(s string) *MockConfigService_GetDataDir_Call { | |
| 133 | + | _c.Call.Return(s) | |
| 134 | + | return _c | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (_c *MockConfigService_GetDataDir_Call) RunAndReturn(run func() string) *MockConfigService_GetDataDir_Call { | |
| 138 | + | _c.Call.Return(run) | |
| 139 | + | return _c | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // GetNetworkPrefix provides a mock function for the type MockConfigService | |
| 143 | + | func (_mock *MockConfigService) GetNetworkPrefix() string { | |
| 144 | + | ret := _mock.Called() | |
| 145 | + | ||
| 146 | + | if len(ret) == 0 { | |
| 147 | + | panic("no return value specified for GetNetworkPrefix") | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | var r0 string | |
| 151 | + | if returnFunc, ok := ret.Get(0).(func() string); ok { | |
| 152 | + | r0 = returnFunc() | |
| 153 | + | } else { | |
| 154 | + | r0 = ret.Get(0).(string) | |
| 155 | + | } | |
| 156 | + | return r0 | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // MockConfigService_GetNetworkPrefix_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNetworkPrefix' | |
| 160 | + | type MockConfigService_GetNetworkPrefix_Call struct { | |
| 161 | + | *mock.Call | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // GetNetworkPrefix is a helper method to define mock.On call | |
| 165 | + | func (_e *MockConfigService_Expecter) GetNetworkPrefix() *MockConfigService_GetNetworkPrefix_Call { | |
| 166 | + | return &MockConfigService_GetNetworkPrefix_Call{Call: _e.mock.On("GetNetworkPrefix")} | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (_c *MockConfigService_GetNetworkPrefix_Call) Run(run func()) *MockConfigService_GetNetworkPrefix_Call { | |
| 170 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 171 | + | run() | |
| 172 | + | }) | |
| 173 | + | return _c | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | func (_c *MockConfigService_GetNetworkPrefix_Call) Return(s string) *MockConfigService_GetNetworkPrefix_Call { | |
| 177 | + | _c.Call.Return(s) | |
| 178 | + | return _c | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | func (_c *MockConfigService_GetNetworkPrefix_Call) RunAndReturn(run func() string) *MockConfigService_GetNetworkPrefix_Call { | |
| 182 | + | _c.Call.Return(run) | |
| 183 | + | return _c | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // GetRegistryDomain provides a mock function for the type MockConfigService | |
| 187 | + | func (_mock *MockConfigService) GetRegistryDomain() string { | |
| 188 | + | ret := _mock.Called() | |
| 189 | + | ||
| 190 | + | if len(ret) == 0 { | |
| 191 | + | panic("no return value specified for GetRegistryDomain") | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | var r0 string | |
| 195 | + | if returnFunc, ok := ret.Get(0).(func() string); ok { | |
| 196 | + | r0 = returnFunc() | |
| 197 | + | } else { | |
| 198 | + | r0 = ret.Get(0).(string) | |
| 199 | + | } | |
| 200 | + | return r0 | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // MockConfigService_GetRegistryDomain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRegistryDomain' | |
| 204 | + | type MockConfigService_GetRegistryDomain_Call struct { | |
| 205 | + | *mock.Call | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // GetRegistryDomain is a helper method to define mock.On call | |
| 209 | + | func (_e *MockConfigService_Expecter) GetRegistryDomain() *MockConfigService_GetRegistryDomain_Call { | |
| 210 | + | return &MockConfigService_GetRegistryDomain_Call{Call: _e.mock.On("GetRegistryDomain")} | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | func (_c *MockConfigService_GetRegistryDomain_Call) Run(run func()) *MockConfigService_GetRegistryDomain_Call { | |
| 214 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 215 | + | run() | |
| 216 | + | }) | |
| 217 | + | return _c | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | func (_c *MockConfigService_GetRegistryDomain_Call) Return(s string) *MockConfigService_GetRegistryDomain_Call { | |
| 221 | + | _c.Call.Return(s) | |
| 222 | + | return _c | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (_c *MockConfigService_GetRegistryDomain_Call) RunAndReturn(run func() string) *MockConfigService_GetRegistryDomain_Call { | |
| 226 | + | _c.Call.Return(run) | |
| 227 | + | return _c | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // GetRegistryPort provides a mock function for the type MockConfigService | |
| 231 | + | func (_mock *MockConfigService) GetRegistryPort() int { | |
| 232 | + | ret := _mock.Called() | |
| 233 | + | ||
| 234 | + | if len(ret) == 0 { | |
| 235 | + | panic("no return value specified for GetRegistryPort") | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | var r0 int | |
| 239 | + | if returnFunc, ok := ret.Get(0).(func() int); ok { | |
| 240 | + | r0 = returnFunc() | |
| 241 | + | } else { | |
| 242 | + | r0 = ret.Get(0).(int) | |
| 243 | + | } | |
| 244 | + | return r0 | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // MockConfigService_GetRegistryPort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRegistryPort' | |
| 248 | + | type MockConfigService_GetRegistryPort_Call struct { | |
| 249 | + | *mock.Call | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // GetRegistryPort is a helper method to define mock.On call | |
| 253 | + | func (_e *MockConfigService_Expecter) GetRegistryPort() *MockConfigService_GetRegistryPort_Call { | |
| 254 | + | return &MockConfigService_GetRegistryPort_Call{Call: _e.mock.On("GetRegistryPort")} | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | func (_c *MockConfigService_GetRegistryPort_Call) Run(run func()) *MockConfigService_GetRegistryPort_Call { | |
| 258 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 259 | + | run() | |
| 260 | + | }) | |
| 261 | + | return _c | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | func (_c *MockConfigService_GetRegistryPort_Call) Return(n int) *MockConfigService_GetRegistryPort_Call { | |
| 265 | + | _c.Call.Return(n) | |
| 266 | + | return _c | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | func (_c *MockConfigService_GetRegistryPort_Call) RunAndReturn(run func() int) *MockConfigService_GetRegistryPort_Call { | |
| 270 | + | _c.Call.Return(run) | |
| 271 | + | return _c | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | // GetRoutes provides a mock function for the type MockConfigService | |
| 275 | + | func (_mock *MockConfigService) GetRoutes(ctx context.Context) []domain.Route { | |
| 276 | + | ret := _mock.Called(ctx) | |
| 277 | + | ||
| 278 | + | if len(ret) == 0 { | |
| 279 | + | panic("no return value specified for GetRoutes") | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | var r0 []domain.Route | |
| 283 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.Route); ok { | |
| 284 | + | r0 = returnFunc(ctx) | |
| 285 | + | } else { | |
| 286 | + | if ret.Get(0) != nil { | |
| 287 | + | r0 = ret.Get(0).([]domain.Route) | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | return r0 | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // MockConfigService_GetRoutes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoutes' | |
| 294 | + | type MockConfigService_GetRoutes_Call struct { | |
| 295 | + | *mock.Call | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // GetRoutes is a helper method to define mock.On call | |
| 299 | + | // - ctx context.Context | |
| 300 | + | func (_e *MockConfigService_Expecter) GetRoutes(ctx interface{}) *MockConfigService_GetRoutes_Call { | |
| 301 | + | return &MockConfigService_GetRoutes_Call{Call: _e.mock.On("GetRoutes", ctx)} | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | func (_c *MockConfigService_GetRoutes_Call) Run(run func(ctx context.Context)) *MockConfigService_GetRoutes_Call { | |
| 305 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 306 | + | var arg0 context.Context | |
| 307 | + | if args[0] != nil { | |
| 308 | + | arg0 = args[0].(context.Context) | |
| 309 | + | } | |
| 310 | + | run( | |
| 311 | + | arg0, | |
| 312 | + | ) | |
| 313 | + | }) | |
| 314 | + | return _c | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func (_c *MockConfigService_GetRoutes_Call) Return(routes []domain.Route) *MockConfigService_GetRoutes_Call { | |
| 318 | + | _c.Call.Return(routes) | |
| 319 | + | return _c | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (_c *MockConfigService_GetRoutes_Call) RunAndReturn(run func(ctx context.Context) []domain.Route) *MockConfigService_GetRoutes_Call { | |
| 323 | + | _c.Call.Return(run) | |
| 324 | + | return _c | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // GetServerPort provides a mock function for the type MockConfigService | |
| 328 | + | func (_mock *MockConfigService) GetServerPort() int { | |
| 329 | + | ret := _mock.Called() | |
| 330 | + | ||
| 331 | + | if len(ret) == 0 { | |
| 332 | + | panic("no return value specified for GetServerPort") | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | var r0 int | |
| 336 | + | if returnFunc, ok := ret.Get(0).(func() int); ok { | |
| 337 | + | r0 = returnFunc() | |
| 338 | + | } else { | |
| 339 | + | r0 = ret.Get(0).(int) | |
| 340 | + | } | |
| 341 | + | return r0 | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // MockConfigService_GetServerPort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServerPort' | |
| 345 | + | type MockConfigService_GetServerPort_Call struct { | |
| 346 | + | *mock.Call | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | // GetServerPort is a helper method to define mock.On call | |
| 350 | + | func (_e *MockConfigService_Expecter) GetServerPort() *MockConfigService_GetServerPort_Call { | |
| 351 | + | return &MockConfigService_GetServerPort_Call{Call: _e.mock.On("GetServerPort")} | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | func (_c *MockConfigService_GetServerPort_Call) Run(run func()) *MockConfigService_GetServerPort_Call { | |
| 355 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 356 | + | run() | |
| 357 | + | }) | |
| 358 | + | return _c | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | func (_c *MockConfigService_GetServerPort_Call) Return(n int) *MockConfigService_GetServerPort_Call { | |
| 362 | + | _c.Call.Return(n) | |
| 363 | + | return _c | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | func (_c *MockConfigService_GetServerPort_Call) RunAndReturn(run func() int) *MockConfigService_GetServerPort_Call { | |
| 367 | + | _c.Call.Return(run) | |
| 368 | + | return _c | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | // IsAutoRouteEnabled provides a mock function for the type MockConfigService | |
| 372 | + | func (_mock *MockConfigService) IsAutoRouteEnabled() bool { | |
| 373 | + | ret := _mock.Called() | |
| 374 | + | ||
| 375 | + | if len(ret) == 0 { | |
| 376 | + | panic("no return value specified for IsAutoRouteEnabled") | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | var r0 bool | |
| 380 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 381 | + | r0 = returnFunc() | |
| 382 | + | } else { | |
| 383 | + | r0 = ret.Get(0).(bool) | |
| 384 | + | } | |
| 385 | + | return r0 | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | // MockConfigService_IsAutoRouteEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAutoRouteEnabled' | |
| 389 | + | type MockConfigService_IsAutoRouteEnabled_Call struct { | |
| 390 | + | *mock.Call | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | // IsAutoRouteEnabled is a helper method to define mock.On call | |
| 394 | + | func (_e *MockConfigService_Expecter) IsAutoRouteEnabled() *MockConfigService_IsAutoRouteEnabled_Call { | |
| 395 | + | return &MockConfigService_IsAutoRouteEnabled_Call{Call: _e.mock.On("IsAutoRouteEnabled")} | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | func (_c *MockConfigService_IsAutoRouteEnabled_Call) Run(run func()) *MockConfigService_IsAutoRouteEnabled_Call { | |
| 399 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 400 | + | run() | |
| 401 | + | }) | |
| 402 | + | return _c | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | func (_c *MockConfigService_IsAutoRouteEnabled_Call) Return(b bool) *MockConfigService_IsAutoRouteEnabled_Call { | |
| 406 | + | _c.Call.Return(b) | |
| 407 | + | return _c | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | func (_c *MockConfigService_IsAutoRouteEnabled_Call) RunAndReturn(run func() bool) *MockConfigService_IsAutoRouteEnabled_Call { | |
| 411 | + | _c.Call.Return(run) | |
| 412 | + | return _c | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | // IsNetworkIsolationEnabled provides a mock function for the type MockConfigService | |
| 416 | + | func (_mock *MockConfigService) IsNetworkIsolationEnabled() bool { | |
| 417 | + | ret := _mock.Called() | |
| 418 | + | ||
| 419 | + | if len(ret) == 0 { | |
| 420 | + | panic("no return value specified for IsNetworkIsolationEnabled") | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | var r0 bool | |
| 424 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 425 | + | r0 = returnFunc() | |
| 426 | + | } else { | |
| 427 | + | r0 = ret.Get(0).(bool) | |
| 428 | + | } | |
| 429 | + | return r0 | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | // MockConfigService_IsNetworkIsolationEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsNetworkIsolationEnabled' | |
| 433 | + | type MockConfigService_IsNetworkIsolationEnabled_Call struct { | |
| 434 | + | *mock.Call | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | // IsNetworkIsolationEnabled is a helper method to define mock.On call | |
| 438 | + | func (_e *MockConfigService_Expecter) IsNetworkIsolationEnabled() *MockConfigService_IsNetworkIsolationEnabled_Call { | |
| 439 | + | return &MockConfigService_IsNetworkIsolationEnabled_Call{Call: _e.mock.On("IsNetworkIsolationEnabled")} | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | func (_c *MockConfigService_IsNetworkIsolationEnabled_Call) Run(run func()) *MockConfigService_IsNetworkIsolationEnabled_Call { | |
| 443 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 444 | + | run() | |
| 445 | + | }) | |
| 446 | + | return _c | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | func (_c *MockConfigService_IsNetworkIsolationEnabled_Call) Return(b bool) *MockConfigService_IsNetworkIsolationEnabled_Call { | |
| 450 | + | _c.Call.Return(b) | |
| 451 | + | return _c | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | func (_c *MockConfigService_IsNetworkIsolationEnabled_Call) RunAndReturn(run func() bool) *MockConfigService_IsNetworkIsolationEnabled_Call { | |
| 455 | + | _c.Call.Return(run) | |
| 456 | + | return _c | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | // Load provides a mock function for the type MockConfigService | |
| 460 | + | func (_mock *MockConfigService) Load(ctx context.Context) error { | |
| 461 | + | ret := _mock.Called(ctx) | |
| 462 | + | ||
| 463 | + | if len(ret) == 0 { | |
| 464 | + | panic("no return value specified for Load") | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | var r0 error | |
| 468 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 469 | + | r0 = returnFunc(ctx) | |
| 470 | + | } else { | |
| 471 | + | r0 = ret.Error(0) | |
| 472 | + | } | |
| 473 | + | return r0 | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | // MockConfigService_Load_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Load' | |
| 477 | + | type MockConfigService_Load_Call struct { | |
| 478 | + | *mock.Call | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | // Load is a helper method to define mock.On call | |
| 482 | + | // - ctx context.Context | |
| 483 | + | func (_e *MockConfigService_Expecter) Load(ctx interface{}) *MockConfigService_Load_Call { | |
| 484 | + | return &MockConfigService_Load_Call{Call: _e.mock.On("Load", ctx)} | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | func (_c *MockConfigService_Load_Call) Run(run func(ctx context.Context)) *MockConfigService_Load_Call { | |
| 488 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 489 | + | var arg0 context.Context | |
| 490 | + | if args[0] != nil { | |
| 491 | + | arg0 = args[0].(context.Context) | |
| 492 | + | } | |
| 493 | + | run( | |
| 494 | + | arg0, | |
| 495 | + | ) | |
| 496 | + | }) | |
| 497 | + | return _c | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | func (_c *MockConfigService_Load_Call) Return(err error) *MockConfigService_Load_Call { | |
| 501 | + | _c.Call.Return(err) | |
| 502 | + | return _c | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | func (_c *MockConfigService_Load_Call) RunAndReturn(run func(ctx context.Context) error) *MockConfigService_Load_Call { | |
| 506 | + | _c.Call.Return(run) | |
| 507 | + | return _c | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | // RemoveRoute provides a mock function for the type MockConfigService | |
| 511 | + | func (_mock *MockConfigService) RemoveRoute(ctx context.Context, domain1 string) error { | |
| 512 | + | ret := _mock.Called(ctx, domain1) | |
| 513 | + | ||
| 514 | + | if len(ret) == 0 { | |
| 515 | + | panic("no return value specified for RemoveRoute") | |
| 516 | + | } | |
| 517 | + | ||
| 518 | + | var r0 error | |
| 519 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 520 | + | r0 = returnFunc(ctx, domain1) | |
| 521 | + | } else { | |
| 522 | + | r0 = ret.Error(0) | |
| 523 | + | } | |
| 524 | + | return r0 | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | // MockConfigService_RemoveRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRoute' | |
| 528 | + | type MockConfigService_RemoveRoute_Call struct { | |
| 529 | + | *mock.Call | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | // RemoveRoute is a helper method to define mock.On call | |
| 533 | + | // - ctx context.Context | |
| 534 | + | // - domain1 string | |
| 535 | + | func (_e *MockConfigService_Expecter) RemoveRoute(ctx interface{}, domain1 interface{}) *MockConfigService_RemoveRoute_Call { | |
| 536 | + | return &MockConfigService_RemoveRoute_Call{Call: _e.mock.On("RemoveRoute", ctx, domain1)} | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | func (_c *MockConfigService_RemoveRoute_Call) Run(run func(ctx context.Context, domain1 string)) *MockConfigService_RemoveRoute_Call { | |
| 540 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 541 | + | var arg0 context.Context | |
| 542 | + | if args[0] != nil { | |
| 543 | + | arg0 = args[0].(context.Context) | |
| 544 | + | } | |
| 545 | + | var arg1 string | |
| 546 | + | if args[1] != nil { | |
| 547 | + | arg1 = args[1].(string) | |
| 548 | + | } | |
| 549 | + | run( | |
| 550 | + | arg0, | |
| 551 | + | arg1, | |
| 552 | + | ) | |
| 553 | + | }) | |
| 554 | + | return _c | |
| 555 | + | } | |
| 556 | + | ||
| 557 | + | func (_c *MockConfigService_RemoveRoute_Call) Return(err error) *MockConfigService_RemoveRoute_Call { | |
| 558 | + | _c.Call.Return(err) | |
| 559 | + | return _c | |
| 560 | + | } | |
| 561 | + | ||
| 562 | + | func (_c *MockConfigService_RemoveRoute_Call) RunAndReturn(run func(ctx context.Context, domain1 string) error) *MockConfigService_RemoveRoute_Call { | |
| 563 | + | _c.Call.Return(run) | |
| 564 | + | return _c | |
| 565 | + | } | |
| 566 | + | ||
| 567 | + | // UpdateRoute provides a mock function for the type MockConfigService | |
| 568 | + | func (_mock *MockConfigService) UpdateRoute(ctx context.Context, route domain.Route) error { | |
| 569 | + | ret := _mock.Called(ctx, route) | |
| 570 | + | ||
| 571 | + | if len(ret) == 0 { | |
| 572 | + | panic("no return value specified for UpdateRoute") | |
| 573 | + | } | |
| 574 | + | ||
| 575 | + | var r0 error | |
| 576 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) error); ok { | |
| 577 | + | r0 = returnFunc(ctx, route) | |
| 578 | + | } else { | |
| 579 | + | r0 = ret.Error(0) | |
| 580 | + | } | |
| 581 | + | return r0 | |
| 582 | + | } | |
| 583 | + | ||
| 584 | + | // MockConfigService_UpdateRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRoute' | |
| 585 | + | type MockConfigService_UpdateRoute_Call struct { | |
| 586 | + | *mock.Call | |
| 587 | + | } | |
| 588 | + | ||
| 589 | + | // UpdateRoute is a helper method to define mock.On call | |
| 590 | + | // - ctx context.Context | |
| 591 | + | // - route domain.Route | |
| 592 | + | func (_e *MockConfigService_Expecter) UpdateRoute(ctx interface{}, route interface{}) *MockConfigService_UpdateRoute_Call { | |
| 593 | + | return &MockConfigService_UpdateRoute_Call{Call: _e.mock.On("UpdateRoute", ctx, route)} | |
| 594 | + | } | |
| 595 | + | ||
| 596 | + | func (_c *MockConfigService_UpdateRoute_Call) Run(run func(ctx context.Context, route domain.Route)) *MockConfigService_UpdateRoute_Call { | |
| 597 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 598 | + | var arg0 context.Context | |
| 599 | + | if args[0] != nil { | |
| 600 | + | arg0 = args[0].(context.Context) | |
| 601 | + | } | |
| 602 | + | var arg1 domain.Route | |
| 603 | + | if args[1] != nil { | |
| 604 | + | arg1 = args[1].(domain.Route) | |
| 605 | + | } | |
| 606 | + | run( | |
| 607 | + | arg0, | |
| 608 | + | arg1, | |
| 609 | + | ) | |
| 610 | + | }) | |
| 611 | + | return _c | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | func (_c *MockConfigService_UpdateRoute_Call) Return(err error) *MockConfigService_UpdateRoute_Call { | |
| 615 | + | _c.Call.Return(err) | |
| 616 | + | return _c | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | func (_c *MockConfigService_UpdateRoute_Call) RunAndReturn(run func(ctx context.Context, route domain.Route) error) *MockConfigService_UpdateRoute_Call { | |
| 620 | + | _c.Call.Return(run) | |
| 621 | + | return _c | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | // Watch provides a mock function for the type MockConfigService | |
| 625 | + | func (_mock *MockConfigService) Watch(ctx context.Context, onChange func()) error { | |
| 626 | + | ret := _mock.Called(ctx, onChange) | |
| 627 | + | ||
| 628 | + | if len(ret) == 0 { | |
| 629 | + | panic("no return value specified for Watch") | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | var r0 error | |
| 633 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, func()) error); ok { | |
| 634 | + | r0 = returnFunc(ctx, onChange) | |
| 635 | + | } else { | |
| 636 | + | r0 = ret.Error(0) | |
| 637 | + | } | |
| 638 | + | return r0 | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | // MockConfigService_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch' | |
| 642 | + | type MockConfigService_Watch_Call struct { | |
| 643 | + | *mock.Call | |
| 644 | + | } | |
| 645 | + | ||
| 646 | + | // Watch is a helper method to define mock.On call | |
| 647 | + | // - ctx context.Context | |
| 648 | + | // - onChange func() | |
| 649 | + | func (_e *MockConfigService_Expecter) Watch(ctx interface{}, onChange interface{}) *MockConfigService_Watch_Call { | |
| 650 | + | return &MockConfigService_Watch_Call{Call: _e.mock.On("Watch", ctx, onChange)} | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | func (_c *MockConfigService_Watch_Call) Run(run func(ctx context.Context, onChange func())) *MockConfigService_Watch_Call { | |
| 654 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 655 | + | var arg0 context.Context | |
| 656 | + | if args[0] != nil { | |
| 657 | + | arg0 = args[0].(context.Context) | |
| 658 | + | } | |
| 659 | + | var arg1 func() | |
| 660 | + | if args[1] != nil { | |
| 661 | + | arg1 = args[1].(func()) | |
| 662 | + | } | |
| 663 | + | run( | |
| 664 | + | arg0, | |
| 665 | + | arg1, | |
| 666 | + | ) | |
| 667 | + | }) | |
| 668 | + | return _c | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | func (_c *MockConfigService_Watch_Call) Return(err error) *MockConfigService_Watch_Call { | |
| 672 | + | _c.Call.Return(err) | |
| 673 | + | return _c | |
| 674 | + | } | |
| 675 | + | ||
| 676 | + | func (_c *MockConfigService_Watch_Call) RunAndReturn(run func(ctx context.Context, onChange func()) error) *MockConfigService_Watch_Call { | |
| 677 | + | _c.Call.Return(run) | |
| 678 | + | return _c | |
| 679 | + | } | |
A internal/boundaries/in/mocks/mock_container_service.go +554 -0
| @@ -0,0 +1,554 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | ||
| 11 | + | mock "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // NewMockContainerService creates a new instance of MockContainerService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 15 | + | // The first argument is typically a *testing.T value. | |
| 16 | + | func NewMockContainerService(t interface { | |
| 17 | + | mock.TestingT | |
| 18 | + | Cleanup(func()) | |
| 19 | + | }) *MockContainerService { | |
| 20 | + | mock := &MockContainerService{} | |
| 21 | + | mock.Mock.Test(t) | |
| 22 | + | ||
| 23 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 24 | + | ||
| 25 | + | return mock | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // MockContainerService is an autogenerated mock type for the ContainerService type | |
| 29 | + | type MockContainerService struct { | |
| 30 | + | mock.Mock | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | type MockContainerService_Expecter struct { | |
| 34 | + | mock *mock.Mock | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (_m *MockContainerService) EXPECT() *MockContainerService_Expecter { | |
| 38 | + | return &MockContainerService_Expecter{mock: &_m.Mock} | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // AutoStart provides a mock function for the type MockContainerService | |
| 42 | + | func (_mock *MockContainerService) AutoStart(ctx context.Context) error { | |
| 43 | + | ret := _mock.Called(ctx) | |
| 44 | + | ||
| 45 | + | if len(ret) == 0 { | |
| 46 | + | panic("no return value specified for AutoStart") | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | var r0 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 51 | + | r0 = returnFunc(ctx) | |
| 52 | + | } else { | |
| 53 | + | r0 = ret.Error(0) | |
| 54 | + | } | |
| 55 | + | return r0 | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // MockContainerService_AutoStart_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AutoStart' | |
| 59 | + | type MockContainerService_AutoStart_Call struct { | |
| 60 | + | *mock.Call | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // AutoStart is a helper method to define mock.On call | |
| 64 | + | // - ctx context.Context | |
| 65 | + | func (_e *MockContainerService_Expecter) AutoStart(ctx interface{}) *MockContainerService_AutoStart_Call { | |
| 66 | + | return &MockContainerService_AutoStart_Call{Call: _e.mock.On("AutoStart", ctx)} | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func (_c *MockContainerService_AutoStart_Call) Run(run func(ctx context.Context)) *MockContainerService_AutoStart_Call { | |
| 70 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 71 | + | var arg0 context.Context | |
| 72 | + | if args[0] != nil { | |
| 73 | + | arg0 = args[0].(context.Context) | |
| 74 | + | } | |
| 75 | + | run( | |
| 76 | + | arg0, | |
| 77 | + | ) | |
| 78 | + | }) | |
| 79 | + | return _c | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (_c *MockContainerService_AutoStart_Call) Return(err error) *MockContainerService_AutoStart_Call { | |
| 83 | + | _c.Call.Return(err) | |
| 84 | + | return _c | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (_c *MockContainerService_AutoStart_Call) RunAndReturn(run func(ctx context.Context) error) *MockContainerService_AutoStart_Call { | |
| 88 | + | _c.Call.Return(run) | |
| 89 | + | return _c | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // Deploy provides a mock function for the type MockContainerService | |
| 93 | + | func (_mock *MockContainerService) Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) { | |
| 94 | + | ret := _mock.Called(ctx, route) | |
| 95 | + | ||
| 96 | + | if len(ret) == 0 { | |
| 97 | + | panic("no return value specified for Deploy") | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | var r0 *domain.Container | |
| 101 | + | var r1 error | |
| 102 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) (*domain.Container, error)); ok { | |
| 103 | + | return returnFunc(ctx, route) | |
| 104 | + | } | |
| 105 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) *domain.Container); ok { | |
| 106 | + | r0 = returnFunc(ctx, route) | |
| 107 | + | } else { | |
| 108 | + | if ret.Get(0) != nil { | |
| 109 | + | r0 = ret.Get(0).(*domain.Container) | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Route) error); ok { | |
| 113 | + | r1 = returnFunc(ctx, route) | |
| 114 | + | } else { | |
| 115 | + | r1 = ret.Error(1) | |
| 116 | + | } | |
| 117 | + | return r0, r1 | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // MockContainerService_Deploy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Deploy' | |
| 121 | + | type MockContainerService_Deploy_Call struct { | |
| 122 | + | *mock.Call | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // Deploy is a helper method to define mock.On call | |
| 126 | + | // - ctx context.Context | |
| 127 | + | // - route domain.Route | |
| 128 | + | func (_e *MockContainerService_Expecter) Deploy(ctx interface{}, route interface{}) *MockContainerService_Deploy_Call { | |
| 129 | + | return &MockContainerService_Deploy_Call{Call: _e.mock.On("Deploy", ctx, route)} | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (_c *MockContainerService_Deploy_Call) Run(run func(ctx context.Context, route domain.Route)) *MockContainerService_Deploy_Call { | |
| 133 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 134 | + | var arg0 context.Context | |
| 135 | + | if args[0] != nil { | |
| 136 | + | arg0 = args[0].(context.Context) | |
| 137 | + | } | |
| 138 | + | var arg1 domain.Route | |
| 139 | + | if args[1] != nil { | |
| 140 | + | arg1 = args[1].(domain.Route) | |
| 141 | + | } | |
| 142 | + | run( | |
| 143 | + | arg0, | |
| 144 | + | arg1, | |
| 145 | + | ) | |
| 146 | + | }) | |
| 147 | + | return _c | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func (_c *MockContainerService_Deploy_Call) Return(container *domain.Container, err error) *MockContainerService_Deploy_Call { | |
| 151 | + | _c.Call.Return(container, err) | |
| 152 | + | return _c | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func (_c *MockContainerService_Deploy_Call) RunAndReturn(run func(ctx context.Context, route domain.Route) (*domain.Container, error)) *MockContainerService_Deploy_Call { | |
| 156 | + | _c.Call.Return(run) | |
| 157 | + | return _c | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | // Get provides a mock function for the type MockContainerService | |
| 161 | + | func (_mock *MockContainerService) Get(ctx context.Context, domain1 string) (*domain.Container, bool) { | |
| 162 | + | ret := _mock.Called(ctx, domain1) | |
| 163 | + | ||
| 164 | + | if len(ret) == 0 { | |
| 165 | + | panic("no return value specified for Get") | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | var r0 *domain.Container | |
| 169 | + | var r1 bool | |
| 170 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.Container, bool)); ok { | |
| 171 | + | return returnFunc(ctx, domain1) | |
| 172 | + | } | |
| 173 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.Container); ok { | |
| 174 | + | r0 = returnFunc(ctx, domain1) | |
| 175 | + | } else { | |
| 176 | + | if ret.Get(0) != nil { | |
| 177 | + | r0 = ret.Get(0).(*domain.Container) | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) bool); ok { | |
| 181 | + | r1 = returnFunc(ctx, domain1) | |
| 182 | + | } else { | |
| 183 | + | r1 = ret.Get(1).(bool) | |
| 184 | + | } | |
| 185 | + | return r0, r1 | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // MockContainerService_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' | |
| 189 | + | type MockContainerService_Get_Call struct { | |
| 190 | + | *mock.Call | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // Get is a helper method to define mock.On call | |
| 194 | + | // - ctx context.Context | |
| 195 | + | // - domain1 string | |
| 196 | + | func (_e *MockContainerService_Expecter) Get(ctx interface{}, domain1 interface{}) *MockContainerService_Get_Call { | |
| 197 | + | return &MockContainerService_Get_Call{Call: _e.mock.On("Get", ctx, domain1)} | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (_c *MockContainerService_Get_Call) Run(run func(ctx context.Context, domain1 string)) *MockContainerService_Get_Call { | |
| 201 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 202 | + | var arg0 context.Context | |
| 203 | + | if args[0] != nil { | |
| 204 | + | arg0 = args[0].(context.Context) | |
| 205 | + | } | |
| 206 | + | var arg1 string | |
| 207 | + | if args[1] != nil { | |
| 208 | + | arg1 = args[1].(string) | |
| 209 | + | } | |
| 210 | + | run( | |
| 211 | + | arg0, | |
| 212 | + | arg1, | |
| 213 | + | ) | |
| 214 | + | }) | |
| 215 | + | return _c | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | func (_c *MockContainerService_Get_Call) Return(container *domain.Container, b bool) *MockContainerService_Get_Call { | |
| 219 | + | _c.Call.Return(container, b) | |
| 220 | + | return _c | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (_c *MockContainerService_Get_Call) RunAndReturn(run func(ctx context.Context, domain1 string) (*domain.Container, bool)) *MockContainerService_Get_Call { | |
| 224 | + | _c.Call.Return(run) | |
| 225 | + | return _c | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // HealthCheck provides a mock function for the type MockContainerService | |
| 229 | + | func (_mock *MockContainerService) HealthCheck(ctx context.Context) map[string]bool { | |
| 230 | + | ret := _mock.Called(ctx) | |
| 231 | + | ||
| 232 | + | if len(ret) == 0 { | |
| 233 | + | panic("no return value specified for HealthCheck") | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | var r0 map[string]bool | |
| 237 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) map[string]bool); ok { | |
| 238 | + | r0 = returnFunc(ctx) | |
| 239 | + | } else { | |
| 240 | + | if ret.Get(0) != nil { | |
| 241 | + | r0 = ret.Get(0).(map[string]bool) | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | return r0 | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // MockContainerService_HealthCheck_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthCheck' | |
| 248 | + | type MockContainerService_HealthCheck_Call struct { | |
| 249 | + | *mock.Call | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // HealthCheck is a helper method to define mock.On call | |
| 253 | + | // - ctx context.Context | |
| 254 | + | func (_e *MockContainerService_Expecter) HealthCheck(ctx interface{}) *MockContainerService_HealthCheck_Call { | |
| 255 | + | return &MockContainerService_HealthCheck_Call{Call: _e.mock.On("HealthCheck", ctx)} | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | func (_c *MockContainerService_HealthCheck_Call) Run(run func(ctx context.Context)) *MockContainerService_HealthCheck_Call { | |
| 259 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 260 | + | var arg0 context.Context | |
| 261 | + | if args[0] != nil { | |
| 262 | + | arg0 = args[0].(context.Context) | |
| 263 | + | } | |
| 264 | + | run( | |
| 265 | + | arg0, | |
| 266 | + | ) | |
| 267 | + | }) | |
| 268 | + | return _c | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func (_c *MockContainerService_HealthCheck_Call) Return(stringToBool map[string]bool) *MockContainerService_HealthCheck_Call { | |
| 272 | + | _c.Call.Return(stringToBool) | |
| 273 | + | return _c | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (_c *MockContainerService_HealthCheck_Call) RunAndReturn(run func(ctx context.Context) map[string]bool) *MockContainerService_HealthCheck_Call { | |
| 277 | + | _c.Call.Return(run) | |
| 278 | + | return _c | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | // List provides a mock function for the type MockContainerService | |
| 282 | + | func (_mock *MockContainerService) List(ctx context.Context) map[string]*domain.Container { | |
| 283 | + | ret := _mock.Called(ctx) | |
| 284 | + | ||
| 285 | + | if len(ret) == 0 { | |
| 286 | + | panic("no return value specified for List") | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | var r0 map[string]*domain.Container | |
| 290 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) map[string]*domain.Container); ok { | |
| 291 | + | r0 = returnFunc(ctx) | |
| 292 | + | } else { | |
| 293 | + | if ret.Get(0) != nil { | |
| 294 | + | r0 = ret.Get(0).(map[string]*domain.Container) | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | return r0 | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | // MockContainerService_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' | |
| 301 | + | type MockContainerService_List_Call struct { | |
| 302 | + | *mock.Call | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // List is a helper method to define mock.On call | |
| 306 | + | // - ctx context.Context | |
| 307 | + | func (_e *MockContainerService_Expecter) List(ctx interface{}) *MockContainerService_List_Call { | |
| 308 | + | return &MockContainerService_List_Call{Call: _e.mock.On("List", ctx)} | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | func (_c *MockContainerService_List_Call) Run(run func(ctx context.Context)) *MockContainerService_List_Call { | |
| 312 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 313 | + | var arg0 context.Context | |
| 314 | + | if args[0] != nil { | |
| 315 | + | arg0 = args[0].(context.Context) | |
| 316 | + | } | |
| 317 | + | run( | |
| 318 | + | arg0, | |
| 319 | + | ) | |
| 320 | + | }) | |
| 321 | + | return _c | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | func (_c *MockContainerService_List_Call) Return(stringToContainer map[string]*domain.Container) *MockContainerService_List_Call { | |
| 325 | + | _c.Call.Return(stringToContainer) | |
| 326 | + | return _c | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | func (_c *MockContainerService_List_Call) RunAndReturn(run func(ctx context.Context) map[string]*domain.Container) *MockContainerService_List_Call { | |
| 330 | + | _c.Call.Return(run) | |
| 331 | + | return _c | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | // Remove provides a mock function for the type MockContainerService | |
| 335 | + | func (_mock *MockContainerService) Remove(ctx context.Context, containerID string, force bool) error { | |
| 336 | + | ret := _mock.Called(ctx, containerID, force) | |
| 337 | + | ||
| 338 | + | if len(ret) == 0 { | |
| 339 | + | panic("no return value specified for Remove") | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | var r0 error | |
| 343 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { | |
| 344 | + | r0 = returnFunc(ctx, containerID, force) | |
| 345 | + | } else { | |
| 346 | + | r0 = ret.Error(0) | |
| 347 | + | } | |
| 348 | + | return r0 | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | // MockContainerService_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |
| 352 | + | type MockContainerService_Remove_Call struct { | |
| 353 | + | *mock.Call | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // Remove is a helper method to define mock.On call | |
| 357 | + | // - ctx context.Context | |
| 358 | + | // - containerID string | |
| 359 | + | // - force bool | |
| 360 | + | func (_e *MockContainerService_Expecter) Remove(ctx interface{}, containerID interface{}, force interface{}) *MockContainerService_Remove_Call { | |
| 361 | + | return &MockContainerService_Remove_Call{Call: _e.mock.On("Remove", ctx, containerID, force)} | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | func (_c *MockContainerService_Remove_Call) Run(run func(ctx context.Context, containerID string, force bool)) *MockContainerService_Remove_Call { | |
| 365 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 366 | + | var arg0 context.Context | |
| 367 | + | if args[0] != nil { | |
| 368 | + | arg0 = args[0].(context.Context) | |
| 369 | + | } | |
| 370 | + | var arg1 string | |
| 371 | + | if args[1] != nil { | |
| 372 | + | arg1 = args[1].(string) | |
| 373 | + | } | |
| 374 | + | var arg2 bool | |
| 375 | + | if args[2] != nil { | |
| 376 | + | arg2 = args[2].(bool) | |
| 377 | + | } | |
| 378 | + | run( | |
| 379 | + | arg0, | |
| 380 | + | arg1, | |
| 381 | + | arg2, | |
| 382 | + | ) | |
| 383 | + | }) | |
| 384 | + | return _c | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | func (_c *MockContainerService_Remove_Call) Return(err error) *MockContainerService_Remove_Call { | |
| 388 | + | _c.Call.Return(err) | |
| 389 | + | return _c | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | func (_c *MockContainerService_Remove_Call) RunAndReturn(run func(ctx context.Context, containerID string, force bool) error) *MockContainerService_Remove_Call { | |
| 393 | + | _c.Call.Return(run) | |
| 394 | + | return _c | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | // Shutdown provides a mock function for the type MockContainerService | |
| 398 | + | func (_mock *MockContainerService) Shutdown(ctx context.Context) error { | |
| 399 | + | ret := _mock.Called(ctx) | |
| 400 | + | ||
| 401 | + | if len(ret) == 0 { | |
| 402 | + | panic("no return value specified for Shutdown") | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | var r0 error | |
| 406 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 407 | + | r0 = returnFunc(ctx) | |
| 408 | + | } else { | |
| 409 | + | r0 = ret.Error(0) | |
| 410 | + | } | |
| 411 | + | return r0 | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | // MockContainerService_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown' | |
| 415 | + | type MockContainerService_Shutdown_Call struct { | |
| 416 | + | *mock.Call | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | // Shutdown is a helper method to define mock.On call | |
| 420 | + | // - ctx context.Context | |
| 421 | + | func (_e *MockContainerService_Expecter) Shutdown(ctx interface{}) *MockContainerService_Shutdown_Call { | |
| 422 | + | return &MockContainerService_Shutdown_Call{Call: _e.mock.On("Shutdown", ctx)} | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | func (_c *MockContainerService_Shutdown_Call) Run(run func(ctx context.Context)) *MockContainerService_Shutdown_Call { | |
| 426 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 427 | + | var arg0 context.Context | |
| 428 | + | if args[0] != nil { | |
| 429 | + | arg0 = args[0].(context.Context) | |
| 430 | + | } | |
| 431 | + | run( | |
| 432 | + | arg0, | |
| 433 | + | ) | |
| 434 | + | }) | |
| 435 | + | return _c | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | func (_c *MockContainerService_Shutdown_Call) Return(err error) *MockContainerService_Shutdown_Call { | |
| 439 | + | _c.Call.Return(err) | |
| 440 | + | return _c | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | func (_c *MockContainerService_Shutdown_Call) RunAndReturn(run func(ctx context.Context) error) *MockContainerService_Shutdown_Call { | |
| 444 | + | _c.Call.Return(run) | |
| 445 | + | return _c | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // Stop provides a mock function for the type MockContainerService | |
| 449 | + | func (_mock *MockContainerService) Stop(ctx context.Context, containerID string) error { | |
| 450 | + | ret := _mock.Called(ctx, containerID) | |
| 451 | + | ||
| 452 | + | if len(ret) == 0 { | |
| 453 | + | panic("no return value specified for Stop") | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | var r0 error | |
| 457 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 458 | + | r0 = returnFunc(ctx, containerID) | |
| 459 | + | } else { | |
| 460 | + | r0 = ret.Error(0) | |
| 461 | + | } | |
| 462 | + | return r0 | |
| 463 | + | } | |
| 464 | + | ||
| 465 | + | // MockContainerService_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop' | |
| 466 | + | type MockContainerService_Stop_Call struct { | |
| 467 | + | *mock.Call | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | // Stop is a helper method to define mock.On call | |
| 471 | + | // - ctx context.Context | |
| 472 | + | // - containerID string | |
| 473 | + | func (_e *MockContainerService_Expecter) Stop(ctx interface{}, containerID interface{}) *MockContainerService_Stop_Call { | |
| 474 | + | return &MockContainerService_Stop_Call{Call: _e.mock.On("Stop", ctx, containerID)} | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | func (_c *MockContainerService_Stop_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerService_Stop_Call { | |
| 478 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 479 | + | var arg0 context.Context | |
| 480 | + | if args[0] != nil { | |
| 481 | + | arg0 = args[0].(context.Context) | |
| 482 | + | } | |
| 483 | + | var arg1 string | |
| 484 | + | if args[1] != nil { | |
| 485 | + | arg1 = args[1].(string) | |
| 486 | + | } | |
| 487 | + | run( | |
| 488 | + | arg0, | |
| 489 | + | arg1, | |
| 490 | + | ) | |
| 491 | + | }) | |
| 492 | + | return _c | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | func (_c *MockContainerService_Stop_Call) Return(err error) *MockContainerService_Stop_Call { | |
| 496 | + | _c.Call.Return(err) | |
| 497 | + | return _c | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | func (_c *MockContainerService_Stop_Call) RunAndReturn(run func(ctx context.Context, containerID string) error) *MockContainerService_Stop_Call { | |
| 501 | + | _c.Call.Return(run) | |
| 502 | + | return _c | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | // SyncContainers provides a mock function for the type MockContainerService | |
| 506 | + | func (_mock *MockContainerService) SyncContainers(ctx context.Context) error { | |
| 507 | + | ret := _mock.Called(ctx) | |
| 508 | + | ||
| 509 | + | if len(ret) == 0 { | |
| 510 | + | panic("no return value specified for SyncContainers") | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | var r0 error | |
| 514 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 515 | + | r0 = returnFunc(ctx) | |
| 516 | + | } else { | |
| 517 | + | r0 = ret.Error(0) | |
| 518 | + | } | |
| 519 | + | return r0 | |
| 520 | + | } | |
| 521 | + | ||
| 522 | + | // MockContainerService_SyncContainers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SyncContainers' | |
| 523 | + | type MockContainerService_SyncContainers_Call struct { | |
| 524 | + | *mock.Call | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | // SyncContainers is a helper method to define mock.On call | |
| 528 | + | // - ctx context.Context | |
| 529 | + | func (_e *MockContainerService_Expecter) SyncContainers(ctx interface{}) *MockContainerService_SyncContainers_Call { | |
| 530 | + | return &MockContainerService_SyncContainers_Call{Call: _e.mock.On("SyncContainers", ctx)} | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | func (_c *MockContainerService_SyncContainers_Call) Run(run func(ctx context.Context)) *MockContainerService_SyncContainers_Call { | |
| 534 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 535 | + | var arg0 context.Context | |
| 536 | + | if args[0] != nil { | |
| 537 | + | arg0 = args[0].(context.Context) | |
| 538 | + | } | |
| 539 | + | run( | |
| 540 | + | arg0, | |
| 541 | + | ) | |
| 542 | + | }) | |
| 543 | + | return _c | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | func (_c *MockContainerService_SyncContainers_Call) Return(err error) *MockContainerService_SyncContainers_Call { | |
| 547 | + | _c.Call.Return(err) | |
| 548 | + | return _c | |
| 549 | + | } | |
| 550 | + | ||
| 551 | + | func (_c *MockContainerService_SyncContainers_Call) RunAndReturn(run func(ctx context.Context) error) *MockContainerService_SyncContainers_Call { | |
| 552 | + | _c.Call.Return(run) | |
| 553 | + | return _c | |
| 554 | + | } | |
A internal/boundaries/in/mocks/mock_proxy_service.go +325 -0
| @@ -0,0 +1,325 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | "net/http" | |
| 11 | + | ||
| 12 | + | mock "github.com/stretchr/testify/mock" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // NewMockProxyService creates a new instance of MockProxyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 16 | + | // The first argument is typically a *testing.T value. | |
| 17 | + | func NewMockProxyService(t interface { | |
| 18 | + | mock.TestingT | |
| 19 | + | Cleanup(func()) | |
| 20 | + | }) *MockProxyService { | |
| 21 | + | mock := &MockProxyService{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 25 | + | ||
| 26 | + | return mock | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // MockProxyService is an autogenerated mock type for the ProxyService type | |
| 30 | + | type MockProxyService struct { | |
| 31 | + | mock.Mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | type MockProxyService_Expecter struct { | |
| 35 | + | mock *mock.Mock | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (_m *MockProxyService) EXPECT() *MockProxyService_Expecter { | |
| 39 | + | return &MockProxyService_Expecter{mock: &_m.Mock} | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // GetTarget provides a mock function for the type MockProxyService | |
| 43 | + | func (_mock *MockProxyService) GetTarget(ctx context.Context, domain1 string) (*domain.ProxyTarget, error) { | |
| 44 | + | ret := _mock.Called(ctx, domain1) | |
| 45 | + | ||
| 46 | + | if len(ret) == 0 { | |
| 47 | + | panic("no return value specified for GetTarget") | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | var r0 *domain.ProxyTarget | |
| 51 | + | var r1 error | |
| 52 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.ProxyTarget, error)); ok { | |
| 53 | + | return returnFunc(ctx, domain1) | |
| 54 | + | } | |
| 55 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.ProxyTarget); ok { | |
| 56 | + | r0 = returnFunc(ctx, domain1) | |
| 57 | + | } else { | |
| 58 | + | if ret.Get(0) != nil { | |
| 59 | + | r0 = ret.Get(0).(*domain.ProxyTarget) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 63 | + | r1 = returnFunc(ctx, domain1) | |
| 64 | + | } else { | |
| 65 | + | r1 = ret.Error(1) | |
| 66 | + | } | |
| 67 | + | return r0, r1 | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // MockProxyService_GetTarget_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTarget' | |
| 71 | + | type MockProxyService_GetTarget_Call struct { | |
| 72 | + | *mock.Call | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // GetTarget is a helper method to define mock.On call | |
| 76 | + | // - ctx context.Context | |
| 77 | + | // - domain1 string | |
| 78 | + | func (_e *MockProxyService_Expecter) GetTarget(ctx interface{}, domain1 interface{}) *MockProxyService_GetTarget_Call { | |
| 79 | + | return &MockProxyService_GetTarget_Call{Call: _e.mock.On("GetTarget", ctx, domain1)} | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (_c *MockProxyService_GetTarget_Call) Run(run func(ctx context.Context, domain1 string)) *MockProxyService_GetTarget_Call { | |
| 83 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 84 | + | var arg0 context.Context | |
| 85 | + | if args[0] != nil { | |
| 86 | + | arg0 = args[0].(context.Context) | |
| 87 | + | } | |
| 88 | + | var arg1 string | |
| 89 | + | if args[1] != nil { | |
| 90 | + | arg1 = args[1].(string) | |
| 91 | + | } | |
| 92 | + | run( | |
| 93 | + | arg0, | |
| 94 | + | arg1, | |
| 95 | + | ) | |
| 96 | + | }) | |
| 97 | + | return _c | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func (_c *MockProxyService_GetTarget_Call) Return(proxyTarget *domain.ProxyTarget, err error) *MockProxyService_GetTarget_Call { | |
| 101 | + | _c.Call.Return(proxyTarget, err) | |
| 102 | + | return _c | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (_c *MockProxyService_GetTarget_Call) RunAndReturn(run func(ctx context.Context, domain1 string) (*domain.ProxyTarget, error)) *MockProxyService_GetTarget_Call { | |
| 106 | + | _c.Call.Return(run) | |
| 107 | + | return _c | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // RefreshTargets provides a mock function for the type MockProxyService | |
| 111 | + | func (_mock *MockProxyService) RefreshTargets(ctx context.Context) error { | |
| 112 | + | ret := _mock.Called(ctx) | |
| 113 | + | ||
| 114 | + | if len(ret) == 0 { | |
| 115 | + | panic("no return value specified for RefreshTargets") | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | var r0 error | |
| 119 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 120 | + | r0 = returnFunc(ctx) | |
| 121 | + | } else { | |
| 122 | + | r0 = ret.Error(0) | |
| 123 | + | } | |
| 124 | + | return r0 | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // MockProxyService_RefreshTargets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RefreshTargets' | |
| 128 | + | type MockProxyService_RefreshTargets_Call struct { | |
| 129 | + | *mock.Call | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // RefreshTargets is a helper method to define mock.On call | |
| 133 | + | // - ctx context.Context | |
| 134 | + | func (_e *MockProxyService_Expecter) RefreshTargets(ctx interface{}) *MockProxyService_RefreshTargets_Call { | |
| 135 | + | return &MockProxyService_RefreshTargets_Call{Call: _e.mock.On("RefreshTargets", ctx)} | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func (_c *MockProxyService_RefreshTargets_Call) Run(run func(ctx context.Context)) *MockProxyService_RefreshTargets_Call { | |
| 139 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 140 | + | var arg0 context.Context | |
| 141 | + | if args[0] != nil { | |
| 142 | + | arg0 = args[0].(context.Context) | |
| 143 | + | } | |
| 144 | + | run( | |
| 145 | + | arg0, | |
| 146 | + | ) | |
| 147 | + | }) | |
| 148 | + | return _c | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func (_c *MockProxyService_RefreshTargets_Call) Return(err error) *MockProxyService_RefreshTargets_Call { | |
| 152 | + | _c.Call.Return(err) | |
| 153 | + | return _c | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (_c *MockProxyService_RefreshTargets_Call) RunAndReturn(run func(ctx context.Context) error) *MockProxyService_RefreshTargets_Call { | |
| 157 | + | _c.Call.Return(run) | |
| 158 | + | return _c | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // RegisterTarget provides a mock function for the type MockProxyService | |
| 162 | + | func (_mock *MockProxyService) RegisterTarget(ctx context.Context, domain1 string, target *domain.ProxyTarget) error { | |
| 163 | + | ret := _mock.Called(ctx, domain1, target) | |
| 164 | + | ||
| 165 | + | if len(ret) == 0 { | |
| 166 | + | panic("no return value specified for RegisterTarget") | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | var r0 error | |
| 170 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, *domain.ProxyTarget) error); ok { | |
| 171 | + | r0 = returnFunc(ctx, domain1, target) | |
| 172 | + | } else { | |
| 173 | + | r0 = ret.Error(0) | |
| 174 | + | } | |
| 175 | + | return r0 | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // MockProxyService_RegisterTarget_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterTarget' | |
| 179 | + | type MockProxyService_RegisterTarget_Call struct { | |
| 180 | + | *mock.Call | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // RegisterTarget is a helper method to define mock.On call | |
| 184 | + | // - ctx context.Context | |
| 185 | + | // - domain1 string | |
| 186 | + | // - target *domain.ProxyTarget | |
| 187 | + | func (_e *MockProxyService_Expecter) RegisterTarget(ctx interface{}, domain1 interface{}, target interface{}) *MockProxyService_RegisterTarget_Call { | |
| 188 | + | return &MockProxyService_RegisterTarget_Call{Call: _e.mock.On("RegisterTarget", ctx, domain1, target)} | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func (_c *MockProxyService_RegisterTarget_Call) Run(run func(ctx context.Context, domain1 string, target *domain.ProxyTarget)) *MockProxyService_RegisterTarget_Call { | |
| 192 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 193 | + | var arg0 context.Context | |
| 194 | + | if args[0] != nil { | |
| 195 | + | arg0 = args[0].(context.Context) | |
| 196 | + | } | |
| 197 | + | var arg1 string | |
| 198 | + | if args[1] != nil { | |
| 199 | + | arg1 = args[1].(string) | |
| 200 | + | } | |
| 201 | + | var arg2 *domain.ProxyTarget | |
| 202 | + | if args[2] != nil { | |
| 203 | + | arg2 = args[2].(*domain.ProxyTarget) | |
| 204 | + | } | |
| 205 | + | run( | |
| 206 | + | arg0, | |
| 207 | + | arg1, | |
| 208 | + | arg2, | |
| 209 | + | ) | |
| 210 | + | }) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *MockProxyService_RegisterTarget_Call) Return(err error) *MockProxyService_RegisterTarget_Call { | |
| 215 | + | _c.Call.Return(err) | |
| 216 | + | return _c | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | func (_c *MockProxyService_RegisterTarget_Call) RunAndReturn(run func(ctx context.Context, domain1 string, target *domain.ProxyTarget) error) *MockProxyService_RegisterTarget_Call { | |
| 220 | + | _c.Call.Return(run) | |
| 221 | + | return _c | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // ServeHTTP provides a mock function for the type MockProxyService | |
| 225 | + | func (_mock *MockProxyService) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 226 | + | _mock.Called(w, r) | |
| 227 | + | return | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // MockProxyService_ServeHTTP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServeHTTP' | |
| 231 | + | type MockProxyService_ServeHTTP_Call struct { | |
| 232 | + | *mock.Call | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // ServeHTTP is a helper method to define mock.On call | |
| 236 | + | // - w http.ResponseWriter | |
| 237 | + | // - r *http.Request | |
| 238 | + | func (_e *MockProxyService_Expecter) ServeHTTP(w interface{}, r interface{}) *MockProxyService_ServeHTTP_Call { | |
| 239 | + | return &MockProxyService_ServeHTTP_Call{Call: _e.mock.On("ServeHTTP", w, r)} | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func (_c *MockProxyService_ServeHTTP_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockProxyService_ServeHTTP_Call { | |
| 243 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 244 | + | var arg0 http.ResponseWriter | |
| 245 | + | if args[0] != nil { | |
| 246 | + | arg0 = args[0].(http.ResponseWriter) | |
| 247 | + | } | |
| 248 | + | var arg1 *http.Request | |
| 249 | + | if args[1] != nil { | |
| 250 | + | arg1 = args[1].(*http.Request) | |
| 251 | + | } | |
| 252 | + | run( | |
| 253 | + | arg0, | |
| 254 | + | arg1, | |
| 255 | + | ) | |
| 256 | + | }) | |
| 257 | + | return _c | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func (_c *MockProxyService_ServeHTTP_Call) Return() *MockProxyService_ServeHTTP_Call { | |
| 261 | + | _c.Call.Return() | |
| 262 | + | return _c | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (_c *MockProxyService_ServeHTTP_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockProxyService_ServeHTTP_Call { | |
| 266 | + | _c.Run(run) | |
| 267 | + | return _c | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // UnregisterTarget provides a mock function for the type MockProxyService | |
| 271 | + | func (_mock *MockProxyService) UnregisterTarget(ctx context.Context, domain1 string) error { | |
| 272 | + | ret := _mock.Called(ctx, domain1) | |
| 273 | + | ||
| 274 | + | if len(ret) == 0 { | |
| 275 | + | panic("no return value specified for UnregisterTarget") | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | var r0 error | |
| 279 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 280 | + | r0 = returnFunc(ctx, domain1) | |
| 281 | + | } else { | |
| 282 | + | r0 = ret.Error(0) | |
| 283 | + | } | |
| 284 | + | return r0 | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // MockProxyService_UnregisterTarget_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UnregisterTarget' | |
| 288 | + | type MockProxyService_UnregisterTarget_Call struct { | |
| 289 | + | *mock.Call | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | // UnregisterTarget is a helper method to define mock.On call | |
| 293 | + | // - ctx context.Context | |
| 294 | + | // - domain1 string | |
| 295 | + | func (_e *MockProxyService_Expecter) UnregisterTarget(ctx interface{}, domain1 interface{}) *MockProxyService_UnregisterTarget_Call { | |
| 296 | + | return &MockProxyService_UnregisterTarget_Call{Call: _e.mock.On("UnregisterTarget", ctx, domain1)} | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func (_c *MockProxyService_UnregisterTarget_Call) Run(run func(ctx context.Context, domain1 string)) *MockProxyService_UnregisterTarget_Call { | |
| 300 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 301 | + | var arg0 context.Context | |
| 302 | + | if args[0] != nil { | |
| 303 | + | arg0 = args[0].(context.Context) | |
| 304 | + | } | |
| 305 | + | var arg1 string | |
| 306 | + | if args[1] != nil { | |
| 307 | + | arg1 = args[1].(string) | |
| 308 | + | } | |
| 309 | + | run( | |
| 310 | + | arg0, | |
| 311 | + | arg1, | |
| 312 | + | ) | |
| 313 | + | }) | |
| 314 | + | return _c | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func (_c *MockProxyService_UnregisterTarget_Call) Return(err error) *MockProxyService_UnregisterTarget_Call { | |
| 318 | + | _c.Call.Return(err) | |
| 319 | + | return _c | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (_c *MockProxyService_UnregisterTarget_Call) RunAndReturn(run func(ctx context.Context, domain1 string) error) *MockProxyService_UnregisterTarget_Call { | |
| 323 | + | _c.Call.Return(run) | |
| 324 | + | return _c | |
| 325 | + | } | |
A internal/boundaries/in/mocks/mock_registry_service.go +744 -0
| @@ -0,0 +1,744 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | "io" | |
| 11 | + | ||
| 12 | + | mock "github.com/stretchr/testify/mock" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // NewMockRegistryService creates a new instance of MockRegistryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 16 | + | // The first argument is typically a *testing.T value. | |
| 17 | + | func NewMockRegistryService(t interface { | |
| 18 | + | mock.TestingT | |
| 19 | + | Cleanup(func()) | |
| 20 | + | }) *MockRegistryService { | |
| 21 | + | mock := &MockRegistryService{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 25 | + | ||
| 26 | + | return mock | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // MockRegistryService is an autogenerated mock type for the RegistryService type | |
| 30 | + | type MockRegistryService struct { | |
| 31 | + | mock.Mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | type MockRegistryService_Expecter struct { | |
| 35 | + | mock *mock.Mock | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (_m *MockRegistryService) EXPECT() *MockRegistryService_Expecter { | |
| 39 | + | return &MockRegistryService_Expecter{mock: &_m.Mock} | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // BlobExists provides a mock function for the type MockRegistryService | |
| 43 | + | func (_mock *MockRegistryService) BlobExists(ctx context.Context, digest string) bool { | |
| 44 | + | ret := _mock.Called(ctx, digest) | |
| 45 | + | ||
| 46 | + | if len(ret) == 0 { | |
| 47 | + | panic("no return value specified for BlobExists") | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | var r0 bool | |
| 51 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok { | |
| 52 | + | r0 = returnFunc(ctx, digest) | |
| 53 | + | } else { | |
| 54 | + | r0 = ret.Get(0).(bool) | |
| 55 | + | } | |
| 56 | + | return r0 | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // MockRegistryService_BlobExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BlobExists' | |
| 60 | + | type MockRegistryService_BlobExists_Call struct { | |
| 61 | + | *mock.Call | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // BlobExists is a helper method to define mock.On call | |
| 65 | + | // - ctx context.Context | |
| 66 | + | // - digest string | |
| 67 | + | func (_e *MockRegistryService_Expecter) BlobExists(ctx interface{}, digest interface{}) *MockRegistryService_BlobExists_Call { | |
| 68 | + | return &MockRegistryService_BlobExists_Call{Call: _e.mock.On("BlobExists", ctx, digest)} | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (_c *MockRegistryService_BlobExists_Call) Run(run func(ctx context.Context, digest string)) *MockRegistryService_BlobExists_Call { | |
| 72 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 73 | + | var arg0 context.Context | |
| 74 | + | if args[0] != nil { | |
| 75 | + | arg0 = args[0].(context.Context) | |
| 76 | + | } | |
| 77 | + | var arg1 string | |
| 78 | + | if args[1] != nil { | |
| 79 | + | arg1 = args[1].(string) | |
| 80 | + | } | |
| 81 | + | run( | |
| 82 | + | arg0, | |
| 83 | + | arg1, | |
| 84 | + | ) | |
| 85 | + | }) | |
| 86 | + | return _c | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (_c *MockRegistryService_BlobExists_Call) Return(b bool) *MockRegistryService_BlobExists_Call { | |
| 90 | + | _c.Call.Return(b) | |
| 91 | + | return _c | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func (_c *MockRegistryService_BlobExists_Call) RunAndReturn(run func(ctx context.Context, digest string) bool) *MockRegistryService_BlobExists_Call { | |
| 95 | + | _c.Call.Return(run) | |
| 96 | + | return _c | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // CancelUpload provides a mock function for the type MockRegistryService | |
| 100 | + | func (_mock *MockRegistryService) CancelUpload(ctx context.Context, uuid string) error { | |
| 101 | + | ret := _mock.Called(ctx, uuid) | |
| 102 | + | ||
| 103 | + | if len(ret) == 0 { | |
| 104 | + | panic("no return value specified for CancelUpload") | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | var r0 error | |
| 108 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 109 | + | r0 = returnFunc(ctx, uuid) | |
| 110 | + | } else { | |
| 111 | + | r0 = ret.Error(0) | |
| 112 | + | } | |
| 113 | + | return r0 | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // MockRegistryService_CancelUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelUpload' | |
| 117 | + | type MockRegistryService_CancelUpload_Call struct { | |
| 118 | + | *mock.Call | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // CancelUpload is a helper method to define mock.On call | |
| 122 | + | // - ctx context.Context | |
| 123 | + | // - uuid string | |
| 124 | + | func (_e *MockRegistryService_Expecter) CancelUpload(ctx interface{}, uuid interface{}) *MockRegistryService_CancelUpload_Call { | |
| 125 | + | return &MockRegistryService_CancelUpload_Call{Call: _e.mock.On("CancelUpload", ctx, uuid)} | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func (_c *MockRegistryService_CancelUpload_Call) Run(run func(ctx context.Context, uuid string)) *MockRegistryService_CancelUpload_Call { | |
| 129 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 130 | + | var arg0 context.Context | |
| 131 | + | if args[0] != nil { | |
| 132 | + | arg0 = args[0].(context.Context) | |
| 133 | + | } | |
| 134 | + | var arg1 string | |
| 135 | + | if args[1] != nil { | |
| 136 | + | arg1 = args[1].(string) | |
| 137 | + | } | |
| 138 | + | run( | |
| 139 | + | arg0, | |
| 140 | + | arg1, | |
| 141 | + | ) | |
| 142 | + | }) | |
| 143 | + | return _c | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | func (_c *MockRegistryService_CancelUpload_Call) Return(err error) *MockRegistryService_CancelUpload_Call { | |
| 147 | + | _c.Call.Return(err) | |
| 148 | + | return _c | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func (_c *MockRegistryService_CancelUpload_Call) RunAndReturn(run func(ctx context.Context, uuid string) error) *MockRegistryService_CancelUpload_Call { | |
| 152 | + | _c.Call.Return(run) | |
| 153 | + | return _c | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // DeleteManifest provides a mock function for the type MockRegistryService | |
| 157 | + | func (_mock *MockRegistryService) DeleteManifest(ctx context.Context, name string, reference string) error { | |
| 158 | + | ret := _mock.Called(ctx, name, reference) | |
| 159 | + | ||
| 160 | + | if len(ret) == 0 { | |
| 161 | + | panic("no return value specified for DeleteManifest") | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | var r0 error | |
| 165 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 166 | + | r0 = returnFunc(ctx, name, reference) | |
| 167 | + | } else { | |
| 168 | + | r0 = ret.Error(0) | |
| 169 | + | } | |
| 170 | + | return r0 | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // MockRegistryService_DeleteManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteManifest' | |
| 174 | + | type MockRegistryService_DeleteManifest_Call struct { | |
| 175 | + | *mock.Call | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // DeleteManifest is a helper method to define mock.On call | |
| 179 | + | // - ctx context.Context | |
| 180 | + | // - name string | |
| 181 | + | // - reference string | |
| 182 | + | func (_e *MockRegistryService_Expecter) DeleteManifest(ctx interface{}, name interface{}, reference interface{}) *MockRegistryService_DeleteManifest_Call { | |
| 183 | + | return &MockRegistryService_DeleteManifest_Call{Call: _e.mock.On("DeleteManifest", ctx, name, reference)} | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | func (_c *MockRegistryService_DeleteManifest_Call) Run(run func(ctx context.Context, name string, reference string)) *MockRegistryService_DeleteManifest_Call { | |
| 187 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 188 | + | var arg0 context.Context | |
| 189 | + | if args[0] != nil { | |
| 190 | + | arg0 = args[0].(context.Context) | |
| 191 | + | } | |
| 192 | + | var arg1 string | |
| 193 | + | if args[1] != nil { | |
| 194 | + | arg1 = args[1].(string) | |
| 195 | + | } | |
| 196 | + | var arg2 string | |
| 197 | + | if args[2] != nil { | |
| 198 | + | arg2 = args[2].(string) | |
| 199 | + | } | |
| 200 | + | run( | |
| 201 | + | arg0, | |
| 202 | + | arg1, | |
| 203 | + | arg2, | |
| 204 | + | ) | |
| 205 | + | }) | |
| 206 | + | return _c | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (_c *MockRegistryService_DeleteManifest_Call) Return(err error) *MockRegistryService_DeleteManifest_Call { | |
| 210 | + | _c.Call.Return(err) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *MockRegistryService_DeleteManifest_Call) RunAndReturn(run func(ctx context.Context, name string, reference string) error) *MockRegistryService_DeleteManifest_Call { | |
| 215 | + | _c.Call.Return(run) | |
| 216 | + | return _c | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // FinishUpload provides a mock function for the type MockRegistryService | |
| 220 | + | func (_mock *MockRegistryService) FinishUpload(ctx context.Context, uuid string, digest string) error { | |
| 221 | + | ret := _mock.Called(ctx, uuid, digest) | |
| 222 | + | ||
| 223 | + | if len(ret) == 0 { | |
| 224 | + | panic("no return value specified for FinishUpload") | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | var r0 error | |
| 228 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 229 | + | r0 = returnFunc(ctx, uuid, digest) | |
| 230 | + | } else { | |
| 231 | + | r0 = ret.Error(0) | |
| 232 | + | } | |
| 233 | + | return r0 | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // MockRegistryService_FinishUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FinishUpload' | |
| 237 | + | type MockRegistryService_FinishUpload_Call struct { | |
| 238 | + | *mock.Call | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | // FinishUpload is a helper method to define mock.On call | |
| 242 | + | // - ctx context.Context | |
| 243 | + | // - uuid string | |
| 244 | + | // - digest string | |
| 245 | + | func (_e *MockRegistryService_Expecter) FinishUpload(ctx interface{}, uuid interface{}, digest interface{}) *MockRegistryService_FinishUpload_Call { | |
| 246 | + | return &MockRegistryService_FinishUpload_Call{Call: _e.mock.On("FinishUpload", ctx, uuid, digest)} | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func (_c *MockRegistryService_FinishUpload_Call) Run(run func(ctx context.Context, uuid string, digest string)) *MockRegistryService_FinishUpload_Call { | |
| 250 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 251 | + | var arg0 context.Context | |
| 252 | + | if args[0] != nil { | |
| 253 | + | arg0 = args[0].(context.Context) | |
| 254 | + | } | |
| 255 | + | var arg1 string | |
| 256 | + | if args[1] != nil { | |
| 257 | + | arg1 = args[1].(string) | |
| 258 | + | } | |
| 259 | + | var arg2 string | |
| 260 | + | if args[2] != nil { | |
| 261 | + | arg2 = args[2].(string) | |
| 262 | + | } | |
| 263 | + | run( | |
| 264 | + | arg0, | |
| 265 | + | arg1, | |
| 266 | + | arg2, | |
| 267 | + | ) | |
| 268 | + | }) | |
| 269 | + | return _c | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | func (_c *MockRegistryService_FinishUpload_Call) Return(err error) *MockRegistryService_FinishUpload_Call { | |
| 273 | + | _c.Call.Return(err) | |
| 274 | + | return _c | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | func (_c *MockRegistryService_FinishUpload_Call) RunAndReturn(run func(ctx context.Context, uuid string, digest string) error) *MockRegistryService_FinishUpload_Call { | |
| 278 | + | _c.Call.Return(run) | |
| 279 | + | return _c | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // GetBlob provides a mock function for the type MockRegistryService | |
| 283 | + | func (_mock *MockRegistryService) GetBlob(ctx context.Context, digest string) (io.ReadCloser, error) { | |
| 284 | + | ret := _mock.Called(ctx, digest) | |
| 285 | + | ||
| 286 | + | if len(ret) == 0 { | |
| 287 | + | panic("no return value specified for GetBlob") | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | var r0 io.ReadCloser | |
| 291 | + | var r1 error | |
| 292 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (io.ReadCloser, error)); ok { | |
| 293 | + | return returnFunc(ctx, digest) | |
| 294 | + | } | |
| 295 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) io.ReadCloser); ok { | |
| 296 | + | r0 = returnFunc(ctx, digest) | |
| 297 | + | } else { | |
| 298 | + | if ret.Get(0) != nil { | |
| 299 | + | r0 = ret.Get(0).(io.ReadCloser) | |
| 300 | + | } | |
| 301 | + | } | |
| 302 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 303 | + | r1 = returnFunc(ctx, digest) | |
| 304 | + | } else { | |
| 305 | + | r1 = ret.Error(1) | |
| 306 | + | } | |
| 307 | + | return r0, r1 | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // MockRegistryService_GetBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlob' | |
| 311 | + | type MockRegistryService_GetBlob_Call struct { | |
| 312 | + | *mock.Call | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // GetBlob is a helper method to define mock.On call | |
| 316 | + | // - ctx context.Context | |
| 317 | + | // - digest string | |
| 318 | + | func (_e *MockRegistryService_Expecter) GetBlob(ctx interface{}, digest interface{}) *MockRegistryService_GetBlob_Call { | |
| 319 | + | return &MockRegistryService_GetBlob_Call{Call: _e.mock.On("GetBlob", ctx, digest)} | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (_c *MockRegistryService_GetBlob_Call) Run(run func(ctx context.Context, digest string)) *MockRegistryService_GetBlob_Call { | |
| 323 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 324 | + | var arg0 context.Context | |
| 325 | + | if args[0] != nil { | |
| 326 | + | arg0 = args[0].(context.Context) | |
| 327 | + | } | |
| 328 | + | var arg1 string | |
| 329 | + | if args[1] != nil { | |
| 330 | + | arg1 = args[1].(string) | |
| 331 | + | } | |
| 332 | + | run( | |
| 333 | + | arg0, | |
| 334 | + | arg1, | |
| 335 | + | ) | |
| 336 | + | }) | |
| 337 | + | return _c | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | func (_c *MockRegistryService_GetBlob_Call) Return(readCloser io.ReadCloser, err error) *MockRegistryService_GetBlob_Call { | |
| 341 | + | _c.Call.Return(readCloser, err) | |
| 342 | + | return _c | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func (_c *MockRegistryService_GetBlob_Call) RunAndReturn(run func(ctx context.Context, digest string) (io.ReadCloser, error)) *MockRegistryService_GetBlob_Call { | |
| 346 | + | _c.Call.Return(run) | |
| 347 | + | return _c | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | // GetManifest provides a mock function for the type MockRegistryService | |
| 351 | + | func (_mock *MockRegistryService) GetManifest(ctx context.Context, name string, reference string) (*domain.Manifest, error) { | |
| 352 | + | ret := _mock.Called(ctx, name, reference) | |
| 353 | + | ||
| 354 | + | if len(ret) == 0 { | |
| 355 | + | panic("no return value specified for GetManifest") | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | var r0 *domain.Manifest | |
| 359 | + | var r1 error | |
| 360 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*domain.Manifest, error)); ok { | |
| 361 | + | return returnFunc(ctx, name, reference) | |
| 362 | + | } | |
| 363 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *domain.Manifest); ok { | |
| 364 | + | r0 = returnFunc(ctx, name, reference) | |
| 365 | + | } else { | |
| 366 | + | if ret.Get(0) != nil { | |
| 367 | + | r0 = ret.Get(0).(*domain.Manifest) | |
| 368 | + | } | |
| 369 | + | } | |
| 370 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { | |
| 371 | + | r1 = returnFunc(ctx, name, reference) | |
| 372 | + | } else { | |
| 373 | + | r1 = ret.Error(1) | |
| 374 | + | } | |
| 375 | + | return r0, r1 | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | // MockRegistryService_GetManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManifest' | |
| 379 | + | type MockRegistryService_GetManifest_Call struct { | |
| 380 | + | *mock.Call | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | // GetManifest is a helper method to define mock.On call | |
| 384 | + | // - ctx context.Context | |
| 385 | + | // - name string | |
| 386 | + | // - reference string | |
| 387 | + | func (_e *MockRegistryService_Expecter) GetManifest(ctx interface{}, name interface{}, reference interface{}) *MockRegistryService_GetManifest_Call { | |
| 388 | + | return &MockRegistryService_GetManifest_Call{Call: _e.mock.On("GetManifest", ctx, name, reference)} | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | func (_c *MockRegistryService_GetManifest_Call) Run(run func(ctx context.Context, name string, reference string)) *MockRegistryService_GetManifest_Call { | |
| 392 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 393 | + | var arg0 context.Context | |
| 394 | + | if args[0] != nil { | |
| 395 | + | arg0 = args[0].(context.Context) | |
| 396 | + | } | |
| 397 | + | var arg1 string | |
| 398 | + | if args[1] != nil { | |
| 399 | + | arg1 = args[1].(string) | |
| 400 | + | } | |
| 401 | + | var arg2 string | |
| 402 | + | if args[2] != nil { | |
| 403 | + | arg2 = args[2].(string) | |
| 404 | + | } | |
| 405 | + | run( | |
| 406 | + | arg0, | |
| 407 | + | arg1, | |
| 408 | + | arg2, | |
| 409 | + | ) | |
| 410 | + | }) | |
| 411 | + | return _c | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | func (_c *MockRegistryService_GetManifest_Call) Return(manifest *domain.Manifest, err error) *MockRegistryService_GetManifest_Call { | |
| 415 | + | _c.Call.Return(manifest, err) | |
| 416 | + | return _c | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | func (_c *MockRegistryService_GetManifest_Call) RunAndReturn(run func(ctx context.Context, name string, reference string) (*domain.Manifest, error)) *MockRegistryService_GetManifest_Call { | |
| 420 | + | _c.Call.Return(run) | |
| 421 | + | return _c | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | // ListRepositories provides a mock function for the type MockRegistryService | |
| 425 | + | func (_mock *MockRegistryService) ListRepositories(ctx context.Context) ([]string, error) { | |
| 426 | + | ret := _mock.Called(ctx) | |
| 427 | + | ||
| 428 | + | if len(ret) == 0 { | |
| 429 | + | panic("no return value specified for ListRepositories") | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | var r0 []string | |
| 433 | + | var r1 error | |
| 434 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { | |
| 435 | + | return returnFunc(ctx) | |
| 436 | + | } | |
| 437 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { | |
| 438 | + | r0 = returnFunc(ctx) | |
| 439 | + | } else { | |
| 440 | + | if ret.Get(0) != nil { | |
| 441 | + | r0 = ret.Get(0).([]string) | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 445 | + | r1 = returnFunc(ctx) | |
| 446 | + | } else { | |
| 447 | + | r1 = ret.Error(1) | |
| 448 | + | } | |
| 449 | + | return r0, r1 | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | // MockRegistryService_ListRepositories_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRepositories' | |
| 453 | + | type MockRegistryService_ListRepositories_Call struct { | |
| 454 | + | *mock.Call | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | // ListRepositories is a helper method to define mock.On call | |
| 458 | + | // - ctx context.Context | |
| 459 | + | func (_e *MockRegistryService_Expecter) ListRepositories(ctx interface{}) *MockRegistryService_ListRepositories_Call { | |
| 460 | + | return &MockRegistryService_ListRepositories_Call{Call: _e.mock.On("ListRepositories", ctx)} | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | func (_c *MockRegistryService_ListRepositories_Call) Run(run func(ctx context.Context)) *MockRegistryService_ListRepositories_Call { | |
| 464 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 465 | + | var arg0 context.Context | |
| 466 | + | if args[0] != nil { | |
| 467 | + | arg0 = args[0].(context.Context) | |
| 468 | + | } | |
| 469 | + | run( | |
| 470 | + | arg0, | |
| 471 | + | ) | |
| 472 | + | }) | |
| 473 | + | return _c | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | func (_c *MockRegistryService_ListRepositories_Call) Return(strings []string, err error) *MockRegistryService_ListRepositories_Call { | |
| 477 | + | _c.Call.Return(strings, err) | |
| 478 | + | return _c | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | func (_c *MockRegistryService_ListRepositories_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *MockRegistryService_ListRepositories_Call { | |
| 482 | + | _c.Call.Return(run) | |
| 483 | + | return _c | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | // ListTags provides a mock function for the type MockRegistryService | |
| 487 | + | func (_mock *MockRegistryService) ListTags(ctx context.Context, name string) ([]string, error) { | |
| 488 | + | ret := _mock.Called(ctx, name) | |
| 489 | + | ||
| 490 | + | if len(ret) == 0 { | |
| 491 | + | panic("no return value specified for ListTags") | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | var r0 []string | |
| 495 | + | var r1 error | |
| 496 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |
| 497 | + | return returnFunc(ctx, name) | |
| 498 | + | } | |
| 499 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |
| 500 | + | r0 = returnFunc(ctx, name) | |
| 501 | + | } else { | |
| 502 | + | if ret.Get(0) != nil { | |
| 503 | + | r0 = ret.Get(0).([]string) | |
| 504 | + | } | |
| 505 | + | } | |
| 506 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 507 | + | r1 = returnFunc(ctx, name) | |
| 508 | + | } else { | |
| 509 | + | r1 = ret.Error(1) | |
| 510 | + | } | |
| 511 | + | return r0, r1 | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | // MockRegistryService_ListTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListTags' | |
| 515 | + | type MockRegistryService_ListTags_Call struct { | |
| 516 | + | *mock.Call | |
| 517 | + | } | |
| 518 | + | ||
| 519 | + | // ListTags is a helper method to define mock.On call | |
| 520 | + | // - ctx context.Context | |
| 521 | + | // - name string | |
| 522 | + | func (_e *MockRegistryService_Expecter) ListTags(ctx interface{}, name interface{}) *MockRegistryService_ListTags_Call { | |
| 523 | + | return &MockRegistryService_ListTags_Call{Call: _e.mock.On("ListTags", ctx, name)} | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | func (_c *MockRegistryService_ListTags_Call) Run(run func(ctx context.Context, name string)) *MockRegistryService_ListTags_Call { | |
| 527 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 528 | + | var arg0 context.Context | |
| 529 | + | if args[0] != nil { | |
| 530 | + | arg0 = args[0].(context.Context) | |
| 531 | + | } | |
| 532 | + | var arg1 string | |
| 533 | + | if args[1] != nil { | |
| 534 | + | arg1 = args[1].(string) | |
| 535 | + | } | |
| 536 | + | run( | |
| 537 | + | arg0, | |
| 538 | + | arg1, | |
| 539 | + | ) | |
| 540 | + | }) | |
| 541 | + | return _c | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | func (_c *MockRegistryService_ListTags_Call) Return(strings []string, err error) *MockRegistryService_ListTags_Call { | |
| 545 | + | _c.Call.Return(strings, err) | |
| 546 | + | return _c | |
| 547 | + | } | |
| 548 | + | ||
| 549 | + | func (_c *MockRegistryService_ListTags_Call) RunAndReturn(run func(ctx context.Context, name string) ([]string, error)) *MockRegistryService_ListTags_Call { | |
| 550 | + | _c.Call.Return(run) | |
| 551 | + | return _c | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | // PutBlob provides a mock function for the type MockRegistryService | |
| 555 | + | func (_mock *MockRegistryService) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error { | |
| 556 | + | ret := _mock.Called(ctx, digest, data, size) | |
| 557 | + | ||
| 558 | + | if len(ret) == 0 { | |
| 559 | + | panic("no return value specified for PutBlob") | |
| 560 | + | } | |
| 561 | + | ||
| 562 | + | var r0 error | |
| 563 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, io.Reader, int64) error); ok { | |
| 564 | + | r0 = returnFunc(ctx, digest, data, size) | |
| 565 | + | } else { | |
| 566 | + | r0 = ret.Error(0) | |
| 567 | + | } | |
| 568 | + | return r0 | |
| 569 | + | } | |
| 570 | + | ||
| 571 | + | // MockRegistryService_PutBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutBlob' | |
| 572 | + | type MockRegistryService_PutBlob_Call struct { | |
| 573 | + | *mock.Call | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | // PutBlob is a helper method to define mock.On call | |
| 577 | + | // - ctx context.Context | |
| 578 | + | // - digest string | |
| 579 | + | // - data io.Reader | |
| 580 | + | // - size int64 | |
| 581 | + | func (_e *MockRegistryService_Expecter) PutBlob(ctx interface{}, digest interface{}, data interface{}, size interface{}) *MockRegistryService_PutBlob_Call { | |
| 582 | + | return &MockRegistryService_PutBlob_Call{Call: _e.mock.On("PutBlob", ctx, digest, data, size)} | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | func (_c *MockRegistryService_PutBlob_Call) Run(run func(ctx context.Context, digest string, data io.Reader, size int64)) *MockRegistryService_PutBlob_Call { | |
| 586 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 587 | + | var arg0 context.Context | |
| 588 | + | if args[0] != nil { | |
| 589 | + | arg0 = args[0].(context.Context) | |
| 590 | + | } | |
| 591 | + | var arg1 string | |
| 592 | + | if args[1] != nil { | |
| 593 | + | arg1 = args[1].(string) | |
| 594 | + | } | |
| 595 | + | var arg2 io.Reader | |
| 596 | + | if args[2] != nil { | |
| 597 | + | arg2 = args[2].(io.Reader) | |
| 598 | + | } | |
| 599 | + | var arg3 int64 | |
| 600 | + | if args[3] != nil { | |
| 601 | + | arg3 = args[3].(int64) | |
| 602 | + | } | |
| 603 | + | run( | |
| 604 | + | arg0, | |
| 605 | + | arg1, | |
| 606 | + | arg2, | |
| 607 | + | arg3, | |
| 608 | + | ) | |
| 609 | + | }) | |
| 610 | + | return _c | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | func (_c *MockRegistryService_PutBlob_Call) Return(err error) *MockRegistryService_PutBlob_Call { | |
| 614 | + | _c.Call.Return(err) | |
| 615 | + | return _c | |
| 616 | + | } | |
| 617 | + | ||
| 618 | + | func (_c *MockRegistryService_PutBlob_Call) RunAndReturn(run func(ctx context.Context, digest string, data io.Reader, size int64) error) *MockRegistryService_PutBlob_Call { | |
| 619 | + | _c.Call.Return(run) | |
| 620 | + | return _c | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | // PutManifest provides a mock function for the type MockRegistryService | |
| 624 | + | func (_mock *MockRegistryService) PutManifest(ctx context.Context, manifest *domain.Manifest) error { | |
| 625 | + | ret := _mock.Called(ctx, manifest) | |
| 626 | + | ||
| 627 | + | if len(ret) == 0 { | |
| 628 | + | panic("no return value specified for PutManifest") | |
| 629 | + | } | |
| 630 | + | ||
| 631 | + | var r0 error | |
| 632 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) error); ok { | |
| 633 | + | r0 = returnFunc(ctx, manifest) | |
| 634 | + | } else { | |
| 635 | + | r0 = ret.Error(0) | |
| 636 | + | } | |
| 637 | + | return r0 | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // MockRegistryService_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest' | |
| 641 | + | type MockRegistryService_PutManifest_Call struct { | |
| 642 | + | *mock.Call | |
| 643 | + | } | |
| 644 | + | ||
| 645 | + | // PutManifest is a helper method to define mock.On call | |
| 646 | + | // - ctx context.Context | |
| 647 | + | // - manifest *domain.Manifest | |
| 648 | + | func (_e *MockRegistryService_Expecter) PutManifest(ctx interface{}, manifest interface{}) *MockRegistryService_PutManifest_Call { | |
| 649 | + | return &MockRegistryService_PutManifest_Call{Call: _e.mock.On("PutManifest", ctx, manifest)} | |
| 650 | + | } | |
| 651 | + | ||
| 652 | + | func (_c *MockRegistryService_PutManifest_Call) Run(run func(ctx context.Context, manifest *domain.Manifest)) *MockRegistryService_PutManifest_Call { | |
| 653 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 654 | + | var arg0 context.Context | |
| 655 | + | if args[0] != nil { | |
| 656 | + | arg0 = args[0].(context.Context) | |
| 657 | + | } | |
| 658 | + | var arg1 *domain.Manifest | |
| 659 | + | if args[1] != nil { | |
| 660 | + | arg1 = args[1].(*domain.Manifest) | |
| 661 | + | } | |
| 662 | + | run( | |
| 663 | + | arg0, | |
| 664 | + | arg1, | |
| 665 | + | ) | |
| 666 | + | }) | |
| 667 | + | return _c | |
| 668 | + | } | |
| 669 | + | ||
| 670 | + | func (_c *MockRegistryService_PutManifest_Call) Return(err error) *MockRegistryService_PutManifest_Call { | |
| 671 | + | _c.Call.Return(err) | |
| 672 | + | return _c | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | func (_c *MockRegistryService_PutManifest_Call) RunAndReturn(run func(ctx context.Context, manifest *domain.Manifest) error) *MockRegistryService_PutManifest_Call { | |
| 676 | + | _c.Call.Return(run) | |
| 677 | + | return _c | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | // StartUpload provides a mock function for the type MockRegistryService | |
| 681 | + | func (_mock *MockRegistryService) StartUpload(ctx context.Context, name string) (string, error) { | |
| 682 | + | ret := _mock.Called(ctx, name) | |
| 683 | + | ||
| 684 | + | if len(ret) == 0 { | |
| 685 | + | panic("no return value specified for StartUpload") | |
| 686 | + | } | |
| 687 | + | ||
| 688 | + | var r0 string | |
| 689 | + | var r1 error | |
| 690 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { | |
| 691 | + | return returnFunc(ctx, name) | |
| 692 | + | } | |
| 693 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { | |
| 694 | + | r0 = returnFunc(ctx, name) | |
| 695 | + | } else { | |
| 696 | + | r0 = ret.Get(0).(string) | |
| 697 | + | } | |
| 698 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 699 | + | r1 = returnFunc(ctx, name) | |
| 700 | + | } else { | |
| 701 | + | r1 = ret.Error(1) | |
| 702 | + | } | |
| 703 | + | return r0, r1 | |
| 704 | + | } | |
| 705 | + | ||
| 706 | + | // MockRegistryService_StartUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartUpload' | |
| 707 | + | type MockRegistryService_StartUpload_Call struct { | |
| 708 | + | *mock.Call | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | // StartUpload is a helper method to define mock.On call | |
| 712 | + | // - ctx context.Context | |
| 713 | + | // - name string | |
| 714 | + | func (_e *MockRegistryService_Expecter) StartUpload(ctx interface{}, name interface{}) *MockRegistryService_StartUpload_Call { | |
| 715 | + | return &MockRegistryService_StartUpload_Call{Call: _e.mock.On("StartUpload", ctx, name)} | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | func (_c *MockRegistryService_StartUpload_Call) Run(run func(ctx context.Context, name string)) *MockRegistryService_StartUpload_Call { | |
| 719 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 720 | + | var arg0 context.Context | |
| 721 | + | if args[0] != nil { | |
| 722 | + | arg0 = args[0].(context.Context) | |
| 723 | + | } | |
| 724 | + | var arg1 string | |
| 725 | + | if args[1] != nil { | |
| 726 | + | arg1 = args[1].(string) | |
| 727 | + | } | |
| 728 | + | run( | |
| 729 | + | arg0, | |
| 730 | + | arg1, | |
| 731 | + | ) | |
| 732 | + | }) | |
| 733 | + | return _c | |
| 734 | + | } | |
| 735 | + | ||
| 736 | + | func (_c *MockRegistryService_StartUpload_Call) Return(s string, err error) *MockRegistryService_StartUpload_Call { | |
| 737 | + | _c.Call.Return(s, err) | |
| 738 | + | return _c | |
| 739 | + | } | |
| 740 | + | ||
| 741 | + | func (_c *MockRegistryService_StartUpload_Call) RunAndReturn(run func(ctx context.Context, name string) (string, error)) *MockRegistryService_StartUpload_Call { | |
| 742 | + | _c.Call.Return(run) | |
| 743 | + | return _c | |
| 744 | + | } | |
A internal/boundaries/in/proxy.go +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | package in | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "net/http" | |
| 6 | + | ||
| 7 | + | "gordon/internal/domain" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // ProxyService defines the contract for reverse proxy operations. | |
| 11 | + | type ProxyService interface { | |
| 12 | + | // ServeHTTP handles incoming HTTP requests and proxies them to the appropriate backend. | |
| 13 | + | ServeHTTP(w http.ResponseWriter, r *http.Request) | |
| 14 | + | ||
| 15 | + | // GetTarget returns the proxy target for a given domain. | |
| 16 | + | GetTarget(ctx context.Context, domain string) (*domain.ProxyTarget, error) | |
| 17 | + | ||
| 18 | + | // RegisterTarget registers a new proxy target for a domain. | |
| 19 | + | RegisterTarget(ctx context.Context, domain string, target *domain.ProxyTarget) error | |
| 20 | + | ||
| 21 | + | // UnregisterTarget removes a proxy target for a domain. | |
| 22 | + | UnregisterTarget(ctx context.Context, domain string) error | |
| 23 | + | ||
| 24 | + | // RefreshTargets refreshes all proxy targets from container state. | |
| 25 | + | RefreshTargets(ctx context.Context) error | |
| 26 | + | } | |
A internal/boundaries/in/registry.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package in | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 6 | + | ||
| 7 | + | "gordon/internal/domain" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // RegistryService defines the contract for container registry operations. | |
| 11 | + | type RegistryService interface { | |
| 12 | + | // Manifest operations | |
| 13 | + | GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error) | |
| 14 | + | PutManifest(ctx context.Context, manifest *domain.Manifest) error | |
| 15 | + | DeleteManifest(ctx context.Context, name, reference string) error | |
| 16 | + | ||
| 17 | + | // Blob operations | |
| 18 | + | GetBlob(ctx context.Context, digest string) (io.ReadCloser, error) | |
| 19 | + | PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error | |
| 20 | + | BlobExists(ctx context.Context, digest string) bool | |
| 21 | + | ||
| 22 | + | // Upload operations | |
| 23 | + | StartUpload(ctx context.Context, name string) (string, error) | |
| 24 | + | FinishUpload(ctx context.Context, uuid, digest string) error | |
| 25 | + | CancelUpload(ctx context.Context, uuid string) error | |
| 26 | + | ||
| 27 | + | // Tag operations | |
| 28 | + | ListTags(ctx context.Context, name string) ([]string, error) | |
| 29 | + | ListRepositories(ctx context.Context) ([]string, error) | |
| 30 | + | } | |
A internal/boundaries/out/envloader.go +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | package out | |
| 2 | + | ||
| 3 | + | import "context" | |
| 4 | + | ||
| 5 | + | // EnvLoader defines the contract for loading environment variables for containers. | |
| 6 | + | type EnvLoader interface { | |
| 7 | + | // LoadEnv loads environment variables for a given domain. | |
| 8 | + | // Returns a slice of "KEY=VALUE" strings. | |
| 9 | + | LoadEnv(ctx context.Context, domain string) ([]string, error) | |
| 10 | + | ||
| 11 | + | // CreateEnvFile creates an empty environment file for a new domain. | |
| 12 | + | CreateEnvFile(ctx context.Context, domain string) error | |
| 13 | + | ||
| 14 | + | // EnvFileExists checks if an environment file exists for a domain. | |
| 15 | + | EnvFileExists(domain string) bool | |
| 16 | + | } | |
A internal/boundaries/out/eventbus.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package out | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "gordon/internal/domain" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | // EventHandler defines the contract for handling events. | |
| 8 | + | type EventHandler interface { | |
| 9 | + | Handle(event domain.Event) error | |
| 10 | + | CanHandle(eventType domain.EventType) bool | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // EventPublisher defines the contract for publishing events. | |
| 14 | + | type EventPublisher interface { | |
| 15 | + | Publish(eventType domain.EventType, payload any) error | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // EventSubscriber defines the contract for subscribing to events. | |
| 19 | + | type EventSubscriber interface { | |
| 20 | + | Subscribe(handler EventHandler) error | |
| 21 | + | Unsubscribe(handler EventHandler) error | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // EventBus combines publishing and subscribing capabilities with lifecycle management. | |
| 25 | + | type EventBus interface { | |
| 26 | + | EventPublisher | |
| 27 | + | EventSubscriber | |
| 28 | + | Start() error | |
| 29 | + | Stop() error | |
| 30 | + | } | |
A internal/boundaries/out/mocks/mock_blob_storage.go +627 -0
| @@ -0,0 +1,627 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "io" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockBlobStorage creates a new instance of MockBlobStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockBlobStorage(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockBlobStorage { | |
| 19 | + | mock := &MockBlobStorage{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockBlobStorage is an autogenerated mock type for the BlobStorage type | |
| 28 | + | type MockBlobStorage struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockBlobStorage_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockBlobStorage) EXPECT() *MockBlobStorage_Expecter { | |
| 37 | + | return &MockBlobStorage_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // AppendBlobChunk provides a mock function for the type MockBlobStorage | |
| 41 | + | func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, chunk []byte) (int64, error) { | |
| 42 | + | ret := _mock.Called(name, uuid, chunk) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for AppendBlobChunk") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 int64 | |
| 49 | + | var r1 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(string, string, []byte) (int64, error)); ok { | |
| 51 | + | return returnFunc(name, uuid, chunk) | |
| 52 | + | } | |
| 53 | + | if returnFunc, ok := ret.Get(0).(func(string, string, []byte) int64); ok { | |
| 54 | + | r0 = returnFunc(name, uuid, chunk) | |
| 55 | + | } else { | |
| 56 | + | r0 = ret.Get(0).(int64) | |
| 57 | + | } | |
| 58 | + | if returnFunc, ok := ret.Get(1).(func(string, string, []byte) error); ok { | |
| 59 | + | r1 = returnFunc(name, uuid, chunk) | |
| 60 | + | } else { | |
| 61 | + | r1 = ret.Error(1) | |
| 62 | + | } | |
| 63 | + | return r0, r1 | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // MockBlobStorage_AppendBlobChunk_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendBlobChunk' | |
| 67 | + | type MockBlobStorage_AppendBlobChunk_Call struct { | |
| 68 | + | *mock.Call | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // AppendBlobChunk is a helper method to define mock.On call | |
| 72 | + | // - name string | |
| 73 | + | // - uuid string | |
| 74 | + | // - chunk []byte | |
| 75 | + | func (_e *MockBlobStorage_Expecter) AppendBlobChunk(name interface{}, uuid interface{}, chunk interface{}) *MockBlobStorage_AppendBlobChunk_Call { | |
| 76 | + | return &MockBlobStorage_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", name, uuid, chunk)} | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid string, chunk []byte)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 80 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 81 | + | var arg0 string | |
| 82 | + | if args[0] != nil { | |
| 83 | + | arg0 = args[0].(string) | |
| 84 | + | } | |
| 85 | + | var arg1 string | |
| 86 | + | if args[1] != nil { | |
| 87 | + | arg1 = args[1].(string) | |
| 88 | + | } | |
| 89 | + | var arg2 []byte | |
| 90 | + | if args[2] != nil { | |
| 91 | + | arg2 = args[2].([]byte) | |
| 92 | + | } | |
| 93 | + | run( | |
| 94 | + | arg0, | |
| 95 | + | arg1, | |
| 96 | + | arg2, | |
| 97 | + | ) | |
| 98 | + | }) | |
| 99 | + | return _c | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) Return(n int64, err error) *MockBlobStorage_AppendBlobChunk_Call { | |
| 103 | + | _c.Call.Return(n, err) | |
| 104 | + | return _c | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) RunAndReturn(run func(name string, uuid string, chunk []byte) (int64, error)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 108 | + | _c.Call.Return(run) | |
| 109 | + | return _c | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // BlobExists provides a mock function for the type MockBlobStorage | |
| 113 | + | func (_mock *MockBlobStorage) BlobExists(digest string) bool { | |
| 114 | + | ret := _mock.Called(digest) | |
| 115 | + | ||
| 116 | + | if len(ret) == 0 { | |
| 117 | + | panic("no return value specified for BlobExists") | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | var r0 bool | |
| 121 | + | if returnFunc, ok := ret.Get(0).(func(string) bool); ok { | |
| 122 | + | r0 = returnFunc(digest) | |
| 123 | + | } else { | |
| 124 | + | r0 = ret.Get(0).(bool) | |
| 125 | + | } | |
| 126 | + | return r0 | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // MockBlobStorage_BlobExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BlobExists' | |
| 130 | + | type MockBlobStorage_BlobExists_Call struct { | |
| 131 | + | *mock.Call | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // BlobExists is a helper method to define mock.On call | |
| 135 | + | // - digest string | |
| 136 | + | func (_e *MockBlobStorage_Expecter) BlobExists(digest interface{}) *MockBlobStorage_BlobExists_Call { | |
| 137 | + | return &MockBlobStorage_BlobExists_Call{Call: _e.mock.On("BlobExists", digest)} | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (_c *MockBlobStorage_BlobExists_Call) Run(run func(digest string)) *MockBlobStorage_BlobExists_Call { | |
| 141 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 142 | + | var arg0 string | |
| 143 | + | if args[0] != nil { | |
| 144 | + | arg0 = args[0].(string) | |
| 145 | + | } | |
| 146 | + | run( | |
| 147 | + | arg0, | |
| 148 | + | ) | |
| 149 | + | }) | |
| 150 | + | return _c | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | func (_c *MockBlobStorage_BlobExists_Call) Return(b bool) *MockBlobStorage_BlobExists_Call { | |
| 154 | + | _c.Call.Return(b) | |
| 155 | + | return _c | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func (_c *MockBlobStorage_BlobExists_Call) RunAndReturn(run func(digest string) bool) *MockBlobStorage_BlobExists_Call { | |
| 159 | + | _c.Call.Return(run) | |
| 160 | + | return _c | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | // CancelBlobUpload provides a mock function for the type MockBlobStorage | |
| 164 | + | func (_mock *MockBlobStorage) CancelBlobUpload(uuid string) error { | |
| 165 | + | ret := _mock.Called(uuid) | |
| 166 | + | ||
| 167 | + | if len(ret) == 0 { | |
| 168 | + | panic("no return value specified for CancelBlobUpload") | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | var r0 error | |
| 172 | + | if returnFunc, ok := ret.Get(0).(func(string) error); ok { | |
| 173 | + | r0 = returnFunc(uuid) | |
| 174 | + | } else { | |
| 175 | + | r0 = ret.Error(0) | |
| 176 | + | } | |
| 177 | + | return r0 | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // MockBlobStorage_CancelBlobUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelBlobUpload' | |
| 181 | + | type MockBlobStorage_CancelBlobUpload_Call struct { | |
| 182 | + | *mock.Call | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // CancelBlobUpload is a helper method to define mock.On call | |
| 186 | + | // - uuid string | |
| 187 | + | func (_e *MockBlobStorage_Expecter) CancelBlobUpload(uuid interface{}) *MockBlobStorage_CancelBlobUpload_Call { | |
| 188 | + | return &MockBlobStorage_CancelBlobUpload_Call{Call: _e.mock.On("CancelBlobUpload", uuid)} | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func (_c *MockBlobStorage_CancelBlobUpload_Call) Run(run func(uuid string)) *MockBlobStorage_CancelBlobUpload_Call { | |
| 192 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 193 | + | var arg0 string | |
| 194 | + | if args[0] != nil { | |
| 195 | + | arg0 = args[0].(string) | |
| 196 | + | } | |
| 197 | + | run( | |
| 198 | + | arg0, | |
| 199 | + | ) | |
| 200 | + | }) | |
| 201 | + | return _c | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | func (_c *MockBlobStorage_CancelBlobUpload_Call) Return(err error) *MockBlobStorage_CancelBlobUpload_Call { | |
| 205 | + | _c.Call.Return(err) | |
| 206 | + | return _c | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (_c *MockBlobStorage_CancelBlobUpload_Call) RunAndReturn(run func(uuid string) error) *MockBlobStorage_CancelBlobUpload_Call { | |
| 210 | + | _c.Call.Return(run) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // DeleteBlob provides a mock function for the type MockBlobStorage | |
| 215 | + | func (_mock *MockBlobStorage) DeleteBlob(digest string) error { | |
| 216 | + | ret := _mock.Called(digest) | |
| 217 | + | ||
| 218 | + | if len(ret) == 0 { | |
| 219 | + | panic("no return value specified for DeleteBlob") | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | var r0 error | |
| 223 | + | if returnFunc, ok := ret.Get(0).(func(string) error); ok { | |
| 224 | + | r0 = returnFunc(digest) | |
| 225 | + | } else { | |
| 226 | + | r0 = ret.Error(0) | |
| 227 | + | } | |
| 228 | + | return r0 | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // MockBlobStorage_DeleteBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBlob' | |
| 232 | + | type MockBlobStorage_DeleteBlob_Call struct { | |
| 233 | + | *mock.Call | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // DeleteBlob is a helper method to define mock.On call | |
| 237 | + | // - digest string | |
| 238 | + | func (_e *MockBlobStorage_Expecter) DeleteBlob(digest interface{}) *MockBlobStorage_DeleteBlob_Call { | |
| 239 | + | return &MockBlobStorage_DeleteBlob_Call{Call: _e.mock.On("DeleteBlob", digest)} | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func (_c *MockBlobStorage_DeleteBlob_Call) Run(run func(digest string)) *MockBlobStorage_DeleteBlob_Call { | |
| 243 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 244 | + | var arg0 string | |
| 245 | + | if args[0] != nil { | |
| 246 | + | arg0 = args[0].(string) | |
| 247 | + | } | |
| 248 | + | run( | |
| 249 | + | arg0, | |
| 250 | + | ) | |
| 251 | + | }) | |
| 252 | + | return _c | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | func (_c *MockBlobStorage_DeleteBlob_Call) Return(err error) *MockBlobStorage_DeleteBlob_Call { | |
| 256 | + | _c.Call.Return(err) | |
| 257 | + | return _c | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func (_c *MockBlobStorage_DeleteBlob_Call) RunAndReturn(run func(digest string) error) *MockBlobStorage_DeleteBlob_Call { | |
| 261 | + | _c.Call.Return(run) | |
| 262 | + | return _c | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | // FinishBlobUpload provides a mock function for the type MockBlobStorage | |
| 266 | + | func (_mock *MockBlobStorage) FinishBlobUpload(uuid string, digest string) error { | |
| 267 | + | ret := _mock.Called(uuid, digest) | |
| 268 | + | ||
| 269 | + | if len(ret) == 0 { | |
| 270 | + | panic("no return value specified for FinishBlobUpload") | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | var r0 error | |
| 274 | + | if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { | |
| 275 | + | r0 = returnFunc(uuid, digest) | |
| 276 | + | } else { | |
| 277 | + | r0 = ret.Error(0) | |
| 278 | + | } | |
| 279 | + | return r0 | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // MockBlobStorage_FinishBlobUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FinishBlobUpload' | |
| 283 | + | type MockBlobStorage_FinishBlobUpload_Call struct { | |
| 284 | + | *mock.Call | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // FinishBlobUpload is a helper method to define mock.On call | |
| 288 | + | // - uuid string | |
| 289 | + | // - digest string | |
| 290 | + | func (_e *MockBlobStorage_Expecter) FinishBlobUpload(uuid interface{}, digest interface{}) *MockBlobStorage_FinishBlobUpload_Call { | |
| 291 | + | return &MockBlobStorage_FinishBlobUpload_Call{Call: _e.mock.On("FinishBlobUpload", uuid, digest)} | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func (_c *MockBlobStorage_FinishBlobUpload_Call) Run(run func(uuid string, digest string)) *MockBlobStorage_FinishBlobUpload_Call { | |
| 295 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 296 | + | var arg0 string | |
| 297 | + | if args[0] != nil { | |
| 298 | + | arg0 = args[0].(string) | |
| 299 | + | } | |
| 300 | + | var arg1 string | |
| 301 | + | if args[1] != nil { | |
| 302 | + | arg1 = args[1].(string) | |
| 303 | + | } | |
| 304 | + | run( | |
| 305 | + | arg0, | |
| 306 | + | arg1, | |
| 307 | + | ) | |
| 308 | + | }) | |
| 309 | + | return _c | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | func (_c *MockBlobStorage_FinishBlobUpload_Call) Return(err error) *MockBlobStorage_FinishBlobUpload_Call { | |
| 313 | + | _c.Call.Return(err) | |
| 314 | + | return _c | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func (_c *MockBlobStorage_FinishBlobUpload_Call) RunAndReturn(run func(uuid string, digest string) error) *MockBlobStorage_FinishBlobUpload_Call { | |
| 318 | + | _c.Call.Return(run) | |
| 319 | + | return _c | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // GetBlob provides a mock function for the type MockBlobStorage | |
| 323 | + | func (_mock *MockBlobStorage) GetBlob(digest string) (io.ReadCloser, error) { | |
| 324 | + | ret := _mock.Called(digest) | |
| 325 | + | ||
| 326 | + | if len(ret) == 0 { | |
| 327 | + | panic("no return value specified for GetBlob") | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | var r0 io.ReadCloser | |
| 331 | + | var r1 error | |
| 332 | + | if returnFunc, ok := ret.Get(0).(func(string) (io.ReadCloser, error)); ok { | |
| 333 | + | return returnFunc(digest) | |
| 334 | + | } | |
| 335 | + | if returnFunc, ok := ret.Get(0).(func(string) io.ReadCloser); ok { | |
| 336 | + | r0 = returnFunc(digest) | |
| 337 | + | } else { | |
| 338 | + | if ret.Get(0) != nil { | |
| 339 | + | r0 = ret.Get(0).(io.ReadCloser) | |
| 340 | + | } | |
| 341 | + | } | |
| 342 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 343 | + | r1 = returnFunc(digest) | |
| 344 | + | } else { | |
| 345 | + | r1 = ret.Error(1) | |
| 346 | + | } | |
| 347 | + | return r0, r1 | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | // MockBlobStorage_GetBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlob' | |
| 351 | + | type MockBlobStorage_GetBlob_Call struct { | |
| 352 | + | *mock.Call | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | // GetBlob is a helper method to define mock.On call | |
| 356 | + | // - digest string | |
| 357 | + | func (_e *MockBlobStorage_Expecter) GetBlob(digest interface{}) *MockBlobStorage_GetBlob_Call { | |
| 358 | + | return &MockBlobStorage_GetBlob_Call{Call: _e.mock.On("GetBlob", digest)} | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | func (_c *MockBlobStorage_GetBlob_Call) Run(run func(digest string)) *MockBlobStorage_GetBlob_Call { | |
| 362 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 363 | + | var arg0 string | |
| 364 | + | if args[0] != nil { | |
| 365 | + | arg0 = args[0].(string) | |
| 366 | + | } | |
| 367 | + | run( | |
| 368 | + | arg0, | |
| 369 | + | ) | |
| 370 | + | }) | |
| 371 | + | return _c | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | func (_c *MockBlobStorage_GetBlob_Call) Return(readCloser io.ReadCloser, err error) *MockBlobStorage_GetBlob_Call { | |
| 375 | + | _c.Call.Return(readCloser, err) | |
| 376 | + | return _c | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | func (_c *MockBlobStorage_GetBlob_Call) RunAndReturn(run func(digest string) (io.ReadCloser, error)) *MockBlobStorage_GetBlob_Call { | |
| 380 | + | _c.Call.Return(run) | |
| 381 | + | return _c | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // GetBlobPath provides a mock function for the type MockBlobStorage | |
| 385 | + | func (_mock *MockBlobStorage) GetBlobPath(digest string) (string, error) { | |
| 386 | + | ret := _mock.Called(digest) | |
| 387 | + | ||
| 388 | + | if len(ret) == 0 { | |
| 389 | + | panic("no return value specified for GetBlobPath") | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | var r0 string | |
| 393 | + | var r1 error | |
| 394 | + | if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok { | |
| 395 | + | return returnFunc(digest) | |
| 396 | + | } | |
| 397 | + | if returnFunc, ok := ret.Get(0).(func(string) string); ok { | |
| 398 | + | r0 = returnFunc(digest) | |
| 399 | + | } else { | |
| 400 | + | r0 = ret.Get(0).(string) | |
| 401 | + | } | |
| 402 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 403 | + | r1 = returnFunc(digest) | |
| 404 | + | } else { | |
| 405 | + | r1 = ret.Error(1) | |
| 406 | + | } | |
| 407 | + | return r0, r1 | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | // MockBlobStorage_GetBlobPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlobPath' | |
| 411 | + | type MockBlobStorage_GetBlobPath_Call struct { | |
| 412 | + | *mock.Call | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | // GetBlobPath is a helper method to define mock.On call | |
| 416 | + | // - digest string | |
| 417 | + | func (_e *MockBlobStorage_Expecter) GetBlobPath(digest interface{}) *MockBlobStorage_GetBlobPath_Call { | |
| 418 | + | return &MockBlobStorage_GetBlobPath_Call{Call: _e.mock.On("GetBlobPath", digest)} | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | func (_c *MockBlobStorage_GetBlobPath_Call) Run(run func(digest string)) *MockBlobStorage_GetBlobPath_Call { | |
| 422 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 423 | + | var arg0 string | |
| 424 | + | if args[0] != nil { | |
| 425 | + | arg0 = args[0].(string) | |
| 426 | + | } | |
| 427 | + | run( | |
| 428 | + | arg0, | |
| 429 | + | ) | |
| 430 | + | }) | |
| 431 | + | return _c | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | func (_c *MockBlobStorage_GetBlobPath_Call) Return(s string, err error) *MockBlobStorage_GetBlobPath_Call { | |
| 435 | + | _c.Call.Return(s, err) | |
| 436 | + | return _c | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | func (_c *MockBlobStorage_GetBlobPath_Call) RunAndReturn(run func(digest string) (string, error)) *MockBlobStorage_GetBlobPath_Call { | |
| 440 | + | _c.Call.Return(run) | |
| 441 | + | return _c | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | // GetBlobUpload provides a mock function for the type MockBlobStorage | |
| 445 | + | func (_mock *MockBlobStorage) GetBlobUpload(uuid string) (io.WriteCloser, error) { | |
| 446 | + | ret := _mock.Called(uuid) | |
| 447 | + | ||
| 448 | + | if len(ret) == 0 { | |
| 449 | + | panic("no return value specified for GetBlobUpload") | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | var r0 io.WriteCloser | |
| 453 | + | var r1 error | |
| 454 | + | if returnFunc, ok := ret.Get(0).(func(string) (io.WriteCloser, error)); ok { | |
| 455 | + | return returnFunc(uuid) | |
| 456 | + | } | |
| 457 | + | if returnFunc, ok := ret.Get(0).(func(string) io.WriteCloser); ok { | |
| 458 | + | r0 = returnFunc(uuid) | |
| 459 | + | } else { | |
| 460 | + | if ret.Get(0) != nil { | |
| 461 | + | r0 = ret.Get(0).(io.WriteCloser) | |
| 462 | + | } | |
| 463 | + | } | |
| 464 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 465 | + | r1 = returnFunc(uuid) | |
| 466 | + | } else { | |
| 467 | + | r1 = ret.Error(1) | |
| 468 | + | } | |
| 469 | + | return r0, r1 | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | // MockBlobStorage_GetBlobUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlobUpload' | |
| 473 | + | type MockBlobStorage_GetBlobUpload_Call struct { | |
| 474 | + | *mock.Call | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | // GetBlobUpload is a helper method to define mock.On call | |
| 478 | + | // - uuid string | |
| 479 | + | func (_e *MockBlobStorage_Expecter) GetBlobUpload(uuid interface{}) *MockBlobStorage_GetBlobUpload_Call { | |
| 480 | + | return &MockBlobStorage_GetBlobUpload_Call{Call: _e.mock.On("GetBlobUpload", uuid)} | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | func (_c *MockBlobStorage_GetBlobUpload_Call) Run(run func(uuid string)) *MockBlobStorage_GetBlobUpload_Call { | |
| 484 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 485 | + | var arg0 string | |
| 486 | + | if args[0] != nil { | |
| 487 | + | arg0 = args[0].(string) | |
| 488 | + | } | |
| 489 | + | run( | |
| 490 | + | arg0, | |
| 491 | + | ) | |
| 492 | + | }) | |
| 493 | + | return _c | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | func (_c *MockBlobStorage_GetBlobUpload_Call) Return(writeCloser io.WriteCloser, err error) *MockBlobStorage_GetBlobUpload_Call { | |
| 497 | + | _c.Call.Return(writeCloser, err) | |
| 498 | + | return _c | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | func (_c *MockBlobStorage_GetBlobUpload_Call) RunAndReturn(run func(uuid string) (io.WriteCloser, error)) *MockBlobStorage_GetBlobUpload_Call { | |
| 502 | + | _c.Call.Return(run) | |
| 503 | + | return _c | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | // PutBlob provides a mock function for the type MockBlobStorage | |
| 507 | + | func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error { | |
| 508 | + | ret := _mock.Called(digest, data, size) | |
| 509 | + | ||
| 510 | + | if len(ret) == 0 { | |
| 511 | + | panic("no return value specified for PutBlob") | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | var r0 error | |
| 515 | + | if returnFunc, ok := ret.Get(0).(func(string, io.Reader, int64) error); ok { | |
| 516 | + | r0 = returnFunc(digest, data, size) | |
| 517 | + | } else { | |
| 518 | + | r0 = ret.Error(0) | |
| 519 | + | } | |
| 520 | + | return r0 | |
| 521 | + | } | |
| 522 | + | ||
| 523 | + | // MockBlobStorage_PutBlob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutBlob' | |
| 524 | + | type MockBlobStorage_PutBlob_Call struct { | |
| 525 | + | *mock.Call | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | // PutBlob is a helper method to define mock.On call | |
| 529 | + | // - digest string | |
| 530 | + | // - data io.Reader | |
| 531 | + | // - size int64 | |
| 532 | + | func (_e *MockBlobStorage_Expecter) PutBlob(digest interface{}, data interface{}, size interface{}) *MockBlobStorage_PutBlob_Call { | |
| 533 | + | return &MockBlobStorage_PutBlob_Call{Call: _e.mock.On("PutBlob", digest, data, size)} | |
| 534 | + | } | |
| 535 | + | ||
| 536 | + | func (_c *MockBlobStorage_PutBlob_Call) Run(run func(digest string, data io.Reader, size int64)) *MockBlobStorage_PutBlob_Call { | |
| 537 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 538 | + | var arg0 string | |
| 539 | + | if args[0] != nil { | |
| 540 | + | arg0 = args[0].(string) | |
| 541 | + | } | |
| 542 | + | var arg1 io.Reader | |
| 543 | + | if args[1] != nil { | |
| 544 | + | arg1 = args[1].(io.Reader) | |
| 545 | + | } | |
| 546 | + | var arg2 int64 | |
| 547 | + | if args[2] != nil { | |
| 548 | + | arg2 = args[2].(int64) | |
| 549 | + | } | |
| 550 | + | run( | |
| 551 | + | arg0, | |
| 552 | + | arg1, | |
| 553 | + | arg2, | |
| 554 | + | ) | |
| 555 | + | }) | |
| 556 | + | return _c | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | func (_c *MockBlobStorage_PutBlob_Call) Return(err error) *MockBlobStorage_PutBlob_Call { | |
| 560 | + | _c.Call.Return(err) | |
| 561 | + | return _c | |
| 562 | + | } | |
| 563 | + | ||
| 564 | + | func (_c *MockBlobStorage_PutBlob_Call) RunAndReturn(run func(digest string, data io.Reader, size int64) error) *MockBlobStorage_PutBlob_Call { | |
| 565 | + | _c.Call.Return(run) | |
| 566 | + | return _c | |
| 567 | + | } | |
| 568 | + | ||
| 569 | + | // StartBlobUpload provides a mock function for the type MockBlobStorage | |
| 570 | + | func (_mock *MockBlobStorage) StartBlobUpload(name string) (string, error) { | |
| 571 | + | ret := _mock.Called(name) | |
| 572 | + | ||
| 573 | + | if len(ret) == 0 { | |
| 574 | + | panic("no return value specified for StartBlobUpload") | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | var r0 string | |
| 578 | + | var r1 error | |
| 579 | + | if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok { | |
| 580 | + | return returnFunc(name) | |
| 581 | + | } | |
| 582 | + | if returnFunc, ok := ret.Get(0).(func(string) string); ok { | |
| 583 | + | r0 = returnFunc(name) | |
| 584 | + | } else { | |
| 585 | + | r0 = ret.Get(0).(string) | |
| 586 | + | } | |
| 587 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 588 | + | r1 = returnFunc(name) | |
| 589 | + | } else { | |
| 590 | + | r1 = ret.Error(1) | |
| 591 | + | } | |
| 592 | + | return r0, r1 | |
| 593 | + | } | |
| 594 | + | ||
| 595 | + | // MockBlobStorage_StartBlobUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartBlobUpload' | |
| 596 | + | type MockBlobStorage_StartBlobUpload_Call struct { | |
| 597 | + | *mock.Call | |
| 598 | + | } | |
| 599 | + | ||
| 600 | + | // StartBlobUpload is a helper method to define mock.On call | |
| 601 | + | // - name string | |
| 602 | + | func (_e *MockBlobStorage_Expecter) StartBlobUpload(name interface{}) *MockBlobStorage_StartBlobUpload_Call { | |
| 603 | + | return &MockBlobStorage_StartBlobUpload_Call{Call: _e.mock.On("StartBlobUpload", name)} | |
| 604 | + | } | |
| 605 | + | ||
| 606 | + | func (_c *MockBlobStorage_StartBlobUpload_Call) Run(run func(name string)) *MockBlobStorage_StartBlobUpload_Call { | |
| 607 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 608 | + | var arg0 string | |
| 609 | + | if args[0] != nil { | |
| 610 | + | arg0 = args[0].(string) | |
| 611 | + | } | |
| 612 | + | run( | |
| 613 | + | arg0, | |
| 614 | + | ) | |
| 615 | + | }) | |
| 616 | + | return _c | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | func (_c *MockBlobStorage_StartBlobUpload_Call) Return(s string, err error) *MockBlobStorage_StartBlobUpload_Call { | |
| 620 | + | _c.Call.Return(s, err) | |
| 621 | + | return _c | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | func (_c *MockBlobStorage_StartBlobUpload_Call) RunAndReturn(run func(name string) (string, error)) *MockBlobStorage_StartBlobUpload_Call { | |
| 625 | + | _c.Call.Return(run) | |
| 626 | + | return _c | |
| 627 | + | } | |
A internal/boundaries/out/mocks/mock_container_runtime.go +1956 -0
| @@ -0,0 +1,1956 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | "io" | |
| 11 | + | ||
| 12 | + | mock "github.com/stretchr/testify/mock" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // NewMockContainerRuntime creates a new instance of MockContainerRuntime. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 16 | + | // The first argument is typically a *testing.T value. | |
| 17 | + | func NewMockContainerRuntime(t interface { | |
| 18 | + | mock.TestingT | |
| 19 | + | Cleanup(func()) | |
| 20 | + | }) *MockContainerRuntime { | |
| 21 | + | mock := &MockContainerRuntime{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 25 | + | ||
| 26 | + | return mock | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // MockContainerRuntime is an autogenerated mock type for the ContainerRuntime type | |
| 30 | + | type MockContainerRuntime struct { | |
| 31 | + | mock.Mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | type MockContainerRuntime_Expecter struct { | |
| 35 | + | mock *mock.Mock | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (_m *MockContainerRuntime) EXPECT() *MockContainerRuntime_Expecter { | |
| 39 | + | return &MockContainerRuntime_Expecter{mock: &_m.Mock} | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // ConnectContainerToNetwork provides a mock function for the type MockContainerRuntime | |
| 43 | + | func (_mock *MockContainerRuntime) ConnectContainerToNetwork(ctx context.Context, containerName string, networkName string) error { | |
| 44 | + | ret := _mock.Called(ctx, containerName, networkName) | |
| 45 | + | ||
| 46 | + | if len(ret) == 0 { | |
| 47 | + | panic("no return value specified for ConnectContainerToNetwork") | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | var r0 error | |
| 51 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 52 | + | r0 = returnFunc(ctx, containerName, networkName) | |
| 53 | + | } else { | |
| 54 | + | r0 = ret.Error(0) | |
| 55 | + | } | |
| 56 | + | return r0 | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // MockContainerRuntime_ConnectContainerToNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectContainerToNetwork' | |
| 60 | + | type MockContainerRuntime_ConnectContainerToNetwork_Call struct { | |
| 61 | + | *mock.Call | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // ConnectContainerToNetwork is a helper method to define mock.On call | |
| 65 | + | // - ctx context.Context | |
| 66 | + | // - containerName string | |
| 67 | + | // - networkName string | |
| 68 | + | func (_e *MockContainerRuntime_Expecter) ConnectContainerToNetwork(ctx interface{}, containerName interface{}, networkName interface{}) *MockContainerRuntime_ConnectContainerToNetwork_Call { | |
| 69 | + | return &MockContainerRuntime_ConnectContainerToNetwork_Call{Call: _e.mock.On("ConnectContainerToNetwork", ctx, containerName, networkName)} | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (_c *MockContainerRuntime_ConnectContainerToNetwork_Call) Run(run func(ctx context.Context, containerName string, networkName string)) *MockContainerRuntime_ConnectContainerToNetwork_Call { | |
| 73 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 74 | + | var arg0 context.Context | |
| 75 | + | if args[0] != nil { | |
| 76 | + | arg0 = args[0].(context.Context) | |
| 77 | + | } | |
| 78 | + | var arg1 string | |
| 79 | + | if args[1] != nil { | |
| 80 | + | arg1 = args[1].(string) | |
| 81 | + | } | |
| 82 | + | var arg2 string | |
| 83 | + | if args[2] != nil { | |
| 84 | + | arg2 = args[2].(string) | |
| 85 | + | } | |
| 86 | + | run( | |
| 87 | + | arg0, | |
| 88 | + | arg1, | |
| 89 | + | arg2, | |
| 90 | + | ) | |
| 91 | + | }) | |
| 92 | + | return _c | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | func (_c *MockContainerRuntime_ConnectContainerToNetwork_Call) Return(err error) *MockContainerRuntime_ConnectContainerToNetwork_Call { | |
| 96 | + | _c.Call.Return(err) | |
| 97 | + | return _c | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func (_c *MockContainerRuntime_ConnectContainerToNetwork_Call) RunAndReturn(run func(ctx context.Context, containerName string, networkName string) error) *MockContainerRuntime_ConnectContainerToNetwork_Call { | |
| 101 | + | _c.Call.Return(run) | |
| 102 | + | return _c | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // CreateContainer provides a mock function for the type MockContainerRuntime | |
| 106 | + | func (_mock *MockContainerRuntime) CreateContainer(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error) { | |
| 107 | + | ret := _mock.Called(ctx, config) | |
| 108 | + | ||
| 109 | + | if len(ret) == 0 { | |
| 110 | + | panic("no return value specified for CreateContainer") | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | var r0 *domain.Container | |
| 114 | + | var r1 error | |
| 115 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.ContainerConfig) (*domain.Container, error)); ok { | |
| 116 | + | return returnFunc(ctx, config) | |
| 117 | + | } | |
| 118 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.ContainerConfig) *domain.Container); ok { | |
| 119 | + | r0 = returnFunc(ctx, config) | |
| 120 | + | } else { | |
| 121 | + | if ret.Get(0) != nil { | |
| 122 | + | r0 = ret.Get(0).(*domain.Container) | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, *domain.ContainerConfig) error); ok { | |
| 126 | + | r1 = returnFunc(ctx, config) | |
| 127 | + | } else { | |
| 128 | + | r1 = ret.Error(1) | |
| 129 | + | } | |
| 130 | + | return r0, r1 | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // MockContainerRuntime_CreateContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateContainer' | |
| 134 | + | type MockContainerRuntime_CreateContainer_Call struct { | |
| 135 | + | *mock.Call | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // CreateContainer is a helper method to define mock.On call | |
| 139 | + | // - ctx context.Context | |
| 140 | + | // - config *domain.ContainerConfig | |
| 141 | + | func (_e *MockContainerRuntime_Expecter) CreateContainer(ctx interface{}, config interface{}) *MockContainerRuntime_CreateContainer_Call { | |
| 142 | + | return &MockContainerRuntime_CreateContainer_Call{Call: _e.mock.On("CreateContainer", ctx, config)} | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func (_c *MockContainerRuntime_CreateContainer_Call) Run(run func(ctx context.Context, config *domain.ContainerConfig)) *MockContainerRuntime_CreateContainer_Call { | |
| 146 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 147 | + | var arg0 context.Context | |
| 148 | + | if args[0] != nil { | |
| 149 | + | arg0 = args[0].(context.Context) | |
| 150 | + | } | |
| 151 | + | var arg1 *domain.ContainerConfig | |
| 152 | + | if args[1] != nil { | |
| 153 | + | arg1 = args[1].(*domain.ContainerConfig) | |
| 154 | + | } | |
| 155 | + | run( | |
| 156 | + | arg0, | |
| 157 | + | arg1, | |
| 158 | + | ) | |
| 159 | + | }) | |
| 160 | + | return _c | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func (_c *MockContainerRuntime_CreateContainer_Call) Return(container *domain.Container, err error) *MockContainerRuntime_CreateContainer_Call { | |
| 164 | + | _c.Call.Return(container, err) | |
| 165 | + | return _c | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func (_c *MockContainerRuntime_CreateContainer_Call) RunAndReturn(run func(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error)) *MockContainerRuntime_CreateContainer_Call { | |
| 169 | + | _c.Call.Return(run) | |
| 170 | + | return _c | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | // CreateNetwork provides a mock function for the type MockContainerRuntime | |
| 174 | + | func (_mock *MockContainerRuntime) CreateNetwork(ctx context.Context, name string, options map[string]string) error { | |
| 175 | + | ret := _mock.Called(ctx, name, options) | |
| 176 | + | ||
| 177 | + | if len(ret) == 0 { | |
| 178 | + | panic("no return value specified for CreateNetwork") | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | var r0 error | |
| 182 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, map[string]string) error); ok { | |
| 183 | + | r0 = returnFunc(ctx, name, options) | |
| 184 | + | } else { | |
| 185 | + | r0 = ret.Error(0) | |
| 186 | + | } | |
| 187 | + | return r0 | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // MockContainerRuntime_CreateNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateNetwork' | |
| 191 | + | type MockContainerRuntime_CreateNetwork_Call struct { | |
| 192 | + | *mock.Call | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // CreateNetwork is a helper method to define mock.On call | |
| 196 | + | // - ctx context.Context | |
| 197 | + | // - name string | |
| 198 | + | // - options map[string]string | |
| 199 | + | func (_e *MockContainerRuntime_Expecter) CreateNetwork(ctx interface{}, name interface{}, options interface{}) *MockContainerRuntime_CreateNetwork_Call { | |
| 200 | + | return &MockContainerRuntime_CreateNetwork_Call{Call: _e.mock.On("CreateNetwork", ctx, name, options)} | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | func (_c *MockContainerRuntime_CreateNetwork_Call) Run(run func(ctx context.Context, name string, options map[string]string)) *MockContainerRuntime_CreateNetwork_Call { | |
| 204 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 205 | + | var arg0 context.Context | |
| 206 | + | if args[0] != nil { | |
| 207 | + | arg0 = args[0].(context.Context) | |
| 208 | + | } | |
| 209 | + | var arg1 string | |
| 210 | + | if args[1] != nil { | |
| 211 | + | arg1 = args[1].(string) | |
| 212 | + | } | |
| 213 | + | var arg2 map[string]string | |
| 214 | + | if args[2] != nil { | |
| 215 | + | arg2 = args[2].(map[string]string) | |
| 216 | + | } | |
| 217 | + | run( | |
| 218 | + | arg0, | |
| 219 | + | arg1, | |
| 220 | + | arg2, | |
| 221 | + | ) | |
| 222 | + | }) | |
| 223 | + | return _c | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | func (_c *MockContainerRuntime_CreateNetwork_Call) Return(err error) *MockContainerRuntime_CreateNetwork_Call { | |
| 227 | + | _c.Call.Return(err) | |
| 228 | + | return _c | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | func (_c *MockContainerRuntime_CreateNetwork_Call) RunAndReturn(run func(ctx context.Context, name string, options map[string]string) error) *MockContainerRuntime_CreateNetwork_Call { | |
| 232 | + | _c.Call.Return(run) | |
| 233 | + | return _c | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // CreateVolume provides a mock function for the type MockContainerRuntime | |
| 237 | + | func (_mock *MockContainerRuntime) CreateVolume(ctx context.Context, volumeName string) error { | |
| 238 | + | ret := _mock.Called(ctx, volumeName) | |
| 239 | + | ||
| 240 | + | if len(ret) == 0 { | |
| 241 | + | panic("no return value specified for CreateVolume") | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | var r0 error | |
| 245 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 246 | + | r0 = returnFunc(ctx, volumeName) | |
| 247 | + | } else { | |
| 248 | + | r0 = ret.Error(0) | |
| 249 | + | } | |
| 250 | + | return r0 | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // MockContainerRuntime_CreateVolume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateVolume' | |
| 254 | + | type MockContainerRuntime_CreateVolume_Call struct { | |
| 255 | + | *mock.Call | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // CreateVolume is a helper method to define mock.On call | |
| 259 | + | // - ctx context.Context | |
| 260 | + | // - volumeName string | |
| 261 | + | func (_e *MockContainerRuntime_Expecter) CreateVolume(ctx interface{}, volumeName interface{}) *MockContainerRuntime_CreateVolume_Call { | |
| 262 | + | return &MockContainerRuntime_CreateVolume_Call{Call: _e.mock.On("CreateVolume", ctx, volumeName)} | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (_c *MockContainerRuntime_CreateVolume_Call) Run(run func(ctx context.Context, volumeName string)) *MockContainerRuntime_CreateVolume_Call { | |
| 266 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 267 | + | var arg0 context.Context | |
| 268 | + | if args[0] != nil { | |
| 269 | + | arg0 = args[0].(context.Context) | |
| 270 | + | } | |
| 271 | + | var arg1 string | |
| 272 | + | if args[1] != nil { | |
| 273 | + | arg1 = args[1].(string) | |
| 274 | + | } | |
| 275 | + | run( | |
| 276 | + | arg0, | |
| 277 | + | arg1, | |
| 278 | + | ) | |
| 279 | + | }) | |
| 280 | + | return _c | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | func (_c *MockContainerRuntime_CreateVolume_Call) Return(err error) *MockContainerRuntime_CreateVolume_Call { | |
| 284 | + | _c.Call.Return(err) | |
| 285 | + | return _c | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | func (_c *MockContainerRuntime_CreateVolume_Call) RunAndReturn(run func(ctx context.Context, volumeName string) error) *MockContainerRuntime_CreateVolume_Call { | |
| 289 | + | _c.Call.Return(run) | |
| 290 | + | return _c | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // DisconnectContainerFromNetwork provides a mock function for the type MockContainerRuntime | |
| 294 | + | func (_mock *MockContainerRuntime) DisconnectContainerFromNetwork(ctx context.Context, containerName string, networkName string) error { | |
| 295 | + | ret := _mock.Called(ctx, containerName, networkName) | |
| 296 | + | ||
| 297 | + | if len(ret) == 0 { | |
| 298 | + | panic("no return value specified for DisconnectContainerFromNetwork") | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | var r0 error | |
| 302 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 303 | + | r0 = returnFunc(ctx, containerName, networkName) | |
| 304 | + | } else { | |
| 305 | + | r0 = ret.Error(0) | |
| 306 | + | } | |
| 307 | + | return r0 | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // MockContainerRuntime_DisconnectContainerFromNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectContainerFromNetwork' | |
| 311 | + | type MockContainerRuntime_DisconnectContainerFromNetwork_Call struct { | |
| 312 | + | *mock.Call | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // DisconnectContainerFromNetwork is a helper method to define mock.On call | |
| 316 | + | // - ctx context.Context | |
| 317 | + | // - containerName string | |
| 318 | + | // - networkName string | |
| 319 | + | func (_e *MockContainerRuntime_Expecter) DisconnectContainerFromNetwork(ctx interface{}, containerName interface{}, networkName interface{}) *MockContainerRuntime_DisconnectContainerFromNetwork_Call { | |
| 320 | + | return &MockContainerRuntime_DisconnectContainerFromNetwork_Call{Call: _e.mock.On("DisconnectContainerFromNetwork", ctx, containerName, networkName)} | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | func (_c *MockContainerRuntime_DisconnectContainerFromNetwork_Call) Run(run func(ctx context.Context, containerName string, networkName string)) *MockContainerRuntime_DisconnectContainerFromNetwork_Call { | |
| 324 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 325 | + | var arg0 context.Context | |
| 326 | + | if args[0] != nil { | |
| 327 | + | arg0 = args[0].(context.Context) | |
| 328 | + | } | |
| 329 | + | var arg1 string | |
| 330 | + | if args[1] != nil { | |
| 331 | + | arg1 = args[1].(string) | |
| 332 | + | } | |
| 333 | + | var arg2 string | |
| 334 | + | if args[2] != nil { | |
| 335 | + | arg2 = args[2].(string) | |
| 336 | + | } | |
| 337 | + | run( | |
| 338 | + | arg0, | |
| 339 | + | arg1, | |
| 340 | + | arg2, | |
| 341 | + | ) | |
| 342 | + | }) | |
| 343 | + | return _c | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | func (_c *MockContainerRuntime_DisconnectContainerFromNetwork_Call) Return(err error) *MockContainerRuntime_DisconnectContainerFromNetwork_Call { | |
| 347 | + | _c.Call.Return(err) | |
| 348 | + | return _c | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | func (_c *MockContainerRuntime_DisconnectContainerFromNetwork_Call) RunAndReturn(run func(ctx context.Context, containerName string, networkName string) error) *MockContainerRuntime_DisconnectContainerFromNetwork_Call { | |
| 352 | + | _c.Call.Return(run) | |
| 353 | + | return _c | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // GetContainerExposedPorts provides a mock function for the type MockContainerRuntime | |
| 357 | + | func (_mock *MockContainerRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { | |
| 358 | + | ret := _mock.Called(ctx, containerID) | |
| 359 | + | ||
| 360 | + | if len(ret) == 0 { | |
| 361 | + | panic("no return value specified for GetContainerExposedPorts") | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | var r0 []int | |
| 365 | + | var r1 error | |
| 366 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]int, error)); ok { | |
| 367 | + | return returnFunc(ctx, containerID) | |
| 368 | + | } | |
| 369 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []int); ok { | |
| 370 | + | r0 = returnFunc(ctx, containerID) | |
| 371 | + | } else { | |
| 372 | + | if ret.Get(0) != nil { | |
| 373 | + | r0 = ret.Get(0).([]int) | |
| 374 | + | } | |
| 375 | + | } | |
| 376 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 377 | + | r1 = returnFunc(ctx, containerID) | |
| 378 | + | } else { | |
| 379 | + | r1 = ret.Error(1) | |
| 380 | + | } | |
| 381 | + | return r0, r1 | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // MockContainerRuntime_GetContainerExposedPorts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerExposedPorts' | |
| 385 | + | type MockContainerRuntime_GetContainerExposedPorts_Call struct { | |
| 386 | + | *mock.Call | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | // GetContainerExposedPorts is a helper method to define mock.On call | |
| 390 | + | // - ctx context.Context | |
| 391 | + | // - containerID string | |
| 392 | + | func (_e *MockContainerRuntime_Expecter) GetContainerExposedPorts(ctx interface{}, containerID interface{}) *MockContainerRuntime_GetContainerExposedPorts_Call { | |
| 393 | + | return &MockContainerRuntime_GetContainerExposedPorts_Call{Call: _e.mock.On("GetContainerExposedPorts", ctx, containerID)} | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | func (_c *MockContainerRuntime_GetContainerExposedPorts_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_GetContainerExposedPorts_Call { | |
| 397 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 398 | + | var arg0 context.Context | |
| 399 | + | if args[0] != nil { | |
| 400 | + | arg0 = args[0].(context.Context) | |
| 401 | + | } | |
| 402 | + | var arg1 string | |
| 403 | + | if args[1] != nil { | |
| 404 | + | arg1 = args[1].(string) | |
| 405 | + | } | |
| 406 | + | run( | |
| 407 | + | arg0, | |
| 408 | + | arg1, | |
| 409 | + | ) | |
| 410 | + | }) | |
| 411 | + | return _c | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | func (_c *MockContainerRuntime_GetContainerExposedPorts_Call) Return(ints []int, err error) *MockContainerRuntime_GetContainerExposedPorts_Call { | |
| 415 | + | _c.Call.Return(ints, err) | |
| 416 | + | return _c | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | func (_c *MockContainerRuntime_GetContainerExposedPorts_Call) RunAndReturn(run func(ctx context.Context, containerID string) ([]int, error)) *MockContainerRuntime_GetContainerExposedPorts_Call { | |
| 420 | + | _c.Call.Return(run) | |
| 421 | + | return _c | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | // GetContainerLogs provides a mock function for the type MockContainerRuntime | |
| 425 | + | func (_mock *MockContainerRuntime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 426 | + | ret := _mock.Called(ctx, containerID, follow) | |
| 427 | + | ||
| 428 | + | if len(ret) == 0 { | |
| 429 | + | panic("no return value specified for GetContainerLogs") | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | var r0 io.ReadCloser | |
| 433 | + | var r1 error | |
| 434 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) (io.ReadCloser, error)); ok { | |
| 435 | + | return returnFunc(ctx, containerID, follow) | |
| 436 | + | } | |
| 437 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) io.ReadCloser); ok { | |
| 438 | + | r0 = returnFunc(ctx, containerID, follow) | |
| 439 | + | } else { | |
| 440 | + | if ret.Get(0) != nil { | |
| 441 | + | r0 = ret.Get(0).(io.ReadCloser) | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { | |
| 445 | + | r1 = returnFunc(ctx, containerID, follow) | |
| 446 | + | } else { | |
| 447 | + | r1 = ret.Error(1) | |
| 448 | + | } | |
| 449 | + | return r0, r1 | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | // MockContainerRuntime_GetContainerLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerLogs' | |
| 453 | + | type MockContainerRuntime_GetContainerLogs_Call struct { | |
| 454 | + | *mock.Call | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | // GetContainerLogs is a helper method to define mock.On call | |
| 458 | + | // - ctx context.Context | |
| 459 | + | // - containerID string | |
| 460 | + | // - follow bool | |
| 461 | + | func (_e *MockContainerRuntime_Expecter) GetContainerLogs(ctx interface{}, containerID interface{}, follow interface{}) *MockContainerRuntime_GetContainerLogs_Call { | |
| 462 | + | return &MockContainerRuntime_GetContainerLogs_Call{Call: _e.mock.On("GetContainerLogs", ctx, containerID, follow)} | |
| 463 | + | } | |
| 464 | + | ||
| 465 | + | func (_c *MockContainerRuntime_GetContainerLogs_Call) Run(run func(ctx context.Context, containerID string, follow bool)) *MockContainerRuntime_GetContainerLogs_Call { | |
| 466 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 467 | + | var arg0 context.Context | |
| 468 | + | if args[0] != nil { | |
| 469 | + | arg0 = args[0].(context.Context) | |
| 470 | + | } | |
| 471 | + | var arg1 string | |
| 472 | + | if args[1] != nil { | |
| 473 | + | arg1 = args[1].(string) | |
| 474 | + | } | |
| 475 | + | var arg2 bool | |
| 476 | + | if args[2] != nil { | |
| 477 | + | arg2 = args[2].(bool) | |
| 478 | + | } | |
| 479 | + | run( | |
| 480 | + | arg0, | |
| 481 | + | arg1, | |
| 482 | + | arg2, | |
| 483 | + | ) | |
| 484 | + | }) | |
| 485 | + | return _c | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | func (_c *MockContainerRuntime_GetContainerLogs_Call) Return(readCloser io.ReadCloser, err error) *MockContainerRuntime_GetContainerLogs_Call { | |
| 489 | + | _c.Call.Return(readCloser, err) | |
| 490 | + | return _c | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | func (_c *MockContainerRuntime_GetContainerLogs_Call) RunAndReturn(run func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error)) *MockContainerRuntime_GetContainerLogs_Call { | |
| 494 | + | _c.Call.Return(run) | |
| 495 | + | return _c | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | // GetContainerNetworkInfo provides a mock function for the type MockContainerRuntime | |
| 499 | + | func (_mock *MockContainerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |
| 500 | + | ret := _mock.Called(ctx, containerID) | |
| 501 | + | ||
| 502 | + | if len(ret) == 0 { | |
| 503 | + | panic("no return value specified for GetContainerNetworkInfo") | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | var r0 string | |
| 507 | + | var r1 int | |
| 508 | + | var r2 error | |
| 509 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, int, error)); ok { | |
| 510 | + | return returnFunc(ctx, containerID) | |
| 511 | + | } | |
| 512 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { | |
| 513 | + | r0 = returnFunc(ctx, containerID) | |
| 514 | + | } else { | |
| 515 | + | r0 = ret.Get(0).(string) | |
| 516 | + | } | |
| 517 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) int); ok { | |
| 518 | + | r1 = returnFunc(ctx, containerID) | |
| 519 | + | } else { | |
| 520 | + | r1 = ret.Get(1).(int) | |
| 521 | + | } | |
| 522 | + | if returnFunc, ok := ret.Get(2).(func(context.Context, string) error); ok { | |
| 523 | + | r2 = returnFunc(ctx, containerID) | |
| 524 | + | } else { | |
| 525 | + | r2 = ret.Error(2) | |
| 526 | + | } | |
| 527 | + | return r0, r1, r2 | |
| 528 | + | } | |
| 529 | + | ||
| 530 | + | // MockContainerRuntime_GetContainerNetworkInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerNetworkInfo' | |
| 531 | + | type MockContainerRuntime_GetContainerNetworkInfo_Call struct { | |
| 532 | + | *mock.Call | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | // GetContainerNetworkInfo is a helper method to define mock.On call | |
| 536 | + | // - ctx context.Context | |
| 537 | + | // - containerID string | |
| 538 | + | func (_e *MockContainerRuntime_Expecter) GetContainerNetworkInfo(ctx interface{}, containerID interface{}) *MockContainerRuntime_GetContainerNetworkInfo_Call { | |
| 539 | + | return &MockContainerRuntime_GetContainerNetworkInfo_Call{Call: _e.mock.On("GetContainerNetworkInfo", ctx, containerID)} | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | func (_c *MockContainerRuntime_GetContainerNetworkInfo_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_GetContainerNetworkInfo_Call { | |
| 543 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 544 | + | var arg0 context.Context | |
| 545 | + | if args[0] != nil { | |
| 546 | + | arg0 = args[0].(context.Context) | |
| 547 | + | } | |
| 548 | + | var arg1 string | |
| 549 | + | if args[1] != nil { | |
| 550 | + | arg1 = args[1].(string) | |
| 551 | + | } | |
| 552 | + | run( | |
| 553 | + | arg0, | |
| 554 | + | arg1, | |
| 555 | + | ) | |
| 556 | + | }) | |
| 557 | + | return _c | |
| 558 | + | } | |
| 559 | + | ||
| 560 | + | func (_c *MockContainerRuntime_GetContainerNetworkInfo_Call) Return(s string, n int, err error) *MockContainerRuntime_GetContainerNetworkInfo_Call { | |
| 561 | + | _c.Call.Return(s, n, err) | |
| 562 | + | return _c | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | func (_c *MockContainerRuntime_GetContainerNetworkInfo_Call) RunAndReturn(run func(ctx context.Context, containerID string) (string, int, error)) *MockContainerRuntime_GetContainerNetworkInfo_Call { | |
| 566 | + | _c.Call.Return(run) | |
| 567 | + | return _c | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | // GetContainerPort provides a mock function for the type MockContainerRuntime | |
| 571 | + | func (_mock *MockContainerRuntime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) { | |
| 572 | + | ret := _mock.Called(ctx, containerID, internalPort) | |
| 573 | + | ||
| 574 | + | if len(ret) == 0 { | |
| 575 | + | panic("no return value specified for GetContainerPort") | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | var r0 int | |
| 579 | + | var r1 error | |
| 580 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) (int, error)); ok { | |
| 581 | + | return returnFunc(ctx, containerID, internalPort) | |
| 582 | + | } | |
| 583 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) int); ok { | |
| 584 | + | r0 = returnFunc(ctx, containerID, internalPort) | |
| 585 | + | } else { | |
| 586 | + | r0 = ret.Get(0).(int) | |
| 587 | + | } | |
| 588 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, int) error); ok { | |
| 589 | + | r1 = returnFunc(ctx, containerID, internalPort) | |
| 590 | + | } else { | |
| 591 | + | r1 = ret.Error(1) | |
| 592 | + | } | |
| 593 | + | return r0, r1 | |
| 594 | + | } | |
| 595 | + | ||
| 596 | + | // MockContainerRuntime_GetContainerPort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerPort' | |
| 597 | + | type MockContainerRuntime_GetContainerPort_Call struct { | |
| 598 | + | *mock.Call | |
| 599 | + | } | |
| 600 | + | ||
| 601 | + | // GetContainerPort is a helper method to define mock.On call | |
| 602 | + | // - ctx context.Context | |
| 603 | + | // - containerID string | |
| 604 | + | // - internalPort int | |
| 605 | + | func (_e *MockContainerRuntime_Expecter) GetContainerPort(ctx interface{}, containerID interface{}, internalPort interface{}) *MockContainerRuntime_GetContainerPort_Call { | |
| 606 | + | return &MockContainerRuntime_GetContainerPort_Call{Call: _e.mock.On("GetContainerPort", ctx, containerID, internalPort)} | |
| 607 | + | } | |
| 608 | + | ||
| 609 | + | func (_c *MockContainerRuntime_GetContainerPort_Call) Run(run func(ctx context.Context, containerID string, internalPort int)) *MockContainerRuntime_GetContainerPort_Call { | |
| 610 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 611 | + | var arg0 context.Context | |
| 612 | + | if args[0] != nil { | |
| 613 | + | arg0 = args[0].(context.Context) | |
| 614 | + | } | |
| 615 | + | var arg1 string | |
| 616 | + | if args[1] != nil { | |
| 617 | + | arg1 = args[1].(string) | |
| 618 | + | } | |
| 619 | + | var arg2 int | |
| 620 | + | if args[2] != nil { | |
| 621 | + | arg2 = args[2].(int) | |
| 622 | + | } | |
| 623 | + | run( | |
| 624 | + | arg0, | |
| 625 | + | arg1, | |
| 626 | + | arg2, | |
| 627 | + | ) | |
| 628 | + | }) | |
| 629 | + | return _c | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | func (_c *MockContainerRuntime_GetContainerPort_Call) Return(n int, err error) *MockContainerRuntime_GetContainerPort_Call { | |
| 633 | + | _c.Call.Return(n, err) | |
| 634 | + | return _c | |
| 635 | + | } | |
| 636 | + | ||
| 637 | + | func (_c *MockContainerRuntime_GetContainerPort_Call) RunAndReturn(run func(ctx context.Context, containerID string, internalPort int) (int, error)) *MockContainerRuntime_GetContainerPort_Call { | |
| 638 | + | _c.Call.Return(run) | |
| 639 | + | return _c | |
| 640 | + | } | |
| 641 | + | ||
| 642 | + | // GetImageExposedPorts provides a mock function for the type MockContainerRuntime | |
| 643 | + | func (_mock *MockContainerRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { | |
| 644 | + | ret := _mock.Called(ctx, imageRef) | |
| 645 | + | ||
| 646 | + | if len(ret) == 0 { | |
| 647 | + | panic("no return value specified for GetImageExposedPorts") | |
| 648 | + | } | |
| 649 | + | ||
| 650 | + | var r0 []int | |
| 651 | + | var r1 error | |
| 652 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]int, error)); ok { | |
| 653 | + | return returnFunc(ctx, imageRef) | |
| 654 | + | } | |
| 655 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []int); ok { | |
| 656 | + | r0 = returnFunc(ctx, imageRef) | |
| 657 | + | } else { | |
| 658 | + | if ret.Get(0) != nil { | |
| 659 | + | r0 = ret.Get(0).([]int) | |
| 660 | + | } | |
| 661 | + | } | |
| 662 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 663 | + | r1 = returnFunc(ctx, imageRef) | |
| 664 | + | } else { | |
| 665 | + | r1 = ret.Error(1) | |
| 666 | + | } | |
| 667 | + | return r0, r1 | |
| 668 | + | } | |
| 669 | + | ||
| 670 | + | // MockContainerRuntime_GetImageExposedPorts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetImageExposedPorts' | |
| 671 | + | type MockContainerRuntime_GetImageExposedPorts_Call struct { | |
| 672 | + | *mock.Call | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | // GetImageExposedPorts is a helper method to define mock.On call | |
| 676 | + | // - ctx context.Context | |
| 677 | + | // - imageRef string | |
| 678 | + | func (_e *MockContainerRuntime_Expecter) GetImageExposedPorts(ctx interface{}, imageRef interface{}) *MockContainerRuntime_GetImageExposedPorts_Call { | |
| 679 | + | return &MockContainerRuntime_GetImageExposedPorts_Call{Call: _e.mock.On("GetImageExposedPorts", ctx, imageRef)} | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | func (_c *MockContainerRuntime_GetImageExposedPorts_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_GetImageExposedPorts_Call { | |
| 683 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 684 | + | var arg0 context.Context | |
| 685 | + | if args[0] != nil { | |
| 686 | + | arg0 = args[0].(context.Context) | |
| 687 | + | } | |
| 688 | + | var arg1 string | |
| 689 | + | if args[1] != nil { | |
| 690 | + | arg1 = args[1].(string) | |
| 691 | + | } | |
| 692 | + | run( | |
| 693 | + | arg0, | |
| 694 | + | arg1, | |
| 695 | + | ) | |
| 696 | + | }) | |
| 697 | + | return _c | |
| 698 | + | } | |
| 699 | + | ||
| 700 | + | func (_c *MockContainerRuntime_GetImageExposedPorts_Call) Return(ints []int, err error) *MockContainerRuntime_GetImageExposedPorts_Call { | |
| 701 | + | _c.Call.Return(ints, err) | |
| 702 | + | return _c | |
| 703 | + | } | |
| 704 | + | ||
| 705 | + | func (_c *MockContainerRuntime_GetImageExposedPorts_Call) RunAndReturn(run func(ctx context.Context, imageRef string) ([]int, error)) *MockContainerRuntime_GetImageExposedPorts_Call { | |
| 706 | + | _c.Call.Return(run) | |
| 707 | + | return _c | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | // InspectContainer provides a mock function for the type MockContainerRuntime | |
| 711 | + | func (_mock *MockContainerRuntime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) { | |
| 712 | + | ret := _mock.Called(ctx, containerID) | |
| 713 | + | ||
| 714 | + | if len(ret) == 0 { | |
| 715 | + | panic("no return value specified for InspectContainer") | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | var r0 *domain.Container | |
| 719 | + | var r1 error | |
| 720 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.Container, error)); ok { | |
| 721 | + | return returnFunc(ctx, containerID) | |
| 722 | + | } | |
| 723 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.Container); ok { | |
| 724 | + | r0 = returnFunc(ctx, containerID) | |
| 725 | + | } else { | |
| 726 | + | if ret.Get(0) != nil { | |
| 727 | + | r0 = ret.Get(0).(*domain.Container) | |
| 728 | + | } | |
| 729 | + | } | |
| 730 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 731 | + | r1 = returnFunc(ctx, containerID) | |
| 732 | + | } else { | |
| 733 | + | r1 = ret.Error(1) | |
| 734 | + | } | |
| 735 | + | return r0, r1 | |
| 736 | + | } | |
| 737 | + | ||
| 738 | + | // MockContainerRuntime_InspectContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InspectContainer' | |
| 739 | + | type MockContainerRuntime_InspectContainer_Call struct { | |
| 740 | + | *mock.Call | |
| 741 | + | } | |
| 742 | + | ||
| 743 | + | // InspectContainer is a helper method to define mock.On call | |
| 744 | + | // - ctx context.Context | |
| 745 | + | // - containerID string | |
| 746 | + | func (_e *MockContainerRuntime_Expecter) InspectContainer(ctx interface{}, containerID interface{}) *MockContainerRuntime_InspectContainer_Call { | |
| 747 | + | return &MockContainerRuntime_InspectContainer_Call{Call: _e.mock.On("InspectContainer", ctx, containerID)} | |
| 748 | + | } | |
| 749 | + | ||
| 750 | + | func (_c *MockContainerRuntime_InspectContainer_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_InspectContainer_Call { | |
| 751 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 752 | + | var arg0 context.Context | |
| 753 | + | if args[0] != nil { | |
| 754 | + | arg0 = args[0].(context.Context) | |
| 755 | + | } | |
| 756 | + | var arg1 string | |
| 757 | + | if args[1] != nil { | |
| 758 | + | arg1 = args[1].(string) | |
| 759 | + | } | |
| 760 | + | run( | |
| 761 | + | arg0, | |
| 762 | + | arg1, | |
| 763 | + | ) | |
| 764 | + | }) | |
| 765 | + | return _c | |
| 766 | + | } | |
| 767 | + | ||
| 768 | + | func (_c *MockContainerRuntime_InspectContainer_Call) Return(container *domain.Container, err error) *MockContainerRuntime_InspectContainer_Call { | |
| 769 | + | _c.Call.Return(container, err) | |
| 770 | + | return _c | |
| 771 | + | } | |
| 772 | + | ||
| 773 | + | func (_c *MockContainerRuntime_InspectContainer_Call) RunAndReturn(run func(ctx context.Context, containerID string) (*domain.Container, error)) *MockContainerRuntime_InspectContainer_Call { | |
| 774 | + | _c.Call.Return(run) | |
| 775 | + | return _c | |
| 776 | + | } | |
| 777 | + | ||
| 778 | + | // InspectImageEnv provides a mock function for the type MockContainerRuntime | |
| 779 | + | func (_mock *MockContainerRuntime) InspectImageEnv(ctx context.Context, imageRef string) ([]string, error) { | |
| 780 | + | ret := _mock.Called(ctx, imageRef) | |
| 781 | + | ||
| 782 | + | if len(ret) == 0 { | |
| 783 | + | panic("no return value specified for InspectImageEnv") | |
| 784 | + | } | |
| 785 | + | ||
| 786 | + | var r0 []string | |
| 787 | + | var r1 error | |
| 788 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |
| 789 | + | return returnFunc(ctx, imageRef) | |
| 790 | + | } | |
| 791 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |
| 792 | + | r0 = returnFunc(ctx, imageRef) | |
| 793 | + | } else { | |
| 794 | + | if ret.Get(0) != nil { | |
| 795 | + | r0 = ret.Get(0).([]string) | |
| 796 | + | } | |
| 797 | + | } | |
| 798 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 799 | + | r1 = returnFunc(ctx, imageRef) | |
| 800 | + | } else { | |
| 801 | + | r1 = ret.Error(1) | |
| 802 | + | } | |
| 803 | + | return r0, r1 | |
| 804 | + | } | |
| 805 | + | ||
| 806 | + | // MockContainerRuntime_InspectImageEnv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InspectImageEnv' | |
| 807 | + | type MockContainerRuntime_InspectImageEnv_Call struct { | |
| 808 | + | *mock.Call | |
| 809 | + | } | |
| 810 | + | ||
| 811 | + | // InspectImageEnv is a helper method to define mock.On call | |
| 812 | + | // - ctx context.Context | |
| 813 | + | // - imageRef string | |
| 814 | + | func (_e *MockContainerRuntime_Expecter) InspectImageEnv(ctx interface{}, imageRef interface{}) *MockContainerRuntime_InspectImageEnv_Call { | |
| 815 | + | return &MockContainerRuntime_InspectImageEnv_Call{Call: _e.mock.On("InspectImageEnv", ctx, imageRef)} | |
| 816 | + | } | |
| 817 | + | ||
| 818 | + | func (_c *MockContainerRuntime_InspectImageEnv_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_InspectImageEnv_Call { | |
| 819 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 820 | + | var arg0 context.Context | |
| 821 | + | if args[0] != nil { | |
| 822 | + | arg0 = args[0].(context.Context) | |
| 823 | + | } | |
| 824 | + | var arg1 string | |
| 825 | + | if args[1] != nil { | |
| 826 | + | arg1 = args[1].(string) | |
| 827 | + | } | |
| 828 | + | run( | |
| 829 | + | arg0, | |
| 830 | + | arg1, | |
| 831 | + | ) | |
| 832 | + | }) | |
| 833 | + | return _c | |
| 834 | + | } | |
| 835 | + | ||
| 836 | + | func (_c *MockContainerRuntime_InspectImageEnv_Call) Return(strings []string, err error) *MockContainerRuntime_InspectImageEnv_Call { | |
| 837 | + | _c.Call.Return(strings, err) | |
| 838 | + | return _c | |
| 839 | + | } | |
| 840 | + | ||
| 841 | + | func (_c *MockContainerRuntime_InspectImageEnv_Call) RunAndReturn(run func(ctx context.Context, imageRef string) ([]string, error)) *MockContainerRuntime_InspectImageEnv_Call { | |
| 842 | + | _c.Call.Return(run) | |
| 843 | + | return _c | |
| 844 | + | } | |
| 845 | + | ||
| 846 | + | // InspectImageVolumes provides a mock function for the type MockContainerRuntime | |
| 847 | + | func (_mock *MockContainerRuntime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | |
| 848 | + | ret := _mock.Called(ctx, imageRef) | |
| 849 | + | ||
| 850 | + | if len(ret) == 0 { | |
| 851 | + | panic("no return value specified for InspectImageVolumes") | |
| 852 | + | } | |
| 853 | + | ||
| 854 | + | var r0 []string | |
| 855 | + | var r1 error | |
| 856 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |
| 857 | + | return returnFunc(ctx, imageRef) | |
| 858 | + | } | |
| 859 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |
| 860 | + | r0 = returnFunc(ctx, imageRef) | |
| 861 | + | } else { | |
| 862 | + | if ret.Get(0) != nil { | |
| 863 | + | r0 = ret.Get(0).([]string) | |
| 864 | + | } | |
| 865 | + | } | |
| 866 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 867 | + | r1 = returnFunc(ctx, imageRef) | |
| 868 | + | } else { | |
| 869 | + | r1 = ret.Error(1) | |
| 870 | + | } | |
| 871 | + | return r0, r1 | |
| 872 | + | } | |
| 873 | + | ||
| 874 | + | // MockContainerRuntime_InspectImageVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InspectImageVolumes' | |
| 875 | + | type MockContainerRuntime_InspectImageVolumes_Call struct { | |
| 876 | + | *mock.Call | |
| 877 | + | } | |
| 878 | + | ||
| 879 | + | // InspectImageVolumes is a helper method to define mock.On call | |
| 880 | + | // - ctx context.Context | |
| 881 | + | // - imageRef string | |
| 882 | + | func (_e *MockContainerRuntime_Expecter) InspectImageVolumes(ctx interface{}, imageRef interface{}) *MockContainerRuntime_InspectImageVolumes_Call { | |
| 883 | + | return &MockContainerRuntime_InspectImageVolumes_Call{Call: _e.mock.On("InspectImageVolumes", ctx, imageRef)} | |
| 884 | + | } | |
| 885 | + | ||
| 886 | + | func (_c *MockContainerRuntime_InspectImageVolumes_Call) Run(run func(ctx context.Context, imageRef string)) *MockContainerRuntime_InspectImageVolumes_Call { | |
| 887 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 888 | + | var arg0 context.Context | |
| 889 | + | if args[0] != nil { | |
| 890 | + | arg0 = args[0].(context.Context) | |
| 891 | + | } | |
| 892 | + | var arg1 string | |
| 893 | + | if args[1] != nil { | |
| 894 | + | arg1 = args[1].(string) | |
| 895 | + | } | |
| 896 | + | run( | |
| 897 | + | arg0, | |
| 898 | + | arg1, | |
| 899 | + | ) | |
| 900 | + | }) | |
| 901 | + | return _c | |
| 902 | + | } | |
| 903 | + | ||
| 904 | + | func (_c *MockContainerRuntime_InspectImageVolumes_Call) Return(strings []string, err error) *MockContainerRuntime_InspectImageVolumes_Call { | |
| 905 | + | _c.Call.Return(strings, err) | |
| 906 | + | return _c | |
| 907 | + | } | |
| 908 | + | ||
| 909 | + | func (_c *MockContainerRuntime_InspectImageVolumes_Call) RunAndReturn(run func(ctx context.Context, imageRef string) ([]string, error)) *MockContainerRuntime_InspectImageVolumes_Call { | |
| 910 | + | _c.Call.Return(run) | |
| 911 | + | return _c | |
| 912 | + | } | |
| 913 | + | ||
| 914 | + | // IsContainerRunning provides a mock function for the type MockContainerRuntime | |
| 915 | + | func (_mock *MockContainerRuntime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) { | |
| 916 | + | ret := _mock.Called(ctx, containerID) | |
| 917 | + | ||
| 918 | + | if len(ret) == 0 { | |
| 919 | + | panic("no return value specified for IsContainerRunning") | |
| 920 | + | } | |
| 921 | + | ||
| 922 | + | var r0 bool | |
| 923 | + | var r1 error | |
| 924 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { | |
| 925 | + | return returnFunc(ctx, containerID) | |
| 926 | + | } | |
| 927 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok { | |
| 928 | + | r0 = returnFunc(ctx, containerID) | |
| 929 | + | } else { | |
| 930 | + | r0 = ret.Get(0).(bool) | |
| 931 | + | } | |
| 932 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 933 | + | r1 = returnFunc(ctx, containerID) | |
| 934 | + | } else { | |
| 935 | + | r1 = ret.Error(1) | |
| 936 | + | } | |
| 937 | + | return r0, r1 | |
| 938 | + | } | |
| 939 | + | ||
| 940 | + | // MockContainerRuntime_IsContainerRunning_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsContainerRunning' | |
| 941 | + | type MockContainerRuntime_IsContainerRunning_Call struct { | |
| 942 | + | *mock.Call | |
| 943 | + | } | |
| 944 | + | ||
| 945 | + | // IsContainerRunning is a helper method to define mock.On call | |
| 946 | + | // - ctx context.Context | |
| 947 | + | // - containerID string | |
| 948 | + | func (_e *MockContainerRuntime_Expecter) IsContainerRunning(ctx interface{}, containerID interface{}) *MockContainerRuntime_IsContainerRunning_Call { | |
| 949 | + | return &MockContainerRuntime_IsContainerRunning_Call{Call: _e.mock.On("IsContainerRunning", ctx, containerID)} | |
| 950 | + | } | |
| 951 | + | ||
| 952 | + | func (_c *MockContainerRuntime_IsContainerRunning_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_IsContainerRunning_Call { | |
| 953 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 954 | + | var arg0 context.Context | |
| 955 | + | if args[0] != nil { | |
| 956 | + | arg0 = args[0].(context.Context) | |
| 957 | + | } | |
| 958 | + | var arg1 string | |
| 959 | + | if args[1] != nil { | |
| 960 | + | arg1 = args[1].(string) | |
| 961 | + | } | |
| 962 | + | run( | |
| 963 | + | arg0, | |
| 964 | + | arg1, | |
| 965 | + | ) | |
| 966 | + | }) | |
| 967 | + | return _c | |
| 968 | + | } | |
| 969 | + | ||
| 970 | + | func (_c *MockContainerRuntime_IsContainerRunning_Call) Return(b bool, err error) *MockContainerRuntime_IsContainerRunning_Call { | |
| 971 | + | _c.Call.Return(b, err) | |
| 972 | + | return _c | |
| 973 | + | } | |
| 974 | + | ||
| 975 | + | func (_c *MockContainerRuntime_IsContainerRunning_Call) RunAndReturn(run func(ctx context.Context, containerID string) (bool, error)) *MockContainerRuntime_IsContainerRunning_Call { | |
| 976 | + | _c.Call.Return(run) | |
| 977 | + | return _c | |
| 978 | + | } | |
| 979 | + | ||
| 980 | + | // ListContainers provides a mock function for the type MockContainerRuntime | |
| 981 | + | func (_mock *MockContainerRuntime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) { | |
| 982 | + | ret := _mock.Called(ctx, all) | |
| 983 | + | ||
| 984 | + | if len(ret) == 0 { | |
| 985 | + | panic("no return value specified for ListContainers") | |
| 986 | + | } | |
| 987 | + | ||
| 988 | + | var r0 []*domain.Container | |
| 989 | + | var r1 error | |
| 990 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, bool) ([]*domain.Container, error)); ok { | |
| 991 | + | return returnFunc(ctx, all) | |
| 992 | + | } | |
| 993 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, bool) []*domain.Container); ok { | |
| 994 | + | r0 = returnFunc(ctx, all) | |
| 995 | + | } else { | |
| 996 | + | if ret.Get(0) != nil { | |
| 997 | + | r0 = ret.Get(0).([]*domain.Container) | |
| 998 | + | } | |
| 999 | + | } | |
| 1000 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, bool) error); ok { | |
| 1001 | + | r1 = returnFunc(ctx, all) | |
| 1002 | + | } else { | |
| 1003 | + | r1 = ret.Error(1) | |
| 1004 | + | } | |
| 1005 | + | return r0, r1 | |
| 1006 | + | } | |
| 1007 | + | ||
| 1008 | + | // MockContainerRuntime_ListContainers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListContainers' | |
| 1009 | + | type MockContainerRuntime_ListContainers_Call struct { | |
| 1010 | + | *mock.Call | |
| 1011 | + | } | |
| 1012 | + | ||
| 1013 | + | // ListContainers is a helper method to define mock.On call | |
| 1014 | + | // - ctx context.Context | |
| 1015 | + | // - all bool | |
| 1016 | + | func (_e *MockContainerRuntime_Expecter) ListContainers(ctx interface{}, all interface{}) *MockContainerRuntime_ListContainers_Call { | |
| 1017 | + | return &MockContainerRuntime_ListContainers_Call{Call: _e.mock.On("ListContainers", ctx, all)} | |
| 1018 | + | } | |
| 1019 | + | ||
| 1020 | + | func (_c *MockContainerRuntime_ListContainers_Call) Run(run func(ctx context.Context, all bool)) *MockContainerRuntime_ListContainers_Call { | |
| 1021 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1022 | + | var arg0 context.Context | |
| 1023 | + | if args[0] != nil { | |
| 1024 | + | arg0 = args[0].(context.Context) | |
| 1025 | + | } | |
| 1026 | + | var arg1 bool | |
| 1027 | + | if args[1] != nil { | |
| 1028 | + | arg1 = args[1].(bool) | |
| 1029 | + | } | |
| 1030 | + | run( | |
| 1031 | + | arg0, | |
| 1032 | + | arg1, | |
| 1033 | + | ) | |
| 1034 | + | }) | |
| 1035 | + | return _c | |
| 1036 | + | } | |
| 1037 | + | ||
| 1038 | + | func (_c *MockContainerRuntime_ListContainers_Call) Return(containers []*domain.Container, err error) *MockContainerRuntime_ListContainers_Call { | |
| 1039 | + | _c.Call.Return(containers, err) | |
| 1040 | + | return _c | |
| 1041 | + | } | |
| 1042 | + | ||
| 1043 | + | func (_c *MockContainerRuntime_ListContainers_Call) RunAndReturn(run func(ctx context.Context, all bool) ([]*domain.Container, error)) *MockContainerRuntime_ListContainers_Call { | |
| 1044 | + | _c.Call.Return(run) | |
| 1045 | + | return _c | |
| 1046 | + | } | |
| 1047 | + | ||
| 1048 | + | // ListImages provides a mock function for the type MockContainerRuntime | |
| 1049 | + | func (_mock *MockContainerRuntime) ListImages(ctx context.Context) ([]string, error) { | |
| 1050 | + | ret := _mock.Called(ctx) | |
| 1051 | + | ||
| 1052 | + | if len(ret) == 0 { | |
| 1053 | + | panic("no return value specified for ListImages") | |
| 1054 | + | } | |
| 1055 | + | ||
| 1056 | + | var r0 []string | |
| 1057 | + | var r1 error | |
| 1058 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { | |
| 1059 | + | return returnFunc(ctx) | |
| 1060 | + | } | |
| 1061 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { | |
| 1062 | + | r0 = returnFunc(ctx) | |
| 1063 | + | } else { | |
| 1064 | + | if ret.Get(0) != nil { | |
| 1065 | + | r0 = ret.Get(0).([]string) | |
| 1066 | + | } | |
| 1067 | + | } | |
| 1068 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 1069 | + | r1 = returnFunc(ctx) | |
| 1070 | + | } else { | |
| 1071 | + | r1 = ret.Error(1) | |
| 1072 | + | } | |
| 1073 | + | return r0, r1 | |
| 1074 | + | } | |
| 1075 | + | ||
| 1076 | + | // MockContainerRuntime_ListImages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListImages' | |
| 1077 | + | type MockContainerRuntime_ListImages_Call struct { | |
| 1078 | + | *mock.Call | |
| 1079 | + | } | |
| 1080 | + | ||
| 1081 | + | // ListImages is a helper method to define mock.On call | |
| 1082 | + | // - ctx context.Context | |
| 1083 | + | func (_e *MockContainerRuntime_Expecter) ListImages(ctx interface{}) *MockContainerRuntime_ListImages_Call { | |
| 1084 | + | return &MockContainerRuntime_ListImages_Call{Call: _e.mock.On("ListImages", ctx)} | |
| 1085 | + | } | |
| 1086 | + | ||
| 1087 | + | func (_c *MockContainerRuntime_ListImages_Call) Run(run func(ctx context.Context)) *MockContainerRuntime_ListImages_Call { | |
| 1088 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1089 | + | var arg0 context.Context | |
| 1090 | + | if args[0] != nil { | |
| 1091 | + | arg0 = args[0].(context.Context) | |
| 1092 | + | } | |
| 1093 | + | run( | |
| 1094 | + | arg0, | |
| 1095 | + | ) | |
| 1096 | + | }) | |
| 1097 | + | return _c | |
| 1098 | + | } | |
| 1099 | + | ||
| 1100 | + | func (_c *MockContainerRuntime_ListImages_Call) Return(strings []string, err error) *MockContainerRuntime_ListImages_Call { | |
| 1101 | + | _c.Call.Return(strings, err) | |
| 1102 | + | return _c | |
| 1103 | + | } | |
| 1104 | + | ||
| 1105 | + | func (_c *MockContainerRuntime_ListImages_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *MockContainerRuntime_ListImages_Call { | |
| 1106 | + | _c.Call.Return(run) | |
| 1107 | + | return _c | |
| 1108 | + | } | |
| 1109 | + | ||
| 1110 | + | // ListNetworks provides a mock function for the type MockContainerRuntime | |
| 1111 | + | func (_mock *MockContainerRuntime) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 1112 | + | ret := _mock.Called(ctx) | |
| 1113 | + | ||
| 1114 | + | if len(ret) == 0 { | |
| 1115 | + | panic("no return value specified for ListNetworks") | |
| 1116 | + | } | |
| 1117 | + | ||
| 1118 | + | var r0 []*domain.NetworkInfo | |
| 1119 | + | var r1 error | |
| 1120 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*domain.NetworkInfo, error)); ok { | |
| 1121 | + | return returnFunc(ctx) | |
| 1122 | + | } | |
| 1123 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*domain.NetworkInfo); ok { | |
| 1124 | + | r0 = returnFunc(ctx) | |
| 1125 | + | } else { | |
| 1126 | + | if ret.Get(0) != nil { | |
| 1127 | + | r0 = ret.Get(0).([]*domain.NetworkInfo) | |
| 1128 | + | } | |
| 1129 | + | } | |
| 1130 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 1131 | + | r1 = returnFunc(ctx) | |
| 1132 | + | } else { | |
| 1133 | + | r1 = ret.Error(1) | |
| 1134 | + | } | |
| 1135 | + | return r0, r1 | |
| 1136 | + | } | |
| 1137 | + | ||
| 1138 | + | // MockContainerRuntime_ListNetworks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListNetworks' | |
| 1139 | + | type MockContainerRuntime_ListNetworks_Call struct { | |
| 1140 | + | *mock.Call | |
| 1141 | + | } | |
| 1142 | + | ||
| 1143 | + | // ListNetworks is a helper method to define mock.On call | |
| 1144 | + | // - ctx context.Context | |
| 1145 | + | func (_e *MockContainerRuntime_Expecter) ListNetworks(ctx interface{}) *MockContainerRuntime_ListNetworks_Call { | |
| 1146 | + | return &MockContainerRuntime_ListNetworks_Call{Call: _e.mock.On("ListNetworks", ctx)} | |
| 1147 | + | } | |
| 1148 | + | ||
| 1149 | + | func (_c *MockContainerRuntime_ListNetworks_Call) Run(run func(ctx context.Context)) *MockContainerRuntime_ListNetworks_Call { | |
| 1150 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1151 | + | var arg0 context.Context | |
| 1152 | + | if args[0] != nil { | |
| 1153 | + | arg0 = args[0].(context.Context) | |
| 1154 | + | } | |
| 1155 | + | run( | |
| 1156 | + | arg0, | |
| 1157 | + | ) | |
| 1158 | + | }) | |
| 1159 | + | return _c | |
| 1160 | + | } | |
| 1161 | + | ||
| 1162 | + | func (_c *MockContainerRuntime_ListNetworks_Call) Return(networkInfos []*domain.NetworkInfo, err error) *MockContainerRuntime_ListNetworks_Call { | |
| 1163 | + | _c.Call.Return(networkInfos, err) | |
| 1164 | + | return _c | |
| 1165 | + | } | |
| 1166 | + | ||
| 1167 | + | func (_c *MockContainerRuntime_ListNetworks_Call) RunAndReturn(run func(ctx context.Context) ([]*domain.NetworkInfo, error)) *MockContainerRuntime_ListNetworks_Call { | |
| 1168 | + | _c.Call.Return(run) | |
| 1169 | + | return _c | |
| 1170 | + | } | |
| 1171 | + | ||
| 1172 | + | // NetworkExists provides a mock function for the type MockContainerRuntime | |
| 1173 | + | func (_mock *MockContainerRuntime) NetworkExists(ctx context.Context, name string) (bool, error) { | |
| 1174 | + | ret := _mock.Called(ctx, name) | |
| 1175 | + | ||
| 1176 | + | if len(ret) == 0 { | |
| 1177 | + | panic("no return value specified for NetworkExists") | |
| 1178 | + | } | |
| 1179 | + | ||
| 1180 | + | var r0 bool | |
| 1181 | + | var r1 error | |
| 1182 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { | |
| 1183 | + | return returnFunc(ctx, name) | |
| 1184 | + | } | |
| 1185 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok { | |
| 1186 | + | r0 = returnFunc(ctx, name) | |
| 1187 | + | } else { | |
| 1188 | + | r0 = ret.Get(0).(bool) | |
| 1189 | + | } | |
| 1190 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 1191 | + | r1 = returnFunc(ctx, name) | |
| 1192 | + | } else { | |
| 1193 | + | r1 = ret.Error(1) | |
| 1194 | + | } | |
| 1195 | + | return r0, r1 | |
| 1196 | + | } | |
| 1197 | + | ||
| 1198 | + | // MockContainerRuntime_NetworkExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NetworkExists' | |
| 1199 | + | type MockContainerRuntime_NetworkExists_Call struct { | |
| 1200 | + | *mock.Call | |
| 1201 | + | } | |
| 1202 | + | ||
| 1203 | + | // NetworkExists is a helper method to define mock.On call | |
| 1204 | + | // - ctx context.Context | |
| 1205 | + | // - name string | |
| 1206 | + | func (_e *MockContainerRuntime_Expecter) NetworkExists(ctx interface{}, name interface{}) *MockContainerRuntime_NetworkExists_Call { | |
| 1207 | + | return &MockContainerRuntime_NetworkExists_Call{Call: _e.mock.On("NetworkExists", ctx, name)} | |
| 1208 | + | } | |
| 1209 | + | ||
| 1210 | + | func (_c *MockContainerRuntime_NetworkExists_Call) Run(run func(ctx context.Context, name string)) *MockContainerRuntime_NetworkExists_Call { | |
| 1211 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1212 | + | var arg0 context.Context | |
| 1213 | + | if args[0] != nil { | |
| 1214 | + | arg0 = args[0].(context.Context) | |
| 1215 | + | } | |
| 1216 | + | var arg1 string | |
| 1217 | + | if args[1] != nil { | |
| 1218 | + | arg1 = args[1].(string) | |
| 1219 | + | } | |
| 1220 | + | run( | |
| 1221 | + | arg0, | |
| 1222 | + | arg1, | |
| 1223 | + | ) | |
| 1224 | + | }) | |
| 1225 | + | return _c | |
| 1226 | + | } | |
| 1227 | + | ||
| 1228 | + | func (_c *MockContainerRuntime_NetworkExists_Call) Return(b bool, err error) *MockContainerRuntime_NetworkExists_Call { | |
| 1229 | + | _c.Call.Return(b, err) | |
| 1230 | + | return _c | |
| 1231 | + | } | |
| 1232 | + | ||
| 1233 | + | func (_c *MockContainerRuntime_NetworkExists_Call) RunAndReturn(run func(ctx context.Context, name string) (bool, error)) *MockContainerRuntime_NetworkExists_Call { | |
| 1234 | + | _c.Call.Return(run) | |
| 1235 | + | return _c | |
| 1236 | + | } | |
| 1237 | + | ||
| 1238 | + | // Ping provides a mock function for the type MockContainerRuntime | |
| 1239 | + | func (_mock *MockContainerRuntime) Ping(ctx context.Context) error { | |
| 1240 | + | ret := _mock.Called(ctx) | |
| 1241 | + | ||
| 1242 | + | if len(ret) == 0 { | |
| 1243 | + | panic("no return value specified for Ping") | |
| 1244 | + | } | |
| 1245 | + | ||
| 1246 | + | var r0 error | |
| 1247 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { | |
| 1248 | + | r0 = returnFunc(ctx) | |
| 1249 | + | } else { | |
| 1250 | + | r0 = ret.Error(0) | |
| 1251 | + | } | |
| 1252 | + | return r0 | |
| 1253 | + | } | |
| 1254 | + | ||
| 1255 | + | // MockContainerRuntime_Ping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ping' | |
| 1256 | + | type MockContainerRuntime_Ping_Call struct { | |
| 1257 | + | *mock.Call | |
| 1258 | + | } | |
| 1259 | + | ||
| 1260 | + | // Ping is a helper method to define mock.On call | |
| 1261 | + | // - ctx context.Context | |
| 1262 | + | func (_e *MockContainerRuntime_Expecter) Ping(ctx interface{}) *MockContainerRuntime_Ping_Call { | |
| 1263 | + | return &MockContainerRuntime_Ping_Call{Call: _e.mock.On("Ping", ctx)} | |
| 1264 | + | } | |
| 1265 | + | ||
| 1266 | + | func (_c *MockContainerRuntime_Ping_Call) Run(run func(ctx context.Context)) *MockContainerRuntime_Ping_Call { | |
| 1267 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1268 | + | var arg0 context.Context | |
| 1269 | + | if args[0] != nil { | |
| 1270 | + | arg0 = args[0].(context.Context) | |
| 1271 | + | } | |
| 1272 | + | run( | |
| 1273 | + | arg0, | |
| 1274 | + | ) | |
| 1275 | + | }) | |
| 1276 | + | return _c | |
| 1277 | + | } | |
| 1278 | + | ||
| 1279 | + | func (_c *MockContainerRuntime_Ping_Call) Return(err error) *MockContainerRuntime_Ping_Call { | |
| 1280 | + | _c.Call.Return(err) | |
| 1281 | + | return _c | |
| 1282 | + | } | |
| 1283 | + | ||
| 1284 | + | func (_c *MockContainerRuntime_Ping_Call) RunAndReturn(run func(ctx context.Context) error) *MockContainerRuntime_Ping_Call { | |
| 1285 | + | _c.Call.Return(run) | |
| 1286 | + | return _c | |
| 1287 | + | } | |
| 1288 | + | ||
| 1289 | + | // PullImage provides a mock function for the type MockContainerRuntime | |
| 1290 | + | func (_mock *MockContainerRuntime) PullImage(ctx context.Context, image string) error { | |
| 1291 | + | ret := _mock.Called(ctx, image) | |
| 1292 | + | ||
| 1293 | + | if len(ret) == 0 { | |
| 1294 | + | panic("no return value specified for PullImage") | |
| 1295 | + | } | |
| 1296 | + | ||
| 1297 | + | var r0 error | |
| 1298 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1299 | + | r0 = returnFunc(ctx, image) | |
| 1300 | + | } else { | |
| 1301 | + | r0 = ret.Error(0) | |
| 1302 | + | } | |
| 1303 | + | return r0 | |
| 1304 | + | } | |
| 1305 | + | ||
| 1306 | + | // MockContainerRuntime_PullImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PullImage' | |
| 1307 | + | type MockContainerRuntime_PullImage_Call struct { | |
| 1308 | + | *mock.Call | |
| 1309 | + | } | |
| 1310 | + | ||
| 1311 | + | // PullImage is a helper method to define mock.On call | |
| 1312 | + | // - ctx context.Context | |
| 1313 | + | // - image string | |
| 1314 | + | func (_e *MockContainerRuntime_Expecter) PullImage(ctx interface{}, image interface{}) *MockContainerRuntime_PullImage_Call { | |
| 1315 | + | return &MockContainerRuntime_PullImage_Call{Call: _e.mock.On("PullImage", ctx, image)} | |
| 1316 | + | } | |
| 1317 | + | ||
| 1318 | + | func (_c *MockContainerRuntime_PullImage_Call) Run(run func(ctx context.Context, image string)) *MockContainerRuntime_PullImage_Call { | |
| 1319 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1320 | + | var arg0 context.Context | |
| 1321 | + | if args[0] != nil { | |
| 1322 | + | arg0 = args[0].(context.Context) | |
| 1323 | + | } | |
| 1324 | + | var arg1 string | |
| 1325 | + | if args[1] != nil { | |
| 1326 | + | arg1 = args[1].(string) | |
| 1327 | + | } | |
| 1328 | + | run( | |
| 1329 | + | arg0, | |
| 1330 | + | arg1, | |
| 1331 | + | ) | |
| 1332 | + | }) | |
| 1333 | + | return _c | |
| 1334 | + | } | |
| 1335 | + | ||
| 1336 | + | func (_c *MockContainerRuntime_PullImage_Call) Return(err error) *MockContainerRuntime_PullImage_Call { | |
| 1337 | + | _c.Call.Return(err) | |
| 1338 | + | return _c | |
| 1339 | + | } | |
| 1340 | + | ||
| 1341 | + | func (_c *MockContainerRuntime_PullImage_Call) RunAndReturn(run func(ctx context.Context, image string) error) *MockContainerRuntime_PullImage_Call { | |
| 1342 | + | _c.Call.Return(run) | |
| 1343 | + | return _c | |
| 1344 | + | } | |
| 1345 | + | ||
| 1346 | + | // PullImageWithAuth provides a mock function for the type MockContainerRuntime | |
| 1347 | + | func (_mock *MockContainerRuntime) PullImageWithAuth(ctx context.Context, image string, username string, password string) error { | |
| 1348 | + | ret := _mock.Called(ctx, image, username, password) | |
| 1349 | + | ||
| 1350 | + | if len(ret) == 0 { | |
| 1351 | + | panic("no return value specified for PullImageWithAuth") | |
| 1352 | + | } | |
| 1353 | + | ||
| 1354 | + | var r0 error | |
| 1355 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { | |
| 1356 | + | r0 = returnFunc(ctx, image, username, password) | |
| 1357 | + | } else { | |
| 1358 | + | r0 = ret.Error(0) | |
| 1359 | + | } | |
| 1360 | + | return r0 | |
| 1361 | + | } | |
| 1362 | + | ||
| 1363 | + | // MockContainerRuntime_PullImageWithAuth_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PullImageWithAuth' | |
| 1364 | + | type MockContainerRuntime_PullImageWithAuth_Call struct { | |
| 1365 | + | *mock.Call | |
| 1366 | + | } | |
| 1367 | + | ||
| 1368 | + | // PullImageWithAuth is a helper method to define mock.On call | |
| 1369 | + | // - ctx context.Context | |
| 1370 | + | // - image string | |
| 1371 | + | // - username string | |
| 1372 | + | // - password string | |
| 1373 | + | func (_e *MockContainerRuntime_Expecter) PullImageWithAuth(ctx interface{}, image interface{}, username interface{}, password interface{}) *MockContainerRuntime_PullImageWithAuth_Call { | |
| 1374 | + | return &MockContainerRuntime_PullImageWithAuth_Call{Call: _e.mock.On("PullImageWithAuth", ctx, image, username, password)} | |
| 1375 | + | } | |
| 1376 | + | ||
| 1377 | + | func (_c *MockContainerRuntime_PullImageWithAuth_Call) Run(run func(ctx context.Context, image string, username string, password string)) *MockContainerRuntime_PullImageWithAuth_Call { | |
| 1378 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1379 | + | var arg0 context.Context | |
| 1380 | + | if args[0] != nil { | |
| 1381 | + | arg0 = args[0].(context.Context) | |
| 1382 | + | } | |
| 1383 | + | var arg1 string | |
| 1384 | + | if args[1] != nil { | |
| 1385 | + | arg1 = args[1].(string) | |
| 1386 | + | } | |
| 1387 | + | var arg2 string | |
| 1388 | + | if args[2] != nil { | |
| 1389 | + | arg2 = args[2].(string) | |
| 1390 | + | } | |
| 1391 | + | var arg3 string | |
| 1392 | + | if args[3] != nil { | |
| 1393 | + | arg3 = args[3].(string) | |
| 1394 | + | } | |
| 1395 | + | run( | |
| 1396 | + | arg0, | |
| 1397 | + | arg1, | |
| 1398 | + | arg2, | |
| 1399 | + | arg3, | |
| 1400 | + | ) | |
| 1401 | + | }) | |
| 1402 | + | return _c | |
| 1403 | + | } | |
| 1404 | + | ||
| 1405 | + | func (_c *MockContainerRuntime_PullImageWithAuth_Call) Return(err error) *MockContainerRuntime_PullImageWithAuth_Call { | |
| 1406 | + | _c.Call.Return(err) | |
| 1407 | + | return _c | |
| 1408 | + | } | |
| 1409 | + | ||
| 1410 | + | func (_c *MockContainerRuntime_PullImageWithAuth_Call) RunAndReturn(run func(ctx context.Context, image string, username string, password string) error) *MockContainerRuntime_PullImageWithAuth_Call { | |
| 1411 | + | _c.Call.Return(run) | |
| 1412 | + | return _c | |
| 1413 | + | } | |
| 1414 | + | ||
| 1415 | + | // RemoveContainer provides a mock function for the type MockContainerRuntime | |
| 1416 | + | func (_mock *MockContainerRuntime) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |
| 1417 | + | ret := _mock.Called(ctx, containerID, force) | |
| 1418 | + | ||
| 1419 | + | if len(ret) == 0 { | |
| 1420 | + | panic("no return value specified for RemoveContainer") | |
| 1421 | + | } | |
| 1422 | + | ||
| 1423 | + | var r0 error | |
| 1424 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { | |
| 1425 | + | r0 = returnFunc(ctx, containerID, force) | |
| 1426 | + | } else { | |
| 1427 | + | r0 = ret.Error(0) | |
| 1428 | + | } | |
| 1429 | + | return r0 | |
| 1430 | + | } | |
| 1431 | + | ||
| 1432 | + | // MockContainerRuntime_RemoveContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveContainer' | |
| 1433 | + | type MockContainerRuntime_RemoveContainer_Call struct { | |
| 1434 | + | *mock.Call | |
| 1435 | + | } | |
| 1436 | + | ||
| 1437 | + | // RemoveContainer is a helper method to define mock.On call | |
| 1438 | + | // - ctx context.Context | |
| 1439 | + | // - containerID string | |
| 1440 | + | // - force bool | |
| 1441 | + | func (_e *MockContainerRuntime_Expecter) RemoveContainer(ctx interface{}, containerID interface{}, force interface{}) *MockContainerRuntime_RemoveContainer_Call { | |
| 1442 | + | return &MockContainerRuntime_RemoveContainer_Call{Call: _e.mock.On("RemoveContainer", ctx, containerID, force)} | |
| 1443 | + | } | |
| 1444 | + | ||
| 1445 | + | func (_c *MockContainerRuntime_RemoveContainer_Call) Run(run func(ctx context.Context, containerID string, force bool)) *MockContainerRuntime_RemoveContainer_Call { | |
| 1446 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1447 | + | var arg0 context.Context | |
| 1448 | + | if args[0] != nil { | |
| 1449 | + | arg0 = args[0].(context.Context) | |
| 1450 | + | } | |
| 1451 | + | var arg1 string | |
| 1452 | + | if args[1] != nil { | |
| 1453 | + | arg1 = args[1].(string) | |
| 1454 | + | } | |
| 1455 | + | var arg2 bool | |
| 1456 | + | if args[2] != nil { | |
| 1457 | + | arg2 = args[2].(bool) | |
| 1458 | + | } | |
| 1459 | + | run( | |
| 1460 | + | arg0, | |
| 1461 | + | arg1, | |
| 1462 | + | arg2, | |
| 1463 | + | ) | |
| 1464 | + | }) | |
| 1465 | + | return _c | |
| 1466 | + | } | |
| 1467 | + | ||
| 1468 | + | func (_c *MockContainerRuntime_RemoveContainer_Call) Return(err error) *MockContainerRuntime_RemoveContainer_Call { | |
| 1469 | + | _c.Call.Return(err) | |
| 1470 | + | return _c | |
| 1471 | + | } | |
| 1472 | + | ||
| 1473 | + | func (_c *MockContainerRuntime_RemoveContainer_Call) RunAndReturn(run func(ctx context.Context, containerID string, force bool) error) *MockContainerRuntime_RemoveContainer_Call { | |
| 1474 | + | _c.Call.Return(run) | |
| 1475 | + | return _c | |
| 1476 | + | } | |
| 1477 | + | ||
| 1478 | + | // RemoveImage provides a mock function for the type MockContainerRuntime | |
| 1479 | + | func (_mock *MockContainerRuntime) RemoveImage(ctx context.Context, image string, force bool) error { | |
| 1480 | + | ret := _mock.Called(ctx, image, force) | |
| 1481 | + | ||
| 1482 | + | if len(ret) == 0 { | |
| 1483 | + | panic("no return value specified for RemoveImage") | |
| 1484 | + | } | |
| 1485 | + | ||
| 1486 | + | var r0 error | |
| 1487 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { | |
| 1488 | + | r0 = returnFunc(ctx, image, force) | |
| 1489 | + | } else { | |
| 1490 | + | r0 = ret.Error(0) | |
| 1491 | + | } | |
| 1492 | + | return r0 | |
| 1493 | + | } | |
| 1494 | + | ||
| 1495 | + | // MockContainerRuntime_RemoveImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveImage' | |
| 1496 | + | type MockContainerRuntime_RemoveImage_Call struct { | |
| 1497 | + | *mock.Call | |
| 1498 | + | } | |
| 1499 | + | ||
| 1500 | + | // RemoveImage is a helper method to define mock.On call | |
| 1501 | + | // - ctx context.Context | |
| 1502 | + | // - image string | |
| 1503 | + | // - force bool | |
| 1504 | + | func (_e *MockContainerRuntime_Expecter) RemoveImage(ctx interface{}, image interface{}, force interface{}) *MockContainerRuntime_RemoveImage_Call { | |
| 1505 | + | return &MockContainerRuntime_RemoveImage_Call{Call: _e.mock.On("RemoveImage", ctx, image, force)} | |
| 1506 | + | } | |
| 1507 | + | ||
| 1508 | + | func (_c *MockContainerRuntime_RemoveImage_Call) Run(run func(ctx context.Context, image string, force bool)) *MockContainerRuntime_RemoveImage_Call { | |
| 1509 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1510 | + | var arg0 context.Context | |
| 1511 | + | if args[0] != nil { | |
| 1512 | + | arg0 = args[0].(context.Context) | |
| 1513 | + | } | |
| 1514 | + | var arg1 string | |
| 1515 | + | if args[1] != nil { | |
| 1516 | + | arg1 = args[1].(string) | |
| 1517 | + | } | |
| 1518 | + | var arg2 bool | |
| 1519 | + | if args[2] != nil { | |
| 1520 | + | arg2 = args[2].(bool) | |
| 1521 | + | } | |
| 1522 | + | run( | |
| 1523 | + | arg0, | |
| 1524 | + | arg1, | |
| 1525 | + | arg2, | |
| 1526 | + | ) | |
| 1527 | + | }) | |
| 1528 | + | return _c | |
| 1529 | + | } | |
| 1530 | + | ||
| 1531 | + | func (_c *MockContainerRuntime_RemoveImage_Call) Return(err error) *MockContainerRuntime_RemoveImage_Call { | |
| 1532 | + | _c.Call.Return(err) | |
| 1533 | + | return _c | |
| 1534 | + | } | |
| 1535 | + | ||
| 1536 | + | func (_c *MockContainerRuntime_RemoveImage_Call) RunAndReturn(run func(ctx context.Context, image string, force bool) error) *MockContainerRuntime_RemoveImage_Call { | |
| 1537 | + | _c.Call.Return(run) | |
| 1538 | + | return _c | |
| 1539 | + | } | |
| 1540 | + | ||
| 1541 | + | // RemoveNetwork provides a mock function for the type MockContainerRuntime | |
| 1542 | + | func (_mock *MockContainerRuntime) RemoveNetwork(ctx context.Context, name string) error { | |
| 1543 | + | ret := _mock.Called(ctx, name) | |
| 1544 | + | ||
| 1545 | + | if len(ret) == 0 { | |
| 1546 | + | panic("no return value specified for RemoveNetwork") | |
| 1547 | + | } | |
| 1548 | + | ||
| 1549 | + | var r0 error | |
| 1550 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1551 | + | r0 = returnFunc(ctx, name) | |
| 1552 | + | } else { | |
| 1553 | + | r0 = ret.Error(0) | |
| 1554 | + | } | |
| 1555 | + | return r0 | |
| 1556 | + | } | |
| 1557 | + | ||
| 1558 | + | // MockContainerRuntime_RemoveNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveNetwork' | |
| 1559 | + | type MockContainerRuntime_RemoveNetwork_Call struct { | |
| 1560 | + | *mock.Call | |
| 1561 | + | } | |
| 1562 | + | ||
| 1563 | + | // RemoveNetwork is a helper method to define mock.On call | |
| 1564 | + | // - ctx context.Context | |
| 1565 | + | // - name string | |
| 1566 | + | func (_e *MockContainerRuntime_Expecter) RemoveNetwork(ctx interface{}, name interface{}) *MockContainerRuntime_RemoveNetwork_Call { | |
| 1567 | + | return &MockContainerRuntime_RemoveNetwork_Call{Call: _e.mock.On("RemoveNetwork", ctx, name)} | |
| 1568 | + | } | |
| 1569 | + | ||
| 1570 | + | func (_c *MockContainerRuntime_RemoveNetwork_Call) Run(run func(ctx context.Context, name string)) *MockContainerRuntime_RemoveNetwork_Call { | |
| 1571 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1572 | + | var arg0 context.Context | |
| 1573 | + | if args[0] != nil { | |
| 1574 | + | arg0 = args[0].(context.Context) | |
| 1575 | + | } | |
| 1576 | + | var arg1 string | |
| 1577 | + | if args[1] != nil { | |
| 1578 | + | arg1 = args[1].(string) | |
| 1579 | + | } | |
| 1580 | + | run( | |
| 1581 | + | arg0, | |
| 1582 | + | arg1, | |
| 1583 | + | ) | |
| 1584 | + | }) | |
| 1585 | + | return _c | |
| 1586 | + | } | |
| 1587 | + | ||
| 1588 | + | func (_c *MockContainerRuntime_RemoveNetwork_Call) Return(err error) *MockContainerRuntime_RemoveNetwork_Call { | |
| 1589 | + | _c.Call.Return(err) | |
| 1590 | + | return _c | |
| 1591 | + | } | |
| 1592 | + | ||
| 1593 | + | func (_c *MockContainerRuntime_RemoveNetwork_Call) RunAndReturn(run func(ctx context.Context, name string) error) *MockContainerRuntime_RemoveNetwork_Call { | |
| 1594 | + | _c.Call.Return(run) | |
| 1595 | + | return _c | |
| 1596 | + | } | |
| 1597 | + | ||
| 1598 | + | // RemoveVolume provides a mock function for the type MockContainerRuntime | |
| 1599 | + | func (_mock *MockContainerRuntime) RemoveVolume(ctx context.Context, volumeName string, force bool) error { | |
| 1600 | + | ret := _mock.Called(ctx, volumeName, force) | |
| 1601 | + | ||
| 1602 | + | if len(ret) == 0 { | |
| 1603 | + | panic("no return value specified for RemoveVolume") | |
| 1604 | + | } | |
| 1605 | + | ||
| 1606 | + | var r0 error | |
| 1607 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) error); ok { | |
| 1608 | + | r0 = returnFunc(ctx, volumeName, force) | |
| 1609 | + | } else { | |
| 1610 | + | r0 = ret.Error(0) | |
| 1611 | + | } | |
| 1612 | + | return r0 | |
| 1613 | + | } | |
| 1614 | + | ||
| 1615 | + | // MockContainerRuntime_RemoveVolume_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveVolume' | |
| 1616 | + | type MockContainerRuntime_RemoveVolume_Call struct { | |
| 1617 | + | *mock.Call | |
| 1618 | + | } | |
| 1619 | + | ||
| 1620 | + | // RemoveVolume is a helper method to define mock.On call | |
| 1621 | + | // - ctx context.Context | |
| 1622 | + | // - volumeName string | |
| 1623 | + | // - force bool | |
| 1624 | + | func (_e *MockContainerRuntime_Expecter) RemoveVolume(ctx interface{}, volumeName interface{}, force interface{}) *MockContainerRuntime_RemoveVolume_Call { | |
| 1625 | + | return &MockContainerRuntime_RemoveVolume_Call{Call: _e.mock.On("RemoveVolume", ctx, volumeName, force)} | |
| 1626 | + | } | |
| 1627 | + | ||
| 1628 | + | func (_c *MockContainerRuntime_RemoveVolume_Call) Run(run func(ctx context.Context, volumeName string, force bool)) *MockContainerRuntime_RemoveVolume_Call { | |
| 1629 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1630 | + | var arg0 context.Context | |
| 1631 | + | if args[0] != nil { | |
| 1632 | + | arg0 = args[0].(context.Context) | |
| 1633 | + | } | |
| 1634 | + | var arg1 string | |
| 1635 | + | if args[1] != nil { | |
| 1636 | + | arg1 = args[1].(string) | |
| 1637 | + | } | |
| 1638 | + | var arg2 bool | |
| 1639 | + | if args[2] != nil { | |
| 1640 | + | arg2 = args[2].(bool) | |
| 1641 | + | } | |
| 1642 | + | run( | |
| 1643 | + | arg0, | |
| 1644 | + | arg1, | |
| 1645 | + | arg2, | |
| 1646 | + | ) | |
| 1647 | + | }) | |
| 1648 | + | return _c | |
| 1649 | + | } | |
| 1650 | + | ||
| 1651 | + | func (_c *MockContainerRuntime_RemoveVolume_Call) Return(err error) *MockContainerRuntime_RemoveVolume_Call { | |
| 1652 | + | _c.Call.Return(err) | |
| 1653 | + | return _c | |
| 1654 | + | } | |
| 1655 | + | ||
| 1656 | + | func (_c *MockContainerRuntime_RemoveVolume_Call) RunAndReturn(run func(ctx context.Context, volumeName string, force bool) error) *MockContainerRuntime_RemoveVolume_Call { | |
| 1657 | + | _c.Call.Return(run) | |
| 1658 | + | return _c | |
| 1659 | + | } | |
| 1660 | + | ||
| 1661 | + | // RestartContainer provides a mock function for the type MockContainerRuntime | |
| 1662 | + | func (_mock *MockContainerRuntime) RestartContainer(ctx context.Context, containerID string) error { | |
| 1663 | + | ret := _mock.Called(ctx, containerID) | |
| 1664 | + | ||
| 1665 | + | if len(ret) == 0 { | |
| 1666 | + | panic("no return value specified for RestartContainer") | |
| 1667 | + | } | |
| 1668 | + | ||
| 1669 | + | var r0 error | |
| 1670 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1671 | + | r0 = returnFunc(ctx, containerID) | |
| 1672 | + | } else { | |
| 1673 | + | r0 = ret.Error(0) | |
| 1674 | + | } | |
| 1675 | + | return r0 | |
| 1676 | + | } | |
| 1677 | + | ||
| 1678 | + | // MockContainerRuntime_RestartContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartContainer' | |
| 1679 | + | type MockContainerRuntime_RestartContainer_Call struct { | |
| 1680 | + | *mock.Call | |
| 1681 | + | } | |
| 1682 | + | ||
| 1683 | + | // RestartContainer is a helper method to define mock.On call | |
| 1684 | + | // - ctx context.Context | |
| 1685 | + | // - containerID string | |
| 1686 | + | func (_e *MockContainerRuntime_Expecter) RestartContainer(ctx interface{}, containerID interface{}) *MockContainerRuntime_RestartContainer_Call { | |
| 1687 | + | return &MockContainerRuntime_RestartContainer_Call{Call: _e.mock.On("RestartContainer", ctx, containerID)} | |
| 1688 | + | } | |
| 1689 | + | ||
| 1690 | + | func (_c *MockContainerRuntime_RestartContainer_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_RestartContainer_Call { | |
| 1691 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1692 | + | var arg0 context.Context | |
| 1693 | + | if args[0] != nil { | |
| 1694 | + | arg0 = args[0].(context.Context) | |
| 1695 | + | } | |
| 1696 | + | var arg1 string | |
| 1697 | + | if args[1] != nil { | |
| 1698 | + | arg1 = args[1].(string) | |
| 1699 | + | } | |
| 1700 | + | run( | |
| 1701 | + | arg0, | |
| 1702 | + | arg1, | |
| 1703 | + | ) | |
| 1704 | + | }) | |
| 1705 | + | return _c | |
| 1706 | + | } | |
| 1707 | + | ||
| 1708 | + | func (_c *MockContainerRuntime_RestartContainer_Call) Return(err error) *MockContainerRuntime_RestartContainer_Call { | |
| 1709 | + | _c.Call.Return(err) | |
| 1710 | + | return _c | |
| 1711 | + | } | |
| 1712 | + | ||
| 1713 | + | func (_c *MockContainerRuntime_RestartContainer_Call) RunAndReturn(run func(ctx context.Context, containerID string) error) *MockContainerRuntime_RestartContainer_Call { | |
| 1714 | + | _c.Call.Return(run) | |
| 1715 | + | return _c | |
| 1716 | + | } | |
| 1717 | + | ||
| 1718 | + | // StartContainer provides a mock function for the type MockContainerRuntime | |
| 1719 | + | func (_mock *MockContainerRuntime) StartContainer(ctx context.Context, containerID string) error { | |
| 1720 | + | ret := _mock.Called(ctx, containerID) | |
| 1721 | + | ||
| 1722 | + | if len(ret) == 0 { | |
| 1723 | + | panic("no return value specified for StartContainer") | |
| 1724 | + | } | |
| 1725 | + | ||
| 1726 | + | var r0 error | |
| 1727 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1728 | + | r0 = returnFunc(ctx, containerID) | |
| 1729 | + | } else { | |
| 1730 | + | r0 = ret.Error(0) | |
| 1731 | + | } | |
| 1732 | + | return r0 | |
| 1733 | + | } | |
| 1734 | + | ||
| 1735 | + | // MockContainerRuntime_StartContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartContainer' | |
| 1736 | + | type MockContainerRuntime_StartContainer_Call struct { | |
| 1737 | + | *mock.Call | |
| 1738 | + | } | |
| 1739 | + | ||
| 1740 | + | // StartContainer is a helper method to define mock.On call | |
| 1741 | + | // - ctx context.Context | |
| 1742 | + | // - containerID string | |
| 1743 | + | func (_e *MockContainerRuntime_Expecter) StartContainer(ctx interface{}, containerID interface{}) *MockContainerRuntime_StartContainer_Call { | |
| 1744 | + | return &MockContainerRuntime_StartContainer_Call{Call: _e.mock.On("StartContainer", ctx, containerID)} | |
| 1745 | + | } | |
| 1746 | + | ||
| 1747 | + | func (_c *MockContainerRuntime_StartContainer_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_StartContainer_Call { | |
| 1748 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1749 | + | var arg0 context.Context | |
| 1750 | + | if args[0] != nil { | |
| 1751 | + | arg0 = args[0].(context.Context) | |
| 1752 | + | } | |
| 1753 | + | var arg1 string | |
| 1754 | + | if args[1] != nil { | |
| 1755 | + | arg1 = args[1].(string) | |
| 1756 | + | } | |
| 1757 | + | run( | |
| 1758 | + | arg0, | |
| 1759 | + | arg1, | |
| 1760 | + | ) | |
| 1761 | + | }) | |
| 1762 | + | return _c | |
| 1763 | + | } | |
| 1764 | + | ||
| 1765 | + | func (_c *MockContainerRuntime_StartContainer_Call) Return(err error) *MockContainerRuntime_StartContainer_Call { | |
| 1766 | + | _c.Call.Return(err) | |
| 1767 | + | return _c | |
| 1768 | + | } | |
| 1769 | + | ||
| 1770 | + | func (_c *MockContainerRuntime_StartContainer_Call) RunAndReturn(run func(ctx context.Context, containerID string) error) *MockContainerRuntime_StartContainer_Call { | |
| 1771 | + | _c.Call.Return(run) | |
| 1772 | + | return _c | |
| 1773 | + | } | |
| 1774 | + | ||
| 1775 | + | // StopContainer provides a mock function for the type MockContainerRuntime | |
| 1776 | + | func (_mock *MockContainerRuntime) StopContainer(ctx context.Context, containerID string) error { | |
| 1777 | + | ret := _mock.Called(ctx, containerID) | |
| 1778 | + | ||
| 1779 | + | if len(ret) == 0 { | |
| 1780 | + | panic("no return value specified for StopContainer") | |
| 1781 | + | } | |
| 1782 | + | ||
| 1783 | + | var r0 error | |
| 1784 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 1785 | + | r0 = returnFunc(ctx, containerID) | |
| 1786 | + | } else { | |
| 1787 | + | r0 = ret.Error(0) | |
| 1788 | + | } | |
| 1789 | + | return r0 | |
| 1790 | + | } | |
| 1791 | + | ||
| 1792 | + | // MockContainerRuntime_StopContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StopContainer' | |
| 1793 | + | type MockContainerRuntime_StopContainer_Call struct { | |
| 1794 | + | *mock.Call | |
| 1795 | + | } | |
| 1796 | + | ||
| 1797 | + | // StopContainer is a helper method to define mock.On call | |
| 1798 | + | // - ctx context.Context | |
| 1799 | + | // - containerID string | |
| 1800 | + | func (_e *MockContainerRuntime_Expecter) StopContainer(ctx interface{}, containerID interface{}) *MockContainerRuntime_StopContainer_Call { | |
| 1801 | + | return &MockContainerRuntime_StopContainer_Call{Call: _e.mock.On("StopContainer", ctx, containerID)} | |
| 1802 | + | } | |
| 1803 | + | ||
| 1804 | + | func (_c *MockContainerRuntime_StopContainer_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_StopContainer_Call { | |
| 1805 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1806 | + | var arg0 context.Context | |
| 1807 | + | if args[0] != nil { | |
| 1808 | + | arg0 = args[0].(context.Context) | |
| 1809 | + | } | |
| 1810 | + | var arg1 string | |
| 1811 | + | if args[1] != nil { | |
| 1812 | + | arg1 = args[1].(string) | |
| 1813 | + | } | |
| 1814 | + | run( | |
| 1815 | + | arg0, | |
| 1816 | + | arg1, | |
| 1817 | + | ) | |
| 1818 | + | }) | |
| 1819 | + | return _c | |
| 1820 | + | } | |
| 1821 | + | ||
| 1822 | + | func (_c *MockContainerRuntime_StopContainer_Call) Return(err error) *MockContainerRuntime_StopContainer_Call { | |
| 1823 | + | _c.Call.Return(err) | |
| 1824 | + | return _c | |
| 1825 | + | } | |
| 1826 | + | ||
| 1827 | + | func (_c *MockContainerRuntime_StopContainer_Call) RunAndReturn(run func(ctx context.Context, containerID string) error) *MockContainerRuntime_StopContainer_Call { | |
| 1828 | + | _c.Call.Return(run) | |
| 1829 | + | return _c | |
| 1830 | + | } | |
| 1831 | + | ||
| 1832 | + | // Version provides a mock function for the type MockContainerRuntime | |
| 1833 | + | func (_mock *MockContainerRuntime) Version(ctx context.Context) (string, error) { | |
| 1834 | + | ret := _mock.Called(ctx) | |
| 1835 | + | ||
| 1836 | + | if len(ret) == 0 { | |
| 1837 | + | panic("no return value specified for Version") | |
| 1838 | + | } | |
| 1839 | + | ||
| 1840 | + | var r0 string | |
| 1841 | + | var r1 error | |
| 1842 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (string, error)); ok { | |
| 1843 | + | return returnFunc(ctx) | |
| 1844 | + | } | |
| 1845 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) string); ok { | |
| 1846 | + | r0 = returnFunc(ctx) | |
| 1847 | + | } else { | |
| 1848 | + | r0 = ret.Get(0).(string) | |
| 1849 | + | } | |
| 1850 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 1851 | + | r1 = returnFunc(ctx) | |
| 1852 | + | } else { | |
| 1853 | + | r1 = ret.Error(1) | |
| 1854 | + | } | |
| 1855 | + | return r0, r1 | |
| 1856 | + | } | |
| 1857 | + | ||
| 1858 | + | // MockContainerRuntime_Version_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Version' | |
| 1859 | + | type MockContainerRuntime_Version_Call struct { | |
| 1860 | + | *mock.Call | |
| 1861 | + | } | |
| 1862 | + | ||
| 1863 | + | // Version is a helper method to define mock.On call | |
| 1864 | + | // - ctx context.Context | |
| 1865 | + | func (_e *MockContainerRuntime_Expecter) Version(ctx interface{}) *MockContainerRuntime_Version_Call { | |
| 1866 | + | return &MockContainerRuntime_Version_Call{Call: _e.mock.On("Version", ctx)} | |
| 1867 | + | } | |
| 1868 | + | ||
| 1869 | + | func (_c *MockContainerRuntime_Version_Call) Run(run func(ctx context.Context)) *MockContainerRuntime_Version_Call { | |
| 1870 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1871 | + | var arg0 context.Context | |
| 1872 | + | if args[0] != nil { | |
| 1873 | + | arg0 = args[0].(context.Context) | |
| 1874 | + | } | |
| 1875 | + | run( | |
| 1876 | + | arg0, | |
| 1877 | + | ) | |
| 1878 | + | }) | |
| 1879 | + | return _c | |
| 1880 | + | } | |
| 1881 | + | ||
| 1882 | + | func (_c *MockContainerRuntime_Version_Call) Return(s string, err error) *MockContainerRuntime_Version_Call { | |
| 1883 | + | _c.Call.Return(s, err) | |
| 1884 | + | return _c | |
| 1885 | + | } | |
| 1886 | + | ||
| 1887 | + | func (_c *MockContainerRuntime_Version_Call) RunAndReturn(run func(ctx context.Context) (string, error)) *MockContainerRuntime_Version_Call { | |
| 1888 | + | _c.Call.Return(run) | |
| 1889 | + | return _c | |
| 1890 | + | } | |
| 1891 | + | ||
| 1892 | + | // VolumeExists provides a mock function for the type MockContainerRuntime | |
| 1893 | + | func (_mock *MockContainerRuntime) VolumeExists(ctx context.Context, volumeName string) (bool, error) { | |
| 1894 | + | ret := _mock.Called(ctx, volumeName) | |
| 1895 | + | ||
| 1896 | + | if len(ret) == 0 { | |
| 1897 | + | panic("no return value specified for VolumeExists") | |
| 1898 | + | } | |
| 1899 | + | ||
| 1900 | + | var r0 bool | |
| 1901 | + | var r1 error | |
| 1902 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { | |
| 1903 | + | return returnFunc(ctx, volumeName) | |
| 1904 | + | } | |
| 1905 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok { | |
| 1906 | + | r0 = returnFunc(ctx, volumeName) | |
| 1907 | + | } else { | |
| 1908 | + | r0 = ret.Get(0).(bool) | |
| 1909 | + | } | |
| 1910 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 1911 | + | r1 = returnFunc(ctx, volumeName) | |
| 1912 | + | } else { | |
| 1913 | + | r1 = ret.Error(1) | |
| 1914 | + | } | |
| 1915 | + | return r0, r1 | |
| 1916 | + | } | |
| 1917 | + | ||
| 1918 | + | // MockContainerRuntime_VolumeExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VolumeExists' | |
| 1919 | + | type MockContainerRuntime_VolumeExists_Call struct { | |
| 1920 | + | *mock.Call | |
| 1921 | + | } | |
| 1922 | + | ||
| 1923 | + | // VolumeExists is a helper method to define mock.On call | |
| 1924 | + | // - ctx context.Context | |
| 1925 | + | // - volumeName string | |
| 1926 | + | func (_e *MockContainerRuntime_Expecter) VolumeExists(ctx interface{}, volumeName interface{}) *MockContainerRuntime_VolumeExists_Call { | |
| 1927 | + | return &MockContainerRuntime_VolumeExists_Call{Call: _e.mock.On("VolumeExists", ctx, volumeName)} | |
| 1928 | + | } | |
| 1929 | + | ||
| 1930 | + | func (_c *MockContainerRuntime_VolumeExists_Call) Run(run func(ctx context.Context, volumeName string)) *MockContainerRuntime_VolumeExists_Call { | |
| 1931 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 1932 | + | var arg0 context.Context | |
| 1933 | + | if args[0] != nil { | |
| 1934 | + | arg0 = args[0].(context.Context) | |
| 1935 | + | } | |
| 1936 | + | var arg1 string | |
| 1937 | + | if args[1] != nil { | |
| 1938 | + | arg1 = args[1].(string) | |
| 1939 | + | } | |
| 1940 | + | run( | |
| 1941 | + | arg0, | |
| 1942 | + | arg1, | |
| 1943 | + | ) | |
| 1944 | + | }) | |
| 1945 | + | return _c | |
| 1946 | + | } | |
| 1947 | + | ||
| 1948 | + | func (_c *MockContainerRuntime_VolumeExists_Call) Return(b bool, err error) *MockContainerRuntime_VolumeExists_Call { | |
| 1949 | + | _c.Call.Return(b, err) | |
| 1950 | + | return _c | |
| 1951 | + | } | |
| 1952 | + | ||
| 1953 | + | func (_c *MockContainerRuntime_VolumeExists_Call) RunAndReturn(run func(ctx context.Context, volumeName string) (bool, error)) *MockContainerRuntime_VolumeExists_Call { | |
| 1954 | + | _c.Call.Return(run) | |
| 1955 | + | return _c | |
| 1956 | + | } | |
A internal/boundaries/out/mocks/mock_env_loader.go +214 -0
| @@ -0,0 +1,214 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockEnvLoader creates a new instance of MockEnvLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockEnvLoader(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockEnvLoader { | |
| 19 | + | mock := &MockEnvLoader{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockEnvLoader is an autogenerated mock type for the EnvLoader type | |
| 28 | + | type MockEnvLoader struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockEnvLoader_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockEnvLoader) EXPECT() *MockEnvLoader_Expecter { | |
| 37 | + | return &MockEnvLoader_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // CreateEnvFile provides a mock function for the type MockEnvLoader | |
| 41 | + | func (_mock *MockEnvLoader) CreateEnvFile(ctx context.Context, domain string) error { | |
| 42 | + | ret := _mock.Called(ctx, domain) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for CreateEnvFile") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 error | |
| 49 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { | |
| 50 | + | r0 = returnFunc(ctx, domain) | |
| 51 | + | } else { | |
| 52 | + | r0 = ret.Error(0) | |
| 53 | + | } | |
| 54 | + | return r0 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // MockEnvLoader_CreateEnvFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateEnvFile' | |
| 58 | + | type MockEnvLoader_CreateEnvFile_Call struct { | |
| 59 | + | *mock.Call | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // CreateEnvFile is a helper method to define mock.On call | |
| 63 | + | // - ctx context.Context | |
| 64 | + | // - domain string | |
| 65 | + | func (_e *MockEnvLoader_Expecter) CreateEnvFile(ctx interface{}, domain interface{}) *MockEnvLoader_CreateEnvFile_Call { | |
| 66 | + | return &MockEnvLoader_CreateEnvFile_Call{Call: _e.mock.On("CreateEnvFile", ctx, domain)} | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func (_c *MockEnvLoader_CreateEnvFile_Call) Run(run func(ctx context.Context, domain string)) *MockEnvLoader_CreateEnvFile_Call { | |
| 70 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 71 | + | var arg0 context.Context | |
| 72 | + | if args[0] != nil { | |
| 73 | + | arg0 = args[0].(context.Context) | |
| 74 | + | } | |
| 75 | + | var arg1 string | |
| 76 | + | if args[1] != nil { | |
| 77 | + | arg1 = args[1].(string) | |
| 78 | + | } | |
| 79 | + | run( | |
| 80 | + | arg0, | |
| 81 | + | arg1, | |
| 82 | + | ) | |
| 83 | + | }) | |
| 84 | + | return _c | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (_c *MockEnvLoader_CreateEnvFile_Call) Return(err error) *MockEnvLoader_CreateEnvFile_Call { | |
| 88 | + | _c.Call.Return(err) | |
| 89 | + | return _c | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func (_c *MockEnvLoader_CreateEnvFile_Call) RunAndReturn(run func(ctx context.Context, domain string) error) *MockEnvLoader_CreateEnvFile_Call { | |
| 93 | + | _c.Call.Return(run) | |
| 94 | + | return _c | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // EnvFileExists provides a mock function for the type MockEnvLoader | |
| 98 | + | func (_mock *MockEnvLoader) EnvFileExists(domain string) bool { | |
| 99 | + | ret := _mock.Called(domain) | |
| 100 | + | ||
| 101 | + | if len(ret) == 0 { | |
| 102 | + | panic("no return value specified for EnvFileExists") | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | var r0 bool | |
| 106 | + | if returnFunc, ok := ret.Get(0).(func(string) bool); ok { | |
| 107 | + | r0 = returnFunc(domain) | |
| 108 | + | } else { | |
| 109 | + | r0 = ret.Get(0).(bool) | |
| 110 | + | } | |
| 111 | + | return r0 | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // MockEnvLoader_EnvFileExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnvFileExists' | |
| 115 | + | type MockEnvLoader_EnvFileExists_Call struct { | |
| 116 | + | *mock.Call | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // EnvFileExists is a helper method to define mock.On call | |
| 120 | + | // - domain string | |
| 121 | + | func (_e *MockEnvLoader_Expecter) EnvFileExists(domain interface{}) *MockEnvLoader_EnvFileExists_Call { | |
| 122 | + | return &MockEnvLoader_EnvFileExists_Call{Call: _e.mock.On("EnvFileExists", domain)} | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func (_c *MockEnvLoader_EnvFileExists_Call) Run(run func(domain string)) *MockEnvLoader_EnvFileExists_Call { | |
| 126 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 127 | + | var arg0 string | |
| 128 | + | if args[0] != nil { | |
| 129 | + | arg0 = args[0].(string) | |
| 130 | + | } | |
| 131 | + | run( | |
| 132 | + | arg0, | |
| 133 | + | ) | |
| 134 | + | }) | |
| 135 | + | return _c | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func (_c *MockEnvLoader_EnvFileExists_Call) Return(b bool) *MockEnvLoader_EnvFileExists_Call { | |
| 139 | + | _c.Call.Return(b) | |
| 140 | + | return _c | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | func (_c *MockEnvLoader_EnvFileExists_Call) RunAndReturn(run func(domain string) bool) *MockEnvLoader_EnvFileExists_Call { | |
| 144 | + | _c.Call.Return(run) | |
| 145 | + | return _c | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // LoadEnv provides a mock function for the type MockEnvLoader | |
| 149 | + | func (_mock *MockEnvLoader) LoadEnv(ctx context.Context, domain string) ([]string, error) { | |
| 150 | + | ret := _mock.Called(ctx, domain) | |
| 151 | + | ||
| 152 | + | if len(ret) == 0 { | |
| 153 | + | panic("no return value specified for LoadEnv") | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | var r0 []string | |
| 157 | + | var r1 error | |
| 158 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |
| 159 | + | return returnFunc(ctx, domain) | |
| 160 | + | } | |
| 161 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |
| 162 | + | r0 = returnFunc(ctx, domain) | |
| 163 | + | } else { | |
| 164 | + | if ret.Get(0) != nil { | |
| 165 | + | r0 = ret.Get(0).([]string) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 169 | + | r1 = returnFunc(ctx, domain) | |
| 170 | + | } else { | |
| 171 | + | r1 = ret.Error(1) | |
| 172 | + | } | |
| 173 | + | return r0, r1 | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // MockEnvLoader_LoadEnv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadEnv' | |
| 177 | + | type MockEnvLoader_LoadEnv_Call struct { | |
| 178 | + | *mock.Call | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // LoadEnv is a helper method to define mock.On call | |
| 182 | + | // - ctx context.Context | |
| 183 | + | // - domain string | |
| 184 | + | func (_e *MockEnvLoader_Expecter) LoadEnv(ctx interface{}, domain interface{}) *MockEnvLoader_LoadEnv_Call { | |
| 185 | + | return &MockEnvLoader_LoadEnv_Call{Call: _e.mock.On("LoadEnv", ctx, domain)} | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (_c *MockEnvLoader_LoadEnv_Call) Run(run func(ctx context.Context, domain string)) *MockEnvLoader_LoadEnv_Call { | |
| 189 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 190 | + | var arg0 context.Context | |
| 191 | + | if args[0] != nil { | |
| 192 | + | arg0 = args[0].(context.Context) | |
| 193 | + | } | |
| 194 | + | var arg1 string | |
| 195 | + | if args[1] != nil { | |
| 196 | + | arg1 = args[1].(string) | |
| 197 | + | } | |
| 198 | + | run( | |
| 199 | + | arg0, | |
| 200 | + | arg1, | |
| 201 | + | ) | |
| 202 | + | }) | |
| 203 | + | return _c | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | func (_c *MockEnvLoader_LoadEnv_Call) Return(strings []string, err error) *MockEnvLoader_LoadEnv_Call { | |
| 207 | + | _c.Call.Return(strings, err) | |
| 208 | + | return _c | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func (_c *MockEnvLoader_LoadEnv_Call) RunAndReturn(run func(ctx context.Context, domain string) ([]string, error)) *MockEnvLoader_LoadEnv_Call { | |
| 212 | + | _c.Call.Return(run) | |
| 213 | + | return _c | |
| 214 | + | } | |
A internal/boundaries/out/mocks/mock_event_bus.go +286 -0
| @@ -0,0 +1,286 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "gordon/internal/boundaries/out" | |
| 9 | + | "gordon/internal/domain" | |
| 10 | + | ||
| 11 | + | mock "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // NewMockEventBus creates a new instance of MockEventBus. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 15 | + | // The first argument is typically a *testing.T value. | |
| 16 | + | func NewMockEventBus(t interface { | |
| 17 | + | mock.TestingT | |
| 18 | + | Cleanup(func()) | |
| 19 | + | }) *MockEventBus { | |
| 20 | + | mock := &MockEventBus{} | |
| 21 | + | mock.Mock.Test(t) | |
| 22 | + | ||
| 23 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 24 | + | ||
| 25 | + | return mock | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // MockEventBus is an autogenerated mock type for the EventBus type | |
| 29 | + | type MockEventBus struct { | |
| 30 | + | mock.Mock | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | type MockEventBus_Expecter struct { | |
| 34 | + | mock *mock.Mock | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (_m *MockEventBus) EXPECT() *MockEventBus_Expecter { | |
| 38 | + | return &MockEventBus_Expecter{mock: &_m.Mock} | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Publish provides a mock function for the type MockEventBus | |
| 42 | + | func (_mock *MockEventBus) Publish(eventType domain.EventType, payload any) error { | |
| 43 | + | ret := _mock.Called(eventType, payload) | |
| 44 | + | ||
| 45 | + | if len(ret) == 0 { | |
| 46 | + | panic("no return value specified for Publish") | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | var r0 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(domain.EventType, any) error); ok { | |
| 51 | + | r0 = returnFunc(eventType, payload) | |
| 52 | + | } else { | |
| 53 | + | r0 = ret.Error(0) | |
| 54 | + | } | |
| 55 | + | return r0 | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // MockEventBus_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' | |
| 59 | + | type MockEventBus_Publish_Call struct { | |
| 60 | + | *mock.Call | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // Publish is a helper method to define mock.On call | |
| 64 | + | // - eventType domain.EventType | |
| 65 | + | // - payload any | |
| 66 | + | func (_e *MockEventBus_Expecter) Publish(eventType interface{}, payload interface{}) *MockEventBus_Publish_Call { | |
| 67 | + | return &MockEventBus_Publish_Call{Call: _e.mock.On("Publish", eventType, payload)} | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *MockEventBus_Publish_Call) Run(run func(eventType domain.EventType, payload any)) *MockEventBus_Publish_Call { | |
| 71 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 72 | + | var arg0 domain.EventType | |
| 73 | + | if args[0] != nil { | |
| 74 | + | arg0 = args[0].(domain.EventType) | |
| 75 | + | } | |
| 76 | + | var arg1 any | |
| 77 | + | if args[1] != nil { | |
| 78 | + | arg1 = args[1].(any) | |
| 79 | + | } | |
| 80 | + | run( | |
| 81 | + | arg0, | |
| 82 | + | arg1, | |
| 83 | + | ) | |
| 84 | + | }) | |
| 85 | + | return _c | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func (_c *MockEventBus_Publish_Call) Return(err error) *MockEventBus_Publish_Call { | |
| 89 | + | _c.Call.Return(err) | |
| 90 | + | return _c | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func (_c *MockEventBus_Publish_Call) RunAndReturn(run func(eventType domain.EventType, payload any) error) *MockEventBus_Publish_Call { | |
| 94 | + | _c.Call.Return(run) | |
| 95 | + | return _c | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Start provides a mock function for the type MockEventBus | |
| 99 | + | func (_mock *MockEventBus) Start() error { | |
| 100 | + | ret := _mock.Called() | |
| 101 | + | ||
| 102 | + | if len(ret) == 0 { | |
| 103 | + | panic("no return value specified for Start") | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | var r0 error | |
| 107 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 108 | + | r0 = returnFunc() | |
| 109 | + | } else { | |
| 110 | + | r0 = ret.Error(0) | |
| 111 | + | } | |
| 112 | + | return r0 | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // MockEventBus_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' | |
| 116 | + | type MockEventBus_Start_Call struct { | |
| 117 | + | *mock.Call | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // Start is a helper method to define mock.On call | |
| 121 | + | func (_e *MockEventBus_Expecter) Start() *MockEventBus_Start_Call { | |
| 122 | + | return &MockEventBus_Start_Call{Call: _e.mock.On("Start")} | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func (_c *MockEventBus_Start_Call) Run(run func()) *MockEventBus_Start_Call { | |
| 126 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 127 | + | run() | |
| 128 | + | }) | |
| 129 | + | return _c | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (_c *MockEventBus_Start_Call) Return(err error) *MockEventBus_Start_Call { | |
| 133 | + | _c.Call.Return(err) | |
| 134 | + | return _c | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (_c *MockEventBus_Start_Call) RunAndReturn(run func() error) *MockEventBus_Start_Call { | |
| 138 | + | _c.Call.Return(run) | |
| 139 | + | return _c | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // Stop provides a mock function for the type MockEventBus | |
| 143 | + | func (_mock *MockEventBus) Stop() error { | |
| 144 | + | ret := _mock.Called() | |
| 145 | + | ||
| 146 | + | if len(ret) == 0 { | |
| 147 | + | panic("no return value specified for Stop") | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | var r0 error | |
| 151 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 152 | + | r0 = returnFunc() | |
| 153 | + | } else { | |
| 154 | + | r0 = ret.Error(0) | |
| 155 | + | } | |
| 156 | + | return r0 | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // MockEventBus_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop' | |
| 160 | + | type MockEventBus_Stop_Call struct { | |
| 161 | + | *mock.Call | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // Stop is a helper method to define mock.On call | |
| 165 | + | func (_e *MockEventBus_Expecter) Stop() *MockEventBus_Stop_Call { | |
| 166 | + | return &MockEventBus_Stop_Call{Call: _e.mock.On("Stop")} | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (_c *MockEventBus_Stop_Call) Run(run func()) *MockEventBus_Stop_Call { | |
| 170 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 171 | + | run() | |
| 172 | + | }) | |
| 173 | + | return _c | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | func (_c *MockEventBus_Stop_Call) Return(err error) *MockEventBus_Stop_Call { | |
| 177 | + | _c.Call.Return(err) | |
| 178 | + | return _c | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | func (_c *MockEventBus_Stop_Call) RunAndReturn(run func() error) *MockEventBus_Stop_Call { | |
| 182 | + | _c.Call.Return(run) | |
| 183 | + | return _c | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // Subscribe provides a mock function for the type MockEventBus | |
| 187 | + | func (_mock *MockEventBus) Subscribe(handler out.EventHandler) error { | |
| 188 | + | ret := _mock.Called(handler) | |
| 189 | + | ||
| 190 | + | if len(ret) == 0 { | |
| 191 | + | panic("no return value specified for Subscribe") | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | var r0 error | |
| 195 | + | if returnFunc, ok := ret.Get(0).(func(out.EventHandler) error); ok { | |
| 196 | + | r0 = returnFunc(handler) | |
| 197 | + | } else { | |
| 198 | + | r0 = ret.Error(0) | |
| 199 | + | } | |
| 200 | + | return r0 | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // MockEventBus_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe' | |
| 204 | + | type MockEventBus_Subscribe_Call struct { | |
| 205 | + | *mock.Call | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // Subscribe is a helper method to define mock.On call | |
| 209 | + | // - handler out.EventHandler | |
| 210 | + | func (_e *MockEventBus_Expecter) Subscribe(handler interface{}) *MockEventBus_Subscribe_Call { | |
| 211 | + | return &MockEventBus_Subscribe_Call{Call: _e.mock.On("Subscribe", handler)} | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *MockEventBus_Subscribe_Call) Run(run func(handler out.EventHandler)) *MockEventBus_Subscribe_Call { | |
| 215 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 216 | + | var arg0 out.EventHandler | |
| 217 | + | if args[0] != nil { | |
| 218 | + | arg0 = args[0].(out.EventHandler) | |
| 219 | + | } | |
| 220 | + | run( | |
| 221 | + | arg0, | |
| 222 | + | ) | |
| 223 | + | }) | |
| 224 | + | return _c | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | func (_c *MockEventBus_Subscribe_Call) Return(err error) *MockEventBus_Subscribe_Call { | |
| 228 | + | _c.Call.Return(err) | |
| 229 | + | return _c | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func (_c *MockEventBus_Subscribe_Call) RunAndReturn(run func(handler out.EventHandler) error) *MockEventBus_Subscribe_Call { | |
| 233 | + | _c.Call.Return(run) | |
| 234 | + | return _c | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Unsubscribe provides a mock function for the type MockEventBus | |
| 238 | + | func (_mock *MockEventBus) Unsubscribe(handler out.EventHandler) error { | |
| 239 | + | ret := _mock.Called(handler) | |
| 240 | + | ||
| 241 | + | if len(ret) == 0 { | |
| 242 | + | panic("no return value specified for Unsubscribe") | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | var r0 error | |
| 246 | + | if returnFunc, ok := ret.Get(0).(func(out.EventHandler) error); ok { | |
| 247 | + | r0 = returnFunc(handler) | |
| 248 | + | } else { | |
| 249 | + | r0 = ret.Error(0) | |
| 250 | + | } | |
| 251 | + | return r0 | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // MockEventBus_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe' | |
| 255 | + | type MockEventBus_Unsubscribe_Call struct { | |
| 256 | + | *mock.Call | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | // Unsubscribe is a helper method to define mock.On call | |
| 260 | + | // - handler out.EventHandler | |
| 261 | + | func (_e *MockEventBus_Expecter) Unsubscribe(handler interface{}) *MockEventBus_Unsubscribe_Call { | |
| 262 | + | return &MockEventBus_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", handler)} | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (_c *MockEventBus_Unsubscribe_Call) Run(run func(handler out.EventHandler)) *MockEventBus_Unsubscribe_Call { | |
| 266 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 267 | + | var arg0 out.EventHandler | |
| 268 | + | if args[0] != nil { | |
| 269 | + | arg0 = args[0].(out.EventHandler) | |
| 270 | + | } | |
| 271 | + | run( | |
| 272 | + | arg0, | |
| 273 | + | ) | |
| 274 | + | }) | |
| 275 | + | return _c | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | func (_c *MockEventBus_Unsubscribe_Call) Return(err error) *MockEventBus_Unsubscribe_Call { | |
| 279 | + | _c.Call.Return(err) | |
| 280 | + | return _c | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | func (_c *MockEventBus_Unsubscribe_Call) RunAndReturn(run func(handler out.EventHandler) error) *MockEventBus_Unsubscribe_Call { | |
| 284 | + | _c.Call.Return(run) | |
| 285 | + | return _c | |
| 286 | + | } | |
A internal/boundaries/out/mocks/mock_event_handler.go +140 -0
| @@ -0,0 +1,140 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "gordon/internal/domain" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockEventHandler creates a new instance of MockEventHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockEventHandler(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockEventHandler { | |
| 19 | + | mock := &MockEventHandler{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockEventHandler is an autogenerated mock type for the EventHandler type | |
| 28 | + | type MockEventHandler struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockEventHandler_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockEventHandler) EXPECT() *MockEventHandler_Expecter { | |
| 37 | + | return &MockEventHandler_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // CanHandle provides a mock function for the type MockEventHandler | |
| 41 | + | func (_mock *MockEventHandler) CanHandle(eventType domain.EventType) bool { | |
| 42 | + | ret := _mock.Called(eventType) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for CanHandle") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 bool | |
| 49 | + | if returnFunc, ok := ret.Get(0).(func(domain.EventType) bool); ok { | |
| 50 | + | r0 = returnFunc(eventType) | |
| 51 | + | } else { | |
| 52 | + | r0 = ret.Get(0).(bool) | |
| 53 | + | } | |
| 54 | + | return r0 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // MockEventHandler_CanHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CanHandle' | |
| 58 | + | type MockEventHandler_CanHandle_Call struct { | |
| 59 | + | *mock.Call | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // CanHandle is a helper method to define mock.On call | |
| 63 | + | // - eventType domain.EventType | |
| 64 | + | func (_e *MockEventHandler_Expecter) CanHandle(eventType interface{}) *MockEventHandler_CanHandle_Call { | |
| 65 | + | return &MockEventHandler_CanHandle_Call{Call: _e.mock.On("CanHandle", eventType)} | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (_c *MockEventHandler_CanHandle_Call) Run(run func(eventType domain.EventType)) *MockEventHandler_CanHandle_Call { | |
| 69 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 70 | + | var arg0 domain.EventType | |
| 71 | + | if args[0] != nil { | |
| 72 | + | arg0 = args[0].(domain.EventType) | |
| 73 | + | } | |
| 74 | + | run( | |
| 75 | + | arg0, | |
| 76 | + | ) | |
| 77 | + | }) | |
| 78 | + | return _c | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (_c *MockEventHandler_CanHandle_Call) Return(b bool) *MockEventHandler_CanHandle_Call { | |
| 82 | + | _c.Call.Return(b) | |
| 83 | + | return _c | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (_c *MockEventHandler_CanHandle_Call) RunAndReturn(run func(eventType domain.EventType) bool) *MockEventHandler_CanHandle_Call { | |
| 87 | + | _c.Call.Return(run) | |
| 88 | + | return _c | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Handle provides a mock function for the type MockEventHandler | |
| 92 | + | func (_mock *MockEventHandler) Handle(event domain.Event) error { | |
| 93 | + | ret := _mock.Called(event) | |
| 94 | + | ||
| 95 | + | if len(ret) == 0 { | |
| 96 | + | panic("no return value specified for Handle") | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | var r0 error | |
| 100 | + | if returnFunc, ok := ret.Get(0).(func(domain.Event) error); ok { | |
| 101 | + | r0 = returnFunc(event) | |
| 102 | + | } else { | |
| 103 | + | r0 = ret.Error(0) | |
| 104 | + | } | |
| 105 | + | return r0 | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // MockEventHandler_Handle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handle' | |
| 109 | + | type MockEventHandler_Handle_Call struct { | |
| 110 | + | *mock.Call | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Handle is a helper method to define mock.On call | |
| 114 | + | // - event domain.Event | |
| 115 | + | func (_e *MockEventHandler_Expecter) Handle(event interface{}) *MockEventHandler_Handle_Call { | |
| 116 | + | return &MockEventHandler_Handle_Call{Call: _e.mock.On("Handle", event)} | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (_c *MockEventHandler_Handle_Call) Run(run func(event domain.Event)) *MockEventHandler_Handle_Call { | |
| 120 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 121 | + | var arg0 domain.Event | |
| 122 | + | if args[0] != nil { | |
| 123 | + | arg0 = args[0].(domain.Event) | |
| 124 | + | } | |
| 125 | + | run( | |
| 126 | + | arg0, | |
| 127 | + | ) | |
| 128 | + | }) | |
| 129 | + | return _c | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (_c *MockEventHandler_Handle_Call) Return(err error) *MockEventHandler_Handle_Call { | |
| 133 | + | _c.Call.Return(err) | |
| 134 | + | return _c | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (_c *MockEventHandler_Handle_Call) RunAndReturn(run func(event domain.Event) error) *MockEventHandler_Handle_Call { | |
| 138 | + | _c.Call.Return(run) | |
| 139 | + | return _c | |
| 140 | + | } | |
A internal/boundaries/out/mocks/mock_event_publisher.go +95 -0
| @@ -0,0 +1,95 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "gordon/internal/domain" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockEventPublisher creates a new instance of MockEventPublisher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockEventPublisher(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockEventPublisher { | |
| 19 | + | mock := &MockEventPublisher{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockEventPublisher is an autogenerated mock type for the EventPublisher type | |
| 28 | + | type MockEventPublisher struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockEventPublisher_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockEventPublisher) EXPECT() *MockEventPublisher_Expecter { | |
| 37 | + | return &MockEventPublisher_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Publish provides a mock function for the type MockEventPublisher | |
| 41 | + | func (_mock *MockEventPublisher) Publish(eventType domain.EventType, payload any) error { | |
| 42 | + | ret := _mock.Called(eventType, payload) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for Publish") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 error | |
| 49 | + | if returnFunc, ok := ret.Get(0).(func(domain.EventType, any) error); ok { | |
| 50 | + | r0 = returnFunc(eventType, payload) | |
| 51 | + | } else { | |
| 52 | + | r0 = ret.Error(0) | |
| 53 | + | } | |
| 54 | + | return r0 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // MockEventPublisher_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' | |
| 58 | + | type MockEventPublisher_Publish_Call struct { | |
| 59 | + | *mock.Call | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Publish is a helper method to define mock.On call | |
| 63 | + | // - eventType domain.EventType | |
| 64 | + | // - payload any | |
| 65 | + | func (_e *MockEventPublisher_Expecter) Publish(eventType interface{}, payload interface{}) *MockEventPublisher_Publish_Call { | |
| 66 | + | return &MockEventPublisher_Publish_Call{Call: _e.mock.On("Publish", eventType, payload)} | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func (_c *MockEventPublisher_Publish_Call) Run(run func(eventType domain.EventType, payload any)) *MockEventPublisher_Publish_Call { | |
| 70 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 71 | + | var arg0 domain.EventType | |
| 72 | + | if args[0] != nil { | |
| 73 | + | arg0 = args[0].(domain.EventType) | |
| 74 | + | } | |
| 75 | + | var arg1 any | |
| 76 | + | if args[1] != nil { | |
| 77 | + | arg1 = args[1].(any) | |
| 78 | + | } | |
| 79 | + | run( | |
| 80 | + | arg0, | |
| 81 | + | arg1, | |
| 82 | + | ) | |
| 83 | + | }) | |
| 84 | + | return _c | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (_c *MockEventPublisher_Publish_Call) Return(err error) *MockEventPublisher_Publish_Call { | |
| 88 | + | _c.Call.Return(err) | |
| 89 | + | return _c | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func (_c *MockEventPublisher_Publish_Call) RunAndReturn(run func(eventType domain.EventType, payload any) error) *MockEventPublisher_Publish_Call { | |
| 93 | + | _c.Call.Return(run) | |
| 94 | + | return _c | |
| 95 | + | } | |
A internal/boundaries/out/mocks/mock_event_subscriber.go +140 -0
| @@ -0,0 +1,140 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "gordon/internal/boundaries/out" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockEventSubscriber creates a new instance of MockEventSubscriber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockEventSubscriber(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockEventSubscriber { | |
| 19 | + | mock := &MockEventSubscriber{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockEventSubscriber is an autogenerated mock type for the EventSubscriber type | |
| 28 | + | type MockEventSubscriber struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockEventSubscriber_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockEventSubscriber) EXPECT() *MockEventSubscriber_Expecter { | |
| 37 | + | return &MockEventSubscriber_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // Subscribe provides a mock function for the type MockEventSubscriber | |
| 41 | + | func (_mock *MockEventSubscriber) Subscribe(handler out.EventHandler) error { | |
| 42 | + | ret := _mock.Called(handler) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for Subscribe") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 error | |
| 49 | + | if returnFunc, ok := ret.Get(0).(func(out.EventHandler) error); ok { | |
| 50 | + | r0 = returnFunc(handler) | |
| 51 | + | } else { | |
| 52 | + | r0 = ret.Error(0) | |
| 53 | + | } | |
| 54 | + | return r0 | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // MockEventSubscriber_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe' | |
| 58 | + | type MockEventSubscriber_Subscribe_Call struct { | |
| 59 | + | *mock.Call | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Subscribe is a helper method to define mock.On call | |
| 63 | + | // - handler out.EventHandler | |
| 64 | + | func (_e *MockEventSubscriber_Expecter) Subscribe(handler interface{}) *MockEventSubscriber_Subscribe_Call { | |
| 65 | + | return &MockEventSubscriber_Subscribe_Call{Call: _e.mock.On("Subscribe", handler)} | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (_c *MockEventSubscriber_Subscribe_Call) Run(run func(handler out.EventHandler)) *MockEventSubscriber_Subscribe_Call { | |
| 69 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 70 | + | var arg0 out.EventHandler | |
| 71 | + | if args[0] != nil { | |
| 72 | + | arg0 = args[0].(out.EventHandler) | |
| 73 | + | } | |
| 74 | + | run( | |
| 75 | + | arg0, | |
| 76 | + | ) | |
| 77 | + | }) | |
| 78 | + | return _c | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (_c *MockEventSubscriber_Subscribe_Call) Return(err error) *MockEventSubscriber_Subscribe_Call { | |
| 82 | + | _c.Call.Return(err) | |
| 83 | + | return _c | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (_c *MockEventSubscriber_Subscribe_Call) RunAndReturn(run func(handler out.EventHandler) error) *MockEventSubscriber_Subscribe_Call { | |
| 87 | + | _c.Call.Return(run) | |
| 88 | + | return _c | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Unsubscribe provides a mock function for the type MockEventSubscriber | |
| 92 | + | func (_mock *MockEventSubscriber) Unsubscribe(handler out.EventHandler) error { | |
| 93 | + | ret := _mock.Called(handler) | |
| 94 | + | ||
| 95 | + | if len(ret) == 0 { | |
| 96 | + | panic("no return value specified for Unsubscribe") | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | var r0 error | |
| 100 | + | if returnFunc, ok := ret.Get(0).(func(out.EventHandler) error); ok { | |
| 101 | + | r0 = returnFunc(handler) | |
| 102 | + | } else { | |
| 103 | + | r0 = ret.Error(0) | |
| 104 | + | } | |
| 105 | + | return r0 | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // MockEventSubscriber_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe' | |
| 109 | + | type MockEventSubscriber_Unsubscribe_Call struct { | |
| 110 | + | *mock.Call | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Unsubscribe is a helper method to define mock.On call | |
| 114 | + | // - handler out.EventHandler | |
| 115 | + | func (_e *MockEventSubscriber_Expecter) Unsubscribe(handler interface{}) *MockEventSubscriber_Unsubscribe_Call { | |
| 116 | + | return &MockEventSubscriber_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", handler)} | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (_c *MockEventSubscriber_Unsubscribe_Call) Run(run func(handler out.EventHandler)) *MockEventSubscriber_Unsubscribe_Call { | |
| 120 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 121 | + | var arg0 out.EventHandler | |
| 122 | + | if args[0] != nil { | |
| 123 | + | arg0 = args[0].(out.EventHandler) | |
| 124 | + | } | |
| 125 | + | run( | |
| 126 | + | arg0, | |
| 127 | + | ) | |
| 128 | + | }) | |
| 129 | + | return _c | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (_c *MockEventSubscriber_Unsubscribe_Call) Return(err error) *MockEventSubscriber_Unsubscribe_Call { | |
| 133 | + | _c.Call.Return(err) | |
| 134 | + | return _c | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func (_c *MockEventSubscriber_Unsubscribe_Call) RunAndReturn(run func(handler out.EventHandler) error) *MockEventSubscriber_Unsubscribe_Call { | |
| 138 | + | _c.Call.Return(run) | |
| 139 | + | return _c | |
| 140 | + | } | |
A internal/boundaries/out/mocks/mock_manifest_storage.go +353 -0
| @@ -0,0 +1,353 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // NewMockManifestStorage creates a new instance of MockManifestStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func NewMockManifestStorage(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *MockManifestStorage { | |
| 17 | + | mock := &MockManifestStorage{} | |
| 18 | + | mock.Mock.Test(t) | |
| 19 | + | ||
| 20 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 21 | + | ||
| 22 | + | return mock | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // MockManifestStorage is an autogenerated mock type for the ManifestStorage type | |
| 26 | + | type MockManifestStorage struct { | |
| 27 | + | mock.Mock | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | type MockManifestStorage_Expecter struct { | |
| 31 | + | mock *mock.Mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (_m *MockManifestStorage) EXPECT() *MockManifestStorage_Expecter { | |
| 35 | + | return &MockManifestStorage_Expecter{mock: &_m.Mock} | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // DeleteManifest provides a mock function for the type MockManifestStorage | |
| 39 | + | func (_mock *MockManifestStorage) DeleteManifest(name string, reference string) error { | |
| 40 | + | ret := _mock.Called(name, reference) | |
| 41 | + | ||
| 42 | + | if len(ret) == 0 { | |
| 43 | + | panic("no return value specified for DeleteManifest") | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | var r0 error | |
| 47 | + | if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { | |
| 48 | + | r0 = returnFunc(name, reference) | |
| 49 | + | } else { | |
| 50 | + | r0 = ret.Error(0) | |
| 51 | + | } | |
| 52 | + | return r0 | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // MockManifestStorage_DeleteManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteManifest' | |
| 56 | + | type MockManifestStorage_DeleteManifest_Call struct { | |
| 57 | + | *mock.Call | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // DeleteManifest is a helper method to define mock.On call | |
| 61 | + | // - name string | |
| 62 | + | // - reference string | |
| 63 | + | func (_e *MockManifestStorage_Expecter) DeleteManifest(name interface{}, reference interface{}) *MockManifestStorage_DeleteManifest_Call { | |
| 64 | + | return &MockManifestStorage_DeleteManifest_Call{Call: _e.mock.On("DeleteManifest", name, reference)} | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (_c *MockManifestStorage_DeleteManifest_Call) Run(run func(name string, reference string)) *MockManifestStorage_DeleteManifest_Call { | |
| 68 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 69 | + | var arg0 string | |
| 70 | + | if args[0] != nil { | |
| 71 | + | arg0 = args[0].(string) | |
| 72 | + | } | |
| 73 | + | var arg1 string | |
| 74 | + | if args[1] != nil { | |
| 75 | + | arg1 = args[1].(string) | |
| 76 | + | } | |
| 77 | + | run( | |
| 78 | + | arg0, | |
| 79 | + | arg1, | |
| 80 | + | ) | |
| 81 | + | }) | |
| 82 | + | return _c | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func (_c *MockManifestStorage_DeleteManifest_Call) Return(err error) *MockManifestStorage_DeleteManifest_Call { | |
| 86 | + | _c.Call.Return(err) | |
| 87 | + | return _c | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (_c *MockManifestStorage_DeleteManifest_Call) RunAndReturn(run func(name string, reference string) error) *MockManifestStorage_DeleteManifest_Call { | |
| 91 | + | _c.Call.Return(run) | |
| 92 | + | return _c | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // GetManifest provides a mock function for the type MockManifestStorage | |
| 96 | + | func (_mock *MockManifestStorage) GetManifest(name string, reference string) ([]byte, string, error) { | |
| 97 | + | ret := _mock.Called(name, reference) | |
| 98 | + | ||
| 99 | + | if len(ret) == 0 { | |
| 100 | + | panic("no return value specified for GetManifest") | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | var r0 []byte | |
| 104 | + | var r1 string | |
| 105 | + | var r2 error | |
| 106 | + | if returnFunc, ok := ret.Get(0).(func(string, string) ([]byte, string, error)); ok { | |
| 107 | + | return returnFunc(name, reference) | |
| 108 | + | } | |
| 109 | + | if returnFunc, ok := ret.Get(0).(func(string, string) []byte); ok { | |
| 110 | + | r0 = returnFunc(name, reference) | |
| 111 | + | } else { | |
| 112 | + | if ret.Get(0) != nil { | |
| 113 | + | r0 = ret.Get(0).([]byte) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | if returnFunc, ok := ret.Get(1).(func(string, string) string); ok { | |
| 117 | + | r1 = returnFunc(name, reference) | |
| 118 | + | } else { | |
| 119 | + | r1 = ret.Get(1).(string) | |
| 120 | + | } | |
| 121 | + | if returnFunc, ok := ret.Get(2).(func(string, string) error); ok { | |
| 122 | + | r2 = returnFunc(name, reference) | |
| 123 | + | } else { | |
| 124 | + | r2 = ret.Error(2) | |
| 125 | + | } | |
| 126 | + | return r0, r1, r2 | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // MockManifestStorage_GetManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManifest' | |
| 130 | + | type MockManifestStorage_GetManifest_Call struct { | |
| 131 | + | *mock.Call | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // GetManifest is a helper method to define mock.On call | |
| 135 | + | // - name string | |
| 136 | + | // - reference string | |
| 137 | + | func (_e *MockManifestStorage_Expecter) GetManifest(name interface{}, reference interface{}) *MockManifestStorage_GetManifest_Call { | |
| 138 | + | return &MockManifestStorage_GetManifest_Call{Call: _e.mock.On("GetManifest", name, reference)} | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | func (_c *MockManifestStorage_GetManifest_Call) Run(run func(name string, reference string)) *MockManifestStorage_GetManifest_Call { | |
| 142 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 143 | + | var arg0 string | |
| 144 | + | if args[0] != nil { | |
| 145 | + | arg0 = args[0].(string) | |
| 146 | + | } | |
| 147 | + | var arg1 string | |
| 148 | + | if args[1] != nil { | |
| 149 | + | arg1 = args[1].(string) | |
| 150 | + | } | |
| 151 | + | run( | |
| 152 | + | arg0, | |
| 153 | + | arg1, | |
| 154 | + | ) | |
| 155 | + | }) | |
| 156 | + | return _c | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | func (_c *MockManifestStorage_GetManifest_Call) Return(bytes []byte, s string, err error) *MockManifestStorage_GetManifest_Call { | |
| 160 | + | _c.Call.Return(bytes, s, err) | |
| 161 | + | return _c | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | func (_c *MockManifestStorage_GetManifest_Call) RunAndReturn(run func(name string, reference string) ([]byte, string, error)) *MockManifestStorage_GetManifest_Call { | |
| 165 | + | _c.Call.Return(run) | |
| 166 | + | return _c | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | // ListRepositories provides a mock function for the type MockManifestStorage | |
| 170 | + | func (_mock *MockManifestStorage) ListRepositories() ([]string, error) { | |
| 171 | + | ret := _mock.Called() | |
| 172 | + | ||
| 173 | + | if len(ret) == 0 { | |
| 174 | + | panic("no return value specified for ListRepositories") | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | var r0 []string | |
| 178 | + | var r1 error | |
| 179 | + | if returnFunc, ok := ret.Get(0).(func() ([]string, error)); ok { | |
| 180 | + | return returnFunc() | |
| 181 | + | } | |
| 182 | + | if returnFunc, ok := ret.Get(0).(func() []string); ok { | |
| 183 | + | r0 = returnFunc() | |
| 184 | + | } else { | |
| 185 | + | if ret.Get(0) != nil { | |
| 186 | + | r0 = ret.Get(0).([]string) | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | if returnFunc, ok := ret.Get(1).(func() error); ok { | |
| 190 | + | r1 = returnFunc() | |
| 191 | + | } else { | |
| 192 | + | r1 = ret.Error(1) | |
| 193 | + | } | |
| 194 | + | return r0, r1 | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | // MockManifestStorage_ListRepositories_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRepositories' | |
| 198 | + | type MockManifestStorage_ListRepositories_Call struct { | |
| 199 | + | *mock.Call | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | // ListRepositories is a helper method to define mock.On call | |
| 203 | + | func (_e *MockManifestStorage_Expecter) ListRepositories() *MockManifestStorage_ListRepositories_Call { | |
| 204 | + | return &MockManifestStorage_ListRepositories_Call{Call: _e.mock.On("ListRepositories")} | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | func (_c *MockManifestStorage_ListRepositories_Call) Run(run func()) *MockManifestStorage_ListRepositories_Call { | |
| 208 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 209 | + | run() | |
| 210 | + | }) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *MockManifestStorage_ListRepositories_Call) Return(strings []string, err error) *MockManifestStorage_ListRepositories_Call { | |
| 215 | + | _c.Call.Return(strings, err) | |
| 216 | + | return _c | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | func (_c *MockManifestStorage_ListRepositories_Call) RunAndReturn(run func() ([]string, error)) *MockManifestStorage_ListRepositories_Call { | |
| 220 | + | _c.Call.Return(run) | |
| 221 | + | return _c | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // ListTags provides a mock function for the type MockManifestStorage | |
| 225 | + | func (_mock *MockManifestStorage) ListTags(name string) ([]string, error) { | |
| 226 | + | ret := _mock.Called(name) | |
| 227 | + | ||
| 228 | + | if len(ret) == 0 { | |
| 229 | + | panic("no return value specified for ListTags") | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | var r0 []string | |
| 233 | + | var r1 error | |
| 234 | + | if returnFunc, ok := ret.Get(0).(func(string) ([]string, error)); ok { | |
| 235 | + | return returnFunc(name) | |
| 236 | + | } | |
| 237 | + | if returnFunc, ok := ret.Get(0).(func(string) []string); ok { | |
| 238 | + | r0 = returnFunc(name) | |
| 239 | + | } else { | |
| 240 | + | if ret.Get(0) != nil { | |
| 241 | + | r0 = ret.Get(0).([]string) | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | if returnFunc, ok := ret.Get(1).(func(string) error); ok { | |
| 245 | + | r1 = returnFunc(name) | |
| 246 | + | } else { | |
| 247 | + | r1 = ret.Error(1) | |
| 248 | + | } | |
| 249 | + | return r0, r1 | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // MockManifestStorage_ListTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListTags' | |
| 253 | + | type MockManifestStorage_ListTags_Call struct { | |
| 254 | + | *mock.Call | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | // ListTags is a helper method to define mock.On call | |
| 258 | + | // - name string | |
| 259 | + | func (_e *MockManifestStorage_Expecter) ListTags(name interface{}) *MockManifestStorage_ListTags_Call { | |
| 260 | + | return &MockManifestStorage_ListTags_Call{Call: _e.mock.On("ListTags", name)} | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | func (_c *MockManifestStorage_ListTags_Call) Run(run func(name string)) *MockManifestStorage_ListTags_Call { | |
| 264 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 265 | + | var arg0 string | |
| 266 | + | if args[0] != nil { | |
| 267 | + | arg0 = args[0].(string) | |
| 268 | + | } | |
| 269 | + | run( | |
| 270 | + | arg0, | |
| 271 | + | ) | |
| 272 | + | }) | |
| 273 | + | return _c | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (_c *MockManifestStorage_ListTags_Call) Return(strings []string, err error) *MockManifestStorage_ListTags_Call { | |
| 277 | + | _c.Call.Return(strings, err) | |
| 278 | + | return _c | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | func (_c *MockManifestStorage_ListTags_Call) RunAndReturn(run func(name string) ([]string, error)) *MockManifestStorage_ListTags_Call { | |
| 282 | + | _c.Call.Return(run) | |
| 283 | + | return _c | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // PutManifest provides a mock function for the type MockManifestStorage | |
| 287 | + | func (_mock *MockManifestStorage) PutManifest(name string, reference string, contentType string, data []byte) error { | |
| 288 | + | ret := _mock.Called(name, reference, contentType, data) | |
| 289 | + | ||
| 290 | + | if len(ret) == 0 { | |
| 291 | + | panic("no return value specified for PutManifest") | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | var r0 error | |
| 295 | + | if returnFunc, ok := ret.Get(0).(func(string, string, string, []byte) error); ok { | |
| 296 | + | r0 = returnFunc(name, reference, contentType, data) | |
| 297 | + | } else { | |
| 298 | + | r0 = ret.Error(0) | |
| 299 | + | } | |
| 300 | + | return r0 | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | // MockManifestStorage_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest' | |
| 304 | + | type MockManifestStorage_PutManifest_Call struct { | |
| 305 | + | *mock.Call | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // PutManifest is a helper method to define mock.On call | |
| 309 | + | // - name string | |
| 310 | + | // - reference string | |
| 311 | + | // - contentType string | |
| 312 | + | // - data []byte | |
| 313 | + | func (_e *MockManifestStorage_Expecter) PutManifest(name interface{}, reference interface{}, contentType interface{}, data interface{}) *MockManifestStorage_PutManifest_Call { | |
| 314 | + | return &MockManifestStorage_PutManifest_Call{Call: _e.mock.On("PutManifest", name, reference, contentType, data)} | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func (_c *MockManifestStorage_PutManifest_Call) Run(run func(name string, reference string, contentType string, data []byte)) *MockManifestStorage_PutManifest_Call { | |
| 318 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 319 | + | var arg0 string | |
| 320 | + | if args[0] != nil { | |
| 321 | + | arg0 = args[0].(string) | |
| 322 | + | } | |
| 323 | + | var arg1 string | |
| 324 | + | if args[1] != nil { | |
| 325 | + | arg1 = args[1].(string) | |
| 326 | + | } | |
| 327 | + | var arg2 string | |
| 328 | + | if args[2] != nil { | |
| 329 | + | arg2 = args[2].(string) | |
| 330 | + | } | |
| 331 | + | var arg3 []byte | |
| 332 | + | if args[3] != nil { | |
| 333 | + | arg3 = args[3].([]byte) | |
| 334 | + | } | |
| 335 | + | run( | |
| 336 | + | arg0, | |
| 337 | + | arg1, | |
| 338 | + | arg2, | |
| 339 | + | arg3, | |
| 340 | + | ) | |
| 341 | + | }) | |
| 342 | + | return _c | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func (_c *MockManifestStorage_PutManifest_Call) Return(err error) *MockManifestStorage_PutManifest_Call { | |
| 346 | + | _c.Call.Return(err) | |
| 347 | + | return _c | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | func (_c *MockManifestStorage_PutManifest_Call) RunAndReturn(run func(name string, reference string, contentType string, data []byte) error) *MockManifestStorage_PutManifest_Call { | |
| 351 | + | _c.Call.Return(run) | |
| 352 | + | return _c | |
| 353 | + | } | |
A internal/boundaries/out/mocks/mock_secret_provider.go +192 -0
| @@ -0,0 +1,192 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // NewMockSecretProvider creates a new instance of MockSecretProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func NewMockSecretProvider(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *MockSecretProvider { | |
| 19 | + | mock := &MockSecretProvider{} | |
| 20 | + | mock.Mock.Test(t) | |
| 21 | + | ||
| 22 | + | t.Cleanup(func() { mock.AssertExpectations(t) }) | |
| 23 | + | ||
| 24 | + | return mock | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MockSecretProvider is an autogenerated mock type for the SecretProvider type | |
| 28 | + | type MockSecretProvider struct { | |
| 29 | + | mock.Mock | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type MockSecretProvider_Expecter struct { | |
| 33 | + | mock *mock.Mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (_m *MockSecretProvider) EXPECT() *MockSecretProvider_Expecter { | |
| 37 | + | return &MockSecretProvider_Expecter{mock: &_m.Mock} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // GetSecret provides a mock function for the type MockSecretProvider | |
| 41 | + | func (_mock *MockSecretProvider) GetSecret(ctx context.Context, key string) (string, error) { | |
| 42 | + | ret := _mock.Called(ctx, key) | |
| 43 | + | ||
| 44 | + | if len(ret) == 0 { | |
| 45 | + | panic("no return value specified for GetSecret") | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | var r0 string | |
| 49 | + | var r1 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { | |
| 51 | + | return returnFunc(ctx, key) | |
| 52 | + | } | |
| 53 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { | |
| 54 | + | r0 = returnFunc(ctx, key) | |
| 55 | + | } else { | |
| 56 | + | r0 = ret.Get(0).(string) | |
| 57 | + | } | |
| 58 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 59 | + | r1 = returnFunc(ctx, key) | |
| 60 | + | } else { | |
| 61 | + | r1 = ret.Error(1) | |
| 62 | + | } | |
| 63 | + | return r0, r1 | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // MockSecretProvider_GetSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSecret' | |
| 67 | + | type MockSecretProvider_GetSecret_Call struct { | |
| 68 | + | *mock.Call | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // GetSecret is a helper method to define mock.On call | |
| 72 | + | // - ctx context.Context | |
| 73 | + | // - key string | |
| 74 | + | func (_e *MockSecretProvider_Expecter) GetSecret(ctx interface{}, key interface{}) *MockSecretProvider_GetSecret_Call { | |
| 75 | + | return &MockSecretProvider_GetSecret_Call{Call: _e.mock.On("GetSecret", ctx, key)} | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (_c *MockSecretProvider_GetSecret_Call) Run(run func(ctx context.Context, key string)) *MockSecretProvider_GetSecret_Call { | |
| 79 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 80 | + | var arg0 context.Context | |
| 81 | + | if args[0] != nil { | |
| 82 | + | arg0 = args[0].(context.Context) | |
| 83 | + | } | |
| 84 | + | var arg1 string | |
| 85 | + | if args[1] != nil { | |
| 86 | + | arg1 = args[1].(string) | |
| 87 | + | } | |
| 88 | + | run( | |
| 89 | + | arg0, | |
| 90 | + | arg1, | |
| 91 | + | ) | |
| 92 | + | }) | |
| 93 | + | return _c | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func (_c *MockSecretProvider_GetSecret_Call) Return(s string, err error) *MockSecretProvider_GetSecret_Call { | |
| 97 | + | _c.Call.Return(s, err) | |
| 98 | + | return _c | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (_c *MockSecretProvider_GetSecret_Call) RunAndReturn(run func(ctx context.Context, key string) (string, error)) *MockSecretProvider_GetSecret_Call { | |
| 102 | + | _c.Call.Return(run) | |
| 103 | + | return _c | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // IsAvailable provides a mock function for the type MockSecretProvider | |
| 107 | + | func (_mock *MockSecretProvider) IsAvailable() bool { | |
| 108 | + | ret := _mock.Called() | |
| 109 | + | ||
| 110 | + | if len(ret) == 0 { | |
| 111 | + | panic("no return value specified for IsAvailable") | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | var r0 bool | |
| 115 | + | if returnFunc, ok := ret.Get(0).(func() bool); ok { | |
| 116 | + | r0 = returnFunc() | |
| 117 | + | } else { | |
| 118 | + | r0 = ret.Get(0).(bool) | |
| 119 | + | } | |
| 120 | + | return r0 | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // MockSecretProvider_IsAvailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAvailable' | |
| 124 | + | type MockSecretProvider_IsAvailable_Call struct { | |
| 125 | + | *mock.Call | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // IsAvailable is a helper method to define mock.On call | |
| 129 | + | func (_e *MockSecretProvider_Expecter) IsAvailable() *MockSecretProvider_IsAvailable_Call { | |
| 130 | + | return &MockSecretProvider_IsAvailable_Call{Call: _e.mock.On("IsAvailable")} | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | func (_c *MockSecretProvider_IsAvailable_Call) Run(run func()) *MockSecretProvider_IsAvailable_Call { | |
| 134 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 135 | + | run() | |
| 136 | + | }) | |
| 137 | + | return _c | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (_c *MockSecretProvider_IsAvailable_Call) Return(b bool) *MockSecretProvider_IsAvailable_Call { | |
| 141 | + | _c.Call.Return(b) | |
| 142 | + | return _c | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func (_c *MockSecretProvider_IsAvailable_Call) RunAndReturn(run func() bool) *MockSecretProvider_IsAvailable_Call { | |
| 146 | + | _c.Call.Return(run) | |
| 147 | + | return _c | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // Name provides a mock function for the type MockSecretProvider | |
| 151 | + | func (_mock *MockSecretProvider) Name() string { | |
| 152 | + | ret := _mock.Called() | |
| 153 | + | ||
| 154 | + | if len(ret) == 0 { | |
| 155 | + | panic("no return value specified for Name") | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | var r0 string | |
| 159 | + | if returnFunc, ok := ret.Get(0).(func() string); ok { | |
| 160 | + | r0 = returnFunc() | |
| 161 | + | } else { | |
| 162 | + | r0 = ret.Get(0).(string) | |
| 163 | + | } | |
| 164 | + | return r0 | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | // MockSecretProvider_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' | |
| 168 | + | type MockSecretProvider_Name_Call struct { | |
| 169 | + | *mock.Call | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // Name is a helper method to define mock.On call | |
| 173 | + | func (_e *MockSecretProvider_Expecter) Name() *MockSecretProvider_Name_Call { | |
| 174 | + | return &MockSecretProvider_Name_Call{Call: _e.mock.On("Name")} | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | func (_c *MockSecretProvider_Name_Call) Run(run func()) *MockSecretProvider_Name_Call { | |
| 178 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 179 | + | run() | |
| 180 | + | }) | |
| 181 | + | return _c | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | func (_c *MockSecretProvider_Name_Call) Return(s string) *MockSecretProvider_Name_Call { | |
| 185 | + | _c.Call.Return(s) | |
| 186 | + | return _c | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func (_c *MockSecretProvider_Name_Call) RunAndReturn(run func() string) *MockSecretProvider_Name_Call { | |
| 190 | + | _c.Call.Return(run) | |
| 191 | + | return _c | |
| 192 | + | } | |
A internal/boundaries/out/runtime.go +63 -0
| @@ -0,0 +1,63 @@ | |||
| 1 | + | // Package out defines output ports (interfaces) for infrastructure. | |
| 2 | + | // These interfaces define the contract between use cases and driven adapters | |
| 3 | + | // (Docker, filesystem, etc.). | |
| 4 | + | package out | |
| 5 | + | ||
| 6 | + | import ( | |
| 7 | + | "context" | |
| 8 | + | "io" | |
| 9 | + | ||
| 10 | + | "gordon/internal/domain" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // ContainerRuntime defines the contract for container runtime operations. | |
| 14 | + | // This interface abstracts the underlying container runtime (Docker, Podman, etc.). | |
| 15 | + | type ContainerRuntime interface { | |
| 16 | + | // Container lifecycle | |
| 17 | + | CreateContainer(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error) | |
| 18 | + | StartContainer(ctx context.Context, containerID string) error | |
| 19 | + | StopContainer(ctx context.Context, containerID string) error | |
| 20 | + | RestartContainer(ctx context.Context, containerID string) error | |
| 21 | + | RemoveContainer(ctx context.Context, containerID string, force bool) error | |
| 22 | + | ||
| 23 | + | // Container inspection | |
| 24 | + | ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) | |
| 25 | + | InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) | |
| 26 | + | GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) | |
| 27 | + | ||
| 28 | + | // Image operations | |
| 29 | + | PullImage(ctx context.Context, image string) error | |
| 30 | + | PullImageWithAuth(ctx context.Context, image, username, password string) error | |
| 31 | + | RemoveImage(ctx context.Context, image string, force bool) error | |
| 32 | + | ListImages(ctx context.Context) ([]string, error) | |
| 33 | + | ||
| 34 | + | // Runtime information | |
| 35 | + | Ping(ctx context.Context) error | |
| 36 | + | Version(ctx context.Context) (string, error) | |
| 37 | + | ||
| 38 | + | // Health and status | |
| 39 | + | IsContainerRunning(ctx context.Context, containerID string) (bool, error) | |
| 40 | + | GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) | |
| 41 | + | ||
| 42 | + | // Image and port inspection | |
| 43 | + | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | |
| 44 | + | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | |
| 45 | + | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | |
| 46 | + | ||
| 47 | + | // Volume management | |
| 48 | + | InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) | |
| 49 | + | VolumeExists(ctx context.Context, volumeName string) (bool, error) | |
| 50 | + | CreateVolume(ctx context.Context, volumeName string) error | |
| 51 | + | RemoveVolume(ctx context.Context, volumeName string, force bool) error | |
| 52 | + | ||
| 53 | + | // Environment inspection | |
| 54 | + | InspectImageEnv(ctx context.Context, imageRef string) ([]string, error) | |
| 55 | + | ||
| 56 | + | // Network management | |
| 57 | + | CreateNetwork(ctx context.Context, name string, options map[string]string) error | |
| 58 | + | RemoveNetwork(ctx context.Context, name string) error | |
| 59 | + | ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) | |
| 60 | + | NetworkExists(ctx context.Context, name string) (bool, error) | |
| 61 | + | ConnectContainerToNetwork(ctx context.Context, containerName, networkName string) error | |
| 62 | + | DisconnectContainerFromNetwork(ctx context.Context, containerName, networkName string) error | |
| 63 | + | } | |
A internal/boundaries/out/secrets.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package out | |
| 2 | + | ||
| 3 | + | import "context" | |
| 4 | + | ||
| 5 | + | // SecretProvider defines the contract for retrieving secrets. | |
| 6 | + | type SecretProvider interface { | |
| 7 | + | // Name returns the provider name (e.g., "pass", "sops"). | |
| 8 | + | Name() string | |
| 9 | + | ||
| 10 | + | // GetSecret retrieves a secret by key. | |
| 11 | + | GetSecret(ctx context.Context, key string) (string, error) | |
| 12 | + | ||
| 13 | + | // IsAvailable checks if this provider is available in the current environment. | |
| 14 | + | IsAvailable() bool | |
| 15 | + | } | |
A internal/boundaries/out/storage.go +57 -0
| @@ -0,0 +1,57 @@ | |||
| 1 | + | package out | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "io" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | // BlobStorage defines the contract for blob storage operations. | |
| 8 | + | type BlobStorage interface { | |
| 9 | + | // GetBlob retrieves a blob by digest. | |
| 10 | + | GetBlob(digest string) (io.ReadCloser, error) | |
| 11 | + | ||
| 12 | + | // GetBlobPath returns the filesystem path to a blob. | |
| 13 | + | GetBlobPath(digest string) (string, error) | |
| 14 | + | ||
| 15 | + | // PutBlob stores a blob with the given digest. | |
| 16 | + | PutBlob(digest string, data io.Reader, size int64) error | |
| 17 | + | ||
| 18 | + | // DeleteBlob removes a blob by digest. | |
| 19 | + | DeleteBlob(digest string) error | |
| 20 | + | ||
| 21 | + | // BlobExists checks if a blob exists. | |
| 22 | + | BlobExists(digest string) bool | |
| 23 | + | ||
| 24 | + | // StartBlobUpload starts a new blob upload and returns the upload UUID. | |
| 25 | + | StartBlobUpload(name string) (string, error) | |
| 26 | + | ||
| 27 | + | // AppendBlobChunk appends data to an in-progress upload. | |
| 28 | + | AppendBlobChunk(name, uuid string, chunk []byte) (int64, error) | |
| 29 | + | ||
| 30 | + | // GetBlobUpload returns a writer for the upload. | |
| 31 | + | GetBlobUpload(uuid string) (io.WriteCloser, error) | |
| 32 | + | ||
| 33 | + | // FinishBlobUpload completes an upload and moves it to blob storage. | |
| 34 | + | FinishBlobUpload(uuid, digest string) error | |
| 35 | + | ||
| 36 | + | // CancelBlobUpload cancels an in-progress upload. | |
| 37 | + | CancelBlobUpload(uuid string) error | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // ManifestStorage defines the contract for manifest storage operations. | |
| 41 | + | type ManifestStorage interface { | |
| 42 | + | // GetManifest retrieves a manifest by name and reference. | |
| 43 | + | // Returns the manifest data and content type. | |
| 44 | + | GetManifest(name, reference string) ([]byte, string, error) | |
| 45 | + | ||
| 46 | + | // PutManifest stores a manifest. | |
| 47 | + | PutManifest(name, reference, contentType string, data []byte) error | |
| 48 | + | ||
| 49 | + | // DeleteManifest removes a manifest. | |
| 50 | + | DeleteManifest(name, reference string) error | |
| 51 | + | ||
| 52 | + | // ListTags returns all tags for a repository. | |
| 53 | + | ListTags(name string) ([]string, error) | |
| 54 | + | ||
| 55 | + | // ListRepositories returns all repository names. | |
| 56 | + | ListRepositories() ([]string, error) | |
| 57 | + | } | |
D internal/config/config.go +0 -529
| @@ -1,529 +0,0 @@ | |||
| 1 | - | package config | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "os" | |
| 6 | - | "path/filepath" | |
| 7 | - | "regexp" | |
| 8 | - | "strings" | |
| 9 | - | ||
| 10 | - | "github.com/rs/zerolog/log" | |
| 11 | - | "github.com/spf13/viper" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | type Config struct { | |
| 15 | - | Server ServerConfig `mapstructure:"server"` | |
| 16 | - | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` | |
| 17 | - | Routes map[string]string `mapstructure:"routes"` | |
| 18 | - | AutoRoute AutoRouteConfig `mapstructure:"auto_route"` | |
| 19 | - | Env EnvConfig `mapstructure:"env"` | |
| 20 | - | Logging LoggingConfig `mapstructure:"logging"` | |
| 21 | - | Volumes VolumeConfig `mapstructure:"volumes"` | |
| 22 | - | Attachments map[string][]string `mapstructure:"attachments"` | |
| 23 | - | NetworkGroups map[string][]string `mapstructure:"network_groups"` | |
| 24 | - | NetworkIsolation NetworkIsolationConfig `mapstructure:"network_isolation"` | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | type ServerConfig struct { | |
| 28 | - | Port int `mapstructure:"port"` | |
| 29 | - | RegistryPort int `mapstructure:"registry_port"` | |
| 30 | - | RegistryDomain string `mapstructure:"registry_domain"` | |
| 31 | - | Runtime string `mapstructure:"runtime"` | |
| 32 | - | SocketPath string `mapstructure:"socket_path"` | |
| 33 | - | SSLEmail string `mapstructure:"ssl_email"` | |
| 34 | - | DataDir string `mapstructure:"data_dir"` | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | type RegistryAuthConfig struct { | |
| 38 | - | Enabled bool `mapstructure:"enabled"` | |
| 39 | - | Username string `mapstructure:"username"` | |
| 40 | - | Password string `mapstructure:"password"` | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | type AutoRouteConfig struct { | |
| 44 | - | Enabled bool `mapstructure:"enabled"` | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | type EnvConfig struct { | |
| 48 | - | Dir string `mapstructure:"dir"` | |
| 49 | - | Providers []string `mapstructure:"providers"` | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | type LoggingConfig struct { | |
| 53 | - | Enabled bool `mapstructure:"enabled"` | |
| 54 | - | Level string `mapstructure:"level"` | |
| 55 | - | Dir string `mapstructure:"dir"` | |
| 56 | - | MainLogFile string `mapstructure:"main_log_file"` | |
| 57 | - | ProxyLogFile string `mapstructure:"proxy_log_file"` | |
| 58 | - | ContainerLogDir string `mapstructure:"container_log_dir"` | |
| 59 | - | MaxSize int `mapstructure:"max_size"` | |
| 60 | - | MaxBackups int `mapstructure:"max_backups"` | |
| 61 | - | MaxAge int `mapstructure:"max_age"` | |
| 62 | - | Compress bool `mapstructure:"compress"` | |
| 63 | - | } | |
| 64 | - | ||
| 65 | - | type VolumeConfig struct { | |
| 66 | - | AutoCreate bool `mapstructure:"auto_create"` | |
| 67 | - | Prefix string `mapstructure:"prefix"` | |
| 68 | - | Preserve bool `mapstructure:"preserve"` | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | type NetworkIsolationConfig struct { | |
| 72 | - | Enabled bool `mapstructure:"enabled"` | |
| 73 | - | NetworkPrefix string `mapstructure:"network_prefix"` | |
| 74 | - | DNSSuffix string `mapstructure:"dns_suffix"` | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | type SecretProvider struct { | |
| 78 | - | Type string `mapstructure:"type"` | |
| 79 | - | Config map[string]string `mapstructure:"config"` | |
| 80 | - | } | |
| 81 | - | ||
| 82 | - | type Route struct { | |
| 83 | - | Domain string | |
| 84 | - | Image string | |
| 85 | - | HTTPS bool | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | func Load() (*Config, error) { | |
| 89 | - | var cfg Config | |
| 90 | - | ||
| 91 | - | // Set defaults | |
| 92 | - | viper.SetDefault("server.port", 8080) | |
| 93 | - | viper.SetDefault("server.registry_port", 5000) | |
| 94 | - | viper.SetDefault("server.runtime", "auto") | |
| 95 | - | viper.SetDefault("server.socket_path", "") | |
| 96 | - | ||
| 97 | - | // Set data_dir default based on environment | |
| 98 | - | defaultDataDir := getDefaultDataDir() | |
| 99 | - | viper.SetDefault("server.data_dir", defaultDataDir) | |
| 100 | - | viper.SetDefault("registry_auth.enabled", true) | |
| 101 | - | viper.SetDefault("auto_route.enabled", false) | |
| 102 | - | viper.SetDefault("env.dir", filepath.Join(defaultDataDir, "env")) | |
| 103 | - | viper.SetDefault("env.providers", []string{"pass", "sops"}) | |
| 104 | - | ||
| 105 | - | // Logging defaults | |
| 106 | - | viper.SetDefault("logging.enabled", true) | |
| 107 | - | viper.SetDefault("logging.level", "info") | |
| 108 | - | viper.SetDefault("logging.dir", filepath.Join(defaultDataDir, "logs")) | |
| 109 | - | viper.SetDefault("logging.main_log_file", "gordon.log") | |
| 110 | - | viper.SetDefault("logging.proxy_log_file", "proxy.log") | |
| 111 | - | viper.SetDefault("logging.container_log_dir", "containers") | |
| 112 | - | viper.SetDefault("logging.max_size", 100) // 100MB | |
| 113 | - | viper.SetDefault("logging.max_backups", 3) // Keep 3 old files | |
| 114 | - | viper.SetDefault("logging.max_age", 28) // 28 days | |
| 115 | - | viper.SetDefault("logging.compress", true) | |
| 116 | - | ||
| 117 | - | // Volume defaults | |
| 118 | - | viper.SetDefault("volumes.auto_create", true) | |
| 119 | - | viper.SetDefault("volumes.prefix", "gordon") | |
| 120 | - | viper.SetDefault("volumes.preserve", true) | |
| 121 | - | ||
| 122 | - | // Network isolation defaults | |
| 123 | - | viper.SetDefault("network_isolation.enabled", true) | |
| 124 | - | viper.SetDefault("network_isolation.network_prefix", "gordon") | |
| 125 | - | viper.SetDefault("network_isolation.dns_suffix", ".internal") | |
| 126 | - | ||
| 127 | - | // Handle the routes manually since Viper struggles with domain names | |
| 128 | - | cfg.Routes = make(map[string]string) | |
| 129 | - | cfg.Attachments = make(map[string][]string) | |
| 130 | - | cfg.NetworkGroups = make(map[string][]string) | |
| 131 | - | ||
| 132 | - | // Get server config first | |
| 133 | - | if err := viper.UnmarshalKey("server", &cfg.Server); err != nil { | |
| 134 | - | return nil, fmt.Errorf("unable to decode server config: %v", err) | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | // If data_dir is empty after loading config, use the default | |
| 138 | - | if cfg.Server.DataDir == "" { | |
| 139 | - | cfg.Server.DataDir = defaultDataDir | |
| 140 | - | log.Debug().Str("data_dir", cfg.Server.DataDir).Msg("Config had empty data_dir, using default") | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | // If registry_port is 0 after loading config, use the default | |
| 144 | - | if cfg.Server.RegistryPort == 0 { | |
| 145 | - | cfg.Server.RegistryPort = 5000 | |
| 146 | - | log.Debug().Int("registry_port", cfg.Server.RegistryPort).Msg("Config had empty registry_port, using default") | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | // Get registry auth config | |
| 150 | - | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { | |
| 151 | - | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // Get auto route config | |
| 155 | - | if err := viper.UnmarshalKey("auto_route", &cfg.AutoRoute); err != nil { | |
| 156 | - | return nil, fmt.Errorf("unable to decode auto route config: %v", err) | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | // Get env config | |
| 160 | - | if err := viper.UnmarshalKey("env", &cfg.Env); err != nil { | |
| 161 | - | return nil, fmt.Errorf("unable to decode env config: %v", err) | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | // If env.dir is empty after loading config, use the default | |
| 165 | - | if cfg.Env.Dir == "" { | |
| 166 | - | cfg.Env.Dir = filepath.Join(cfg.Server.DataDir, "env") | |
| 167 | - | log.Debug().Str("env_dir", cfg.Env.Dir).Msg("Config had empty env.dir, using default") | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | // Get logging config | |
| 171 | - | if err := viper.UnmarshalKey("logging", &cfg.Logging); err != nil { | |
| 172 | - | return nil, fmt.Errorf("unable to decode logging config: %v", err) | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | // If logging.dir is empty after loading config, use the default | |
| 176 | - | if cfg.Logging.Dir == "" { | |
| 177 | - | cfg.Logging.Dir = filepath.Join(cfg.Server.DataDir, "logs") | |
| 178 | - | log.Debug().Str("logging_dir", cfg.Logging.Dir).Msg("Config had empty logging.dir, using default") | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | // Get volumes config | |
| 182 | - | if err := viper.UnmarshalKey("volumes", &cfg.Volumes); err != nil { | |
| 183 | - | return nil, fmt.Errorf("unable to decode volumes config: %v", err) | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | // Get network isolation config | |
| 187 | - | if err := viper.UnmarshalKey("network_isolation", &cfg.NetworkIsolation); err != nil { | |
| 188 | - | return nil, fmt.Errorf("unable to decode network isolation config: %v", err) | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | // Get routes manually from the raw config | |
| 192 | - | routesRaw := viper.Get("routes") | |
| 193 | - | if routesRaw != nil { | |
| 194 | - | if routes, ok := routesRaw.(map[string]interface{}); ok { | |
| 195 | - | for domain, image := range routes { | |
| 196 | - | if imageStr, ok := image.(string); ok { | |
| 197 | - | cfg.Routes[domain] = imageStr | |
| 198 | - | } | |
| 199 | - | } | |
| 200 | - | } | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | // Get attachments manually from the raw config | |
| 204 | - | attachmentsRaw := viper.Get("attachments") | |
| 205 | - | if attachmentsRaw != nil { | |
| 206 | - | if attachments, ok := attachmentsRaw.(map[string]interface{}); ok { | |
| 207 | - | for identifier, services := range attachments { | |
| 208 | - | if servicesList, ok := services.([]interface{}); ok { | |
| 209 | - | var serviceStrings []string | |
| 210 | - | for _, service := range servicesList { | |
| 211 | - | if serviceStr, ok := service.(string); ok { | |
| 212 | - | serviceStrings = append(serviceStrings, serviceStr) | |
| 213 | - | } | |
| 214 | - | } | |
| 215 | - | cfg.Attachments[identifier] = serviceStrings | |
| 216 | - | } | |
| 217 | - | } | |
| 218 | - | } | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | // Get network_groups manually from the raw config | |
| 222 | - | networkGroupsRaw := viper.Get("network_groups") | |
| 223 | - | if networkGroupsRaw != nil { | |
| 224 | - | if networkGroups, ok := networkGroupsRaw.(map[string]interface{}); ok { | |
| 225 | - | for groupName, domains := range networkGroups { | |
| 226 | - | if domainsList, ok := domains.([]interface{}); ok { | |
| 227 | - | var domainStrings []string | |
| 228 | - | for _, domain := range domainsList { | |
| 229 | - | if domainStr, ok := domain.(string); ok { | |
| 230 | - | domainStrings = append(domainStrings, domainStr) | |
| 231 | - | } | |
| 232 | - | } | |
| 233 | - | cfg.NetworkGroups[groupName] = domainStrings | |
| 234 | - | } | |
| 235 | - | } | |
| 236 | - | } | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | // Note: ssl_email is optional and only needed for future Let's Encrypt integration | |
| 240 | - | // Currently using Cloudflare for HTTPS | |
| 241 | - | ||
| 242 | - | validRuntimes := []string{"auto", "docker", "podman", "podman-rootless"} | |
| 243 | - | isValid := false | |
| 244 | - | for _, valid := range validRuntimes { | |
| 245 | - | if cfg.Server.Runtime == valid { | |
| 246 | - | isValid = true | |
| 247 | - | break | |
| 248 | - | } | |
| 249 | - | } | |
| 250 | - | if !isValid { | |
| 251 | - | return nil, fmt.Errorf("server.runtime must be one of: %s", strings.Join(validRuntimes, ", ")) | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | // Validate registry auth config | |
| 255 | - | if cfg.RegistryAuth.Enabled { | |
| 256 | - | if cfg.RegistryAuth.Username == "" || cfg.RegistryAuth.Password == "" { | |
| 257 | - | return nil, fmt.Errorf("Registry auth enabled but username/password not provided") | |
| 258 | - | } | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | // Validate registry domain if provided | |
| 262 | - | if cfg.Server.RegistryDomain != "" { | |
| 263 | - | // Basic domain validation - should not contain protocol or paths | |
| 264 | - | if strings.Contains(cfg.Server.RegistryDomain, "://") || strings.Contains(cfg.Server.RegistryDomain, "/") { | |
| 265 | - | return nil, fmt.Errorf("registry_domain should be just the domain name (e.g. 'registry.example.com')") | |
| 266 | - | } | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | // Validate env config | |
| 270 | - | if err := validateEnvConfig(&cfg.Env); err != nil { | |
| 271 | - | return nil, fmt.Errorf("invalid env config: %w", err) | |
| 272 | - | } | |
| 273 | - | ||
| 274 | - | return &cfg, nil | |
| 275 | - | } | |
| 276 | - | ||
| 277 | - | func (c *Config) GetRoutes() []Route { | |
| 278 | - | var routes []Route | |
| 279 | - | ||
| 280 | - | for domain, image := range c.Routes { | |
| 281 | - | route := Route{ | |
| 282 | - | Image: image, | |
| 283 | - | HTTPS: true, // Default to HTTPS | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | // Check if domain has http:// prefix | |
| 287 | - | if strings.HasPrefix(domain, "http://") { | |
| 288 | - | route.Domain = strings.TrimPrefix(domain, "http://") | |
| 289 | - | route.HTTPS = false | |
| 290 | - | } else { | |
| 291 | - | route.Domain = domain | |
| 292 | - | } | |
| 293 | - | ||
| 294 | - | routes = append(routes, route) | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | return routes | |
| 298 | - | } | |
| 299 | - | ||
| 300 | - | // getDefaultDataDir returns a platform-appropriate default data directory | |
| 301 | - | func getDefaultDataDir() string { | |
| 302 | - | uid := os.Getuid() | |
| 303 | - | log.Debug().Int("uid", uid).Msg("Detecting default data directory") | |
| 304 | - | ||
| 305 | - | // Check if we're running in a rootless environment | |
| 306 | - | if uid != 0 { | |
| 307 | - | // For rootless environments, use user's home directory or current directory | |
| 308 | - | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 309 | - | dataDir := filepath.Join(homeDir, ".local/share/gordon") | |
| 310 | - | log.Debug().Str("data_dir", dataDir).Msg("Using user data directory for rootless environment") | |
| 311 | - | return dataDir | |
| 312 | - | } | |
| 313 | - | log.Debug().Msg("Failed to get user home directory, falling back to ./data") | |
| 314 | - | } else { | |
| 315 | - | log.Debug().Msg("Running as root, using ./data") | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | // For root or when home directory is not available, use relative path | |
| 319 | - | return "./data" | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | // ExtractDomainFromImageName extracts domain from image names like "myapp.bamen.dev:latest" | |
| 323 | - | func ExtractDomainFromImageName(imageName string) (string, bool) { | |
| 324 | - | // Split by colon to separate image name from tag | |
| 325 | - | parts := strings.Split(imageName, ":") | |
| 326 | - | imageNamePart := parts[0] | |
| 327 | - | ||
| 328 | - | // Domain pattern: should contain at least one dot and valid domain characters | |
| 329 | - | domainPattern := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) | |
| 330 | - | ||
| 331 | - | if domainPattern.MatchString(imageNamePart) { | |
| 332 | - | return imageNamePart, true | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | return "", false | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | // AddRoute adds a new route to the configuration and saves it | |
| 339 | - | func (c *Config) AddRoute(domain, image string) error { | |
| 340 | - | if c.Routes == nil { | |
| 341 | - | c.Routes = make(map[string]string) | |
| 342 | - | } | |
| 343 | - | ||
| 344 | - | c.Routes[domain] = image | |
| 345 | - | ||
| 346 | - | // Read the current config file to preserve formatting | |
| 347 | - | configFile := viper.ConfigFileUsed() | |
| 348 | - | if configFile == "" { | |
| 349 | - | return fmt.Errorf("no config file path available") | |
| 350 | - | } | |
| 351 | - | ||
| 352 | - | // Read existing content | |
| 353 | - | content, err := os.ReadFile(configFile) | |
| 354 | - | if err != nil { | |
| 355 | - | return fmt.Errorf("failed to read config file: %w", err) | |
| 356 | - | } | |
| 357 | - | ||
| 358 | - | // Parse and update only the routes section while preserving other formatting | |
| 359 | - | lines := strings.Split(string(content), "\n") | |
| 360 | - | var newLines []string | |
| 361 | - | inRoutesSection := false | |
| 362 | - | routeAdded := false | |
| 363 | - | ||
| 364 | - | for _, line := range lines { | |
| 365 | - | trimmed := strings.TrimSpace(line) | |
| 366 | - | ||
| 367 | - | // Check if we're entering or leaving the routes section | |
| 368 | - | if trimmed == "[routes]" { | |
| 369 | - | inRoutesSection = true | |
| 370 | - | newLines = append(newLines, line) | |
| 371 | - | continue | |
| 372 | - | } | |
| 373 | - | ||
| 374 | - | // Check if we're entering a new section | |
| 375 | - | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | |
| 376 | - | // If we were in routes section and haven't added the route yet, add it | |
| 377 | - | if inRoutesSection && !routeAdded { | |
| 378 | - | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 379 | - | routeAdded = true | |
| 380 | - | } | |
| 381 | - | inRoutesSection = false | |
| 382 | - | } | |
| 383 | - | ||
| 384 | - | // If we're in routes section, check if this route already exists (check both quote styles) | |
| 385 | - | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | |
| 386 | - | // Update existing route using double quotes for consistency | |
| 387 | - | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 388 | - | routeAdded = true | |
| 389 | - | } else { | |
| 390 | - | newLines = append(newLines, line) | |
| 391 | - | } | |
| 392 | - | } | |
| 393 | - | ||
| 394 | - | // If we were in routes section at the end and haven't added the route yet, add it | |
| 395 | - | if inRoutesSection && !routeAdded { | |
| 396 | - | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 397 | - | } | |
| 398 | - | ||
| 399 | - | // Write the updated content back | |
| 400 | - | newContent := strings.Join(newLines, "\n") | |
| 401 | - | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | |
| 402 | - | return fmt.Errorf("failed to write config file: %w", err) | |
| 403 | - | } | |
| 404 | - | ||
| 405 | - | log.Info(). | |
| 406 | - | Str("domain", domain). | |
| 407 | - | Str("image", image). | |
| 408 | - | Msg("Auto-added route to configuration") | |
| 409 | - | ||
| 410 | - | // Create env file for the new route (if env loader is available) | |
| 411 | - | // This will be called from the auto-route handler which has access to the env loader | |
| 412 | - | ||
| 413 | - | return nil | |
| 414 | - | } | |
| 415 | - | ||
| 416 | - | // UpdateRoute updates an existing route configuration | |
| 417 | - | func (c *Config) UpdateRoute(domain, image string) error { | |
| 418 | - | if c.Routes == nil { | |
| 419 | - | c.Routes = make(map[string]string) | |
| 420 | - | } | |
| 421 | - | ||
| 422 | - | // Check if route exists | |
| 423 | - | if _, exists := c.Routes[domain]; !exists { | |
| 424 | - | return fmt.Errorf("route %s does not exist", domain) | |
| 425 | - | } | |
| 426 | - | ||
| 427 | - | c.Routes[domain] = image | |
| 428 | - | ||
| 429 | - | // Read the current config file to preserve formatting | |
| 430 | - | configFile := viper.ConfigFileUsed() | |
| 431 | - | if configFile == "" { | |
| 432 | - | return fmt.Errorf("no config file path available") | |
| 433 | - | } | |
| 434 | - | ||
| 435 | - | // Read existing content | |
| 436 | - | content, err := os.ReadFile(configFile) | |
| 437 | - | if err != nil { | |
| 438 | - | return fmt.Errorf("failed to read config file: %w", err) | |
| 439 | - | } | |
| 440 | - | ||
| 441 | - | // Parse and update only the routes section while preserving other formatting | |
| 442 | - | lines := strings.Split(string(content), "\n") | |
| 443 | - | var newLines []string | |
| 444 | - | inRoutesSection := false | |
| 445 | - | routeUpdated := false | |
| 446 | - | ||
| 447 | - | for _, line := range lines { | |
| 448 | - | trimmed := strings.TrimSpace(line) | |
| 449 | - | ||
| 450 | - | // Check if we're entering or leaving the routes section | |
| 451 | - | if trimmed == "[routes]" { | |
| 452 | - | inRoutesSection = true | |
| 453 | - | newLines = append(newLines, line) | |
| 454 | - | continue | |
| 455 | - | } | |
| 456 | - | ||
| 457 | - | // Check if we're entering a new section | |
| 458 | - | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | |
| 459 | - | inRoutesSection = false | |
| 460 | - | } | |
| 461 | - | ||
| 462 | - | // If we're in routes section, check if this route matches (check both quote styles) | |
| 463 | - | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | |
| 464 | - | // Update existing route using double quotes for consistency | |
| 465 | - | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 466 | - | routeUpdated = true | |
| 467 | - | } else { | |
| 468 | - | newLines = append(newLines, line) | |
| 469 | - | } | |
| 470 | - | } | |
| 471 | - | ||
| 472 | - | if !routeUpdated { | |
| 473 | - | return fmt.Errorf("route %s not found in config file", domain) | |
| 474 | - | } | |
| 475 | - | ||
| 476 | - | // Write the updated content back | |
| 477 | - | newContent := strings.Join(newLines, "\n") | |
| 478 | - | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | |
| 479 | - | return fmt.Errorf("failed to write config file: %w", err) | |
| 480 | - | } | |
| 481 | - | ||
| 482 | - | log.Info(). | |
| 483 | - | Str("domain", domain). | |
| 484 | - | Str("image", image). | |
| 485 | - | Msg("Updated route configuration") | |
| 486 | - | ||
| 487 | - | return nil | |
| 488 | - | } | |
| 489 | - | ||
| 490 | - | // validateEnvConfig validates the environment configuration | |
| 491 | - | func validateEnvConfig(envCfg *EnvConfig) error { | |
| 492 | - | // Validate env directory path | |
| 493 | - | if envCfg.Dir != "" { | |
| 494 | - | if filepath.IsAbs(envCfg.Dir) { | |
| 495 | - | // For absolute paths, check if the directory is accessible | |
| 496 | - | if info, err := os.Stat(envCfg.Dir); err != nil { | |
| 497 | - | if !os.IsNotExist(err) { | |
| 498 | - | return fmt.Errorf("env.dir path is not accessible: %w", err) | |
| 499 | - | } | |
| 500 | - | // Directory doesn't exist - that's okay, it will be created | |
| 501 | - | } else if !info.IsDir() { | |
| 502 | - | return fmt.Errorf("env.dir path exists but is not a directory: %s", envCfg.Dir) | |
| 503 | - | } | |
| 504 | - | } | |
| 505 | - | // For relative paths, we can't easily validate without changing working directory | |
| 506 | - | } | |
| 507 | - | ||
| 508 | - | // Validate secret providers configuration | |
| 509 | - | validProviders := []string{"pass", "sops"} | |
| 510 | - | for _, providerName := range envCfg.Providers { | |
| 511 | - | if providerName == "" { | |
| 512 | - | return fmt.Errorf("empty provider name in env.providers") | |
| 513 | - | } | |
| 514 | - | ||
| 515 | - | isValid := false | |
| 516 | - | for _, validType := range validProviders { | |
| 517 | - | if providerName == validType { | |
| 518 | - | isValid = true | |
| 519 | - | break | |
| 520 | - | } | |
| 521 | - | } | |
| 522 | - | if !isValid { | |
| 523 | - | return fmt.Errorf("unsupported provider '%s' in env.providers. Supported providers: %s", | |
| 524 | - | providerName, strings.Join(validProviders, ", ")) | |
| 525 | - | } | |
| 526 | - | } | |
| 527 | - | ||
| 528 | - | return nil | |
| 529 | - | } | |
D internal/config/config_test.go +0 -650
| @@ -1,650 +0,0 @@ | |||
| 1 | - | package config | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | "path/filepath" | |
| 6 | - | "testing" | |
| 7 | - | ||
| 8 | - | "github.com/spf13/viper" | |
| 9 | - | "github.com/stretchr/testify/assert" | |
| 10 | - | "github.com/stretchr/testify/require" | |
| 11 | - | ||
| 12 | - | "gordon/internal/testutils" | |
| 13 | - | ) | |
| 14 | - | ||
| 15 | - | func TestConfig_Load_ValidBasicConfig(t *testing.T) { | |
| 16 | - | // Create temporary config file | |
| 17 | - | configContent := testutils.LoadFixtureConfig(t, "basic.toml") | |
| 18 | - | ||
| 19 | - | // Create a temporary directory for the config file | |
| 20 | - | tempDir := t.TempDir() | |
| 21 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 22 | - | ||
| 23 | - | // Write config to temporary file | |
| 24 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 25 | - | require.NoError(t, err) | |
| 26 | - | ||
| 27 | - | // Set viper to use this config file | |
| 28 | - | viper.Reset() | |
| 29 | - | viper.SetConfigFile(configFile) | |
| 30 | - | err = viper.ReadInConfig() | |
| 31 | - | require.NoError(t, err) | |
| 32 | - | ||
| 33 | - | // Load config | |
| 34 | - | cfg, err := Load() | |
| 35 | - | require.NoError(t, err) | |
| 36 | - | require.NotNil(t, cfg) | |
| 37 | - | ||
| 38 | - | // Verify server config | |
| 39 | - | assert.Equal(t, 8080, cfg.Server.Port) | |
| 40 | - | assert.Equal(t, 5000, cfg.Server.RegistryPort) | |
| 41 | - | assert.Equal(t, "registry.example.com", cfg.Server.RegistryDomain) | |
| 42 | - | assert.Equal(t, "docker", cfg.Server.Runtime) | |
| 43 | - | ||
| 44 | - | // Verify registry auth config | |
| 45 | - | assert.True(t, cfg.RegistryAuth.Enabled) | |
| 46 | - | assert.Equal(t, "admin", cfg.RegistryAuth.Username) | |
| 47 | - | assert.Equal(t, "password123", cfg.RegistryAuth.Password) | |
| 48 | - | ||
| 49 | - | // Verify routes | |
| 50 | - | assert.Len(t, cfg.Routes, 2) | |
| 51 | - | assert.Equal(t, "nginx:latest", cfg.Routes["app.example.com"]) | |
| 52 | - | assert.Equal(t, "myapi:v1", cfg.Routes["api.example.com"]) | |
| 53 | - | ||
| 54 | - | // Verify volumes config | |
| 55 | - | assert.True(t, cfg.Volumes.AutoCreate) | |
| 56 | - | assert.Equal(t, "gordon", cfg.Volumes.Prefix) | |
| 57 | - | assert.True(t, cfg.Volumes.Preserve) | |
| 58 | - | ||
| 59 | - | // Verify env config | |
| 60 | - | assert.Equal(t, "/tmp/env", cfg.Env.Dir) | |
| 61 | - | assert.Contains(t, cfg.Env.Providers, "pass") | |
| 62 | - | assert.Contains(t, cfg.Env.Providers, "sops") | |
| 63 | - | ||
| 64 | - | // Verify logging config | |
| 65 | - | assert.True(t, cfg.Logging.Enabled) | |
| 66 | - | assert.Equal(t, "info", cfg.Logging.Level) | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | func TestConfig_Load_MinimalConfig(t *testing.T) { | |
| 70 | - | configContent := testutils.LoadFixtureConfig(t, "minimal.toml") | |
| 71 | - | ||
| 72 | - | tempDir := t.TempDir() | |
| 73 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 74 | - | ||
| 75 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 76 | - | require.NoError(t, err) | |
| 77 | - | ||
| 78 | - | viper.Reset() | |
| 79 | - | viper.SetConfigFile(configFile) | |
| 80 | - | err = viper.ReadInConfig() | |
| 81 | - | require.NoError(t, err) | |
| 82 | - | ||
| 83 | - | cfg, err := Load() | |
| 84 | - | require.NoError(t, err) | |
| 85 | - | require.NotNil(t, cfg) | |
| 86 | - | ||
| 87 | - | // Should have defaults applied where not specified, but explicit values where set | |
| 88 | - | // Note: viper defaults may not apply if config structure is partially loaded | |
| 89 | - | assert.Equal(t, "registry.example.com", cfg.Server.RegistryDomain) | |
| 90 | - | assert.Equal(t, "docker", cfg.Server.Runtime) // explicitly set in minimal config | |
| 91 | - | assert.False(t, cfg.RegistryAuth.Enabled) // explicitly set in minimal config | |
| 92 | - | assert.True(t, cfg.Volumes.AutoCreate) // default | |
| 93 | - | assert.Equal(t, "gordon", cfg.Volumes.Prefix) // default | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | func TestConfig_Load_InvalidTOMLSyntax(t *testing.T) { | |
| 97 | - | configContent := testutils.LoadFixtureConfig(t, "invalid.toml") | |
| 98 | - | ||
| 99 | - | tempDir := t.TempDir() | |
| 100 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 101 | - | ||
| 102 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 103 | - | require.NoError(t, err) | |
| 104 | - | ||
| 105 | - | viper.Reset() | |
| 106 | - | viper.SetConfigFile(configFile) | |
| 107 | - | err = viper.ReadInConfig() | |
| 108 | - | require.Error(t, err) | |
| 109 | - | assert.Contains(t, err.Error(), "toml") | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | func TestConfig_Load_InvalidRuntime(t *testing.T) { | |
| 113 | - | configContent := `[server] | |
| 114 | - | registry_domain = "registry.example.com" | |
| 115 | - | runtime = "invalid-runtime"` | |
| 116 | - | ||
| 117 | - | tempDir := t.TempDir() | |
| 118 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 119 | - | ||
| 120 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 121 | - | require.NoError(t, err) | |
| 122 | - | ||
| 123 | - | viper.Reset() | |
| 124 | - | viper.SetConfigFile(configFile) | |
| 125 | - | err = viper.ReadInConfig() | |
| 126 | - | require.NoError(t, err) | |
| 127 | - | ||
| 128 | - | _, err = Load() | |
| 129 | - | require.Error(t, err) | |
| 130 | - | assert.Contains(t, err.Error(), "server.runtime must be one of") | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | func TestConfig_Load_InvalidRegistryDomain(t *testing.T) { | |
| 134 | - | testCases := []struct { | |
| 135 | - | name string | |
| 136 | - | domain string | |
| 137 | - | }{ | |
| 138 | - | {"with protocol", "https://registry.example.com"}, | |
| 139 | - | {"with path", "registry.example.com/path"}, | |
| 140 | - | {"with protocol and path", "https://registry.example.com/path"}, | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | for _, tc := range testCases { | |
| 144 | - | t.Run(tc.name, func(t *testing.T) { | |
| 145 | - | configContent := `[server] | |
| 146 | - | registry_domain = "` + tc.domain + `" | |
| 147 | - | runtime = "docker" | |
| 148 | - | ||
| 149 | - | [registry_auth] | |
| 150 | - | enabled = false` | |
| 151 | - | ||
| 152 | - | tempDir := t.TempDir() | |
| 153 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 154 | - | ||
| 155 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 156 | - | require.NoError(t, err) | |
| 157 | - | ||
| 158 | - | viper.Reset() | |
| 159 | - | viper.SetConfigFile(configFile) | |
| 160 | - | err = viper.ReadInConfig() | |
| 161 | - | require.NoError(t, err) | |
| 162 | - | ||
| 163 | - | _, err = Load() | |
| 164 | - | require.Error(t, err) | |
| 165 | - | assert.Contains(t, err.Error(), "registry_domain should be just the domain name") | |
| 166 | - | }) | |
| 167 | - | } | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | func TestConfig_Load_RegistryAuthValidation(t *testing.T) { | |
| 171 | - | configContent := `[server] | |
| 172 | - | registry_domain = "registry.example.com" | |
| 173 | - | runtime = "docker" | |
| 174 | - | ||
| 175 | - | [registry_auth] | |
| 176 | - | enabled = true | |
| 177 | - | username = "" | |
| 178 | - | password = ""` | |
| 179 | - | ||
| 180 | - | tempDir := t.TempDir() | |
| 181 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 182 | - | ||
| 183 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 184 | - | require.NoError(t, err) | |
| 185 | - | ||
| 186 | - | viper.Reset() | |
| 187 | - | viper.SetConfigFile(configFile) | |
| 188 | - | err = viper.ReadInConfig() | |
| 189 | - | require.NoError(t, err) | |
| 190 | - | ||
| 191 | - | _, err = Load() | |
| 192 | - | require.Error(t, err) | |
| 193 | - | assert.Contains(t, err.Error(), "Registry auth enabled but username/password not provided") | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | func TestConfig_GetRoutes(t *testing.T) { | |
| 197 | - | cfg := &Config{ | |
| 198 | - | Routes: map[string]string{ | |
| 199 | - | "app.example.com": "nginx:latest", | |
| 200 | - | "http://api.example.com": "api:v1", | |
| 201 | - | "secure.example.com": "secure:latest", | |
| 202 | - | }, | |
| 203 | - | } | |
| 204 | - | ||
| 205 | - | routes := cfg.GetRoutes() | |
| 206 | - | ||
| 207 | - | require.Len(t, routes, 3) | |
| 208 | - | ||
| 209 | - | // Find routes by domain | |
| 210 | - | var appRoute, apiRoute, secureRoute Route | |
| 211 | - | for _, route := range routes { | |
| 212 | - | switch route.Domain { | |
| 213 | - | case "app.example.com": | |
| 214 | - | appRoute = route | |
| 215 | - | case "api.example.com": | |
| 216 | - | apiRoute = route | |
| 217 | - | case "secure.example.com": | |
| 218 | - | secureRoute = route | |
| 219 | - | } | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | // Verify HTTPS defaults | |
| 223 | - | assert.Equal(t, "nginx:latest", appRoute.Image) | |
| 224 | - | assert.True(t, appRoute.HTTPS) | |
| 225 | - | ||
| 226 | - | // Verify HTTP override | |
| 227 | - | assert.Equal(t, "api:v1", apiRoute.Image) | |
| 228 | - | assert.False(t, apiRoute.HTTPS) | |
| 229 | - | ||
| 230 | - | // Verify HTTPS default | |
| 231 | - | assert.Equal(t, "secure:latest", secureRoute.Image) | |
| 232 | - | assert.True(t, secureRoute.HTTPS) | |
| 233 | - | } | |
| 234 | - | ||
| 235 | - | func TestExtractDomainFromImageName(t *testing.T) { | |
| 236 | - | testCases := []struct { | |
| 237 | - | name string | |
| 238 | - | imageName string | |
| 239 | - | expectedDomain string | |
| 240 | - | shouldMatch bool | |
| 241 | - | }{ | |
| 242 | - | {"valid domain with tag", "app.example.com:latest", "app.example.com", true}, | |
| 243 | - | {"valid domain without tag", "api.example.com", "api.example.com", true}, | |
| 244 | - | {"subdomain", "staging.api.example.com:v1", "staging.api.example.com", true}, | |
| 245 | - | {"simple name", "nginx", "", false}, | |
| 246 | - | {"dockerhub format", "user/repo", "", false}, | |
| 247 | - | {"localhost", "localhost:5000", "", false}, | |
| 248 | - | {"ip address", "192.168.1.1:8080", "", false}, | |
| 249 | - | {"invalid domain", "app-.example.com", "", false}, | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | for _, tc := range testCases { | |
| 253 | - | t.Run(tc.name, func(t *testing.T) { | |
| 254 | - | domain, match := ExtractDomainFromImageName(tc.imageName) | |
| 255 | - | assert.Equal(t, tc.shouldMatch, match) | |
| 256 | - | assert.Equal(t, tc.expectedDomain, domain) | |
| 257 | - | }) | |
| 258 | - | } | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | func TestConfig_AddRoute(t *testing.T) { | |
| 262 | - | // Create a config with existing routes | |
| 263 | - | configContent := `[server] | |
| 264 | - | registry_domain = "registry.example.com" | |
| 265 | - | runtime = "docker" | |
| 266 | - | ||
| 267 | - | [registry_auth] | |
| 268 | - | enabled = false | |
| 269 | - | ||
| 270 | - | [routes] | |
| 271 | - | "existing.example.com" = "existing:latest"` | |
| 272 | - | ||
| 273 | - | tempDir := t.TempDir() | |
| 274 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 275 | - | ||
| 276 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 277 | - | require.NoError(t, err) | |
| 278 | - | ||
| 279 | - | viper.Reset() | |
| 280 | - | viper.SetConfigFile(configFile) | |
| 281 | - | err = viper.ReadInConfig() | |
| 282 | - | require.NoError(t, err) | |
| 283 | - | ||
| 284 | - | cfg, err := Load() | |
| 285 | - | require.NoError(t, err) | |
| 286 | - | ||
| 287 | - | // Add a new route | |
| 288 | - | err = cfg.AddRoute("new.example.com", "newapp:v1") | |
| 289 | - | require.NoError(t, err) | |
| 290 | - | ||
| 291 | - | // Verify route was added to memory | |
| 292 | - | assert.Equal(t, "newapp:v1", cfg.Routes["new.example.com"]) | |
| 293 | - | ||
| 294 | - | // Verify config file was updated | |
| 295 | - | updatedContent, err := os.ReadFile(configFile) | |
| 296 | - | require.NoError(t, err) | |
| 297 | - | assert.Contains(t, string(updatedContent), `"new.example.com" = "newapp:v1"`) | |
| 298 | - | assert.Contains(t, string(updatedContent), `"existing.example.com" = "existing:latest"`) | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | func TestConfig_UpdateRoute(t *testing.T) { | |
| 302 | - | configContent := `[server] | |
| 303 | - | registry_domain = "registry.example.com" | |
| 304 | - | runtime = "docker" | |
| 305 | - | ||
| 306 | - | [registry_auth] | |
| 307 | - | enabled = false | |
| 308 | - | ||
| 309 | - | [routes] | |
| 310 | - | "app.example.com" = "oldapp:v1"` | |
| 311 | - | ||
| 312 | - | tempDir := t.TempDir() | |
| 313 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 314 | - | ||
| 315 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 316 | - | require.NoError(t, err) | |
| 317 | - | ||
| 318 | - | viper.Reset() | |
| 319 | - | viper.SetConfigFile(configFile) | |
| 320 | - | err = viper.ReadInConfig() | |
| 321 | - | require.NoError(t, err) | |
| 322 | - | ||
| 323 | - | cfg, err := Load() | |
| 324 | - | require.NoError(t, err) | |
| 325 | - | ||
| 326 | - | // Update existing route | |
| 327 | - | err = cfg.UpdateRoute("app.example.com", "newapp:v2") | |
| 328 | - | require.NoError(t, err) | |
| 329 | - | ||
| 330 | - | // Verify route was updated in memory | |
| 331 | - | assert.Equal(t, "newapp:v2", cfg.Routes["app.example.com"]) | |
| 332 | - | ||
| 333 | - | // Verify config file was updated | |
| 334 | - | updatedContent, err := os.ReadFile(configFile) | |
| 335 | - | require.NoError(t, err) | |
| 336 | - | assert.Contains(t, string(updatedContent), `"app.example.com" = "newapp:v2"`) | |
| 337 | - | assert.NotContains(t, string(updatedContent), "oldapp:v1") | |
| 338 | - | } | |
| 339 | - | ||
| 340 | - | func TestConfig_UpdateRoute_NonExistent(t *testing.T) { | |
| 341 | - | configContent := `[server] | |
| 342 | - | registry_domain = "registry.example.com" | |
| 343 | - | runtime = "docker" | |
| 344 | - | ||
| 345 | - | [registry_auth] | |
| 346 | - | enabled = false | |
| 347 | - | ||
| 348 | - | [routes]` | |
| 349 | - | ||
| 350 | - | tempDir := t.TempDir() | |
| 351 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 352 | - | ||
| 353 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 354 | - | require.NoError(t, err) | |
| 355 | - | ||
| 356 | - | viper.Reset() | |
| 357 | - | viper.SetConfigFile(configFile) | |
| 358 | - | err = viper.ReadInConfig() | |
| 359 | - | require.NoError(t, err) | |
| 360 | - | ||
| 361 | - | cfg, err := Load() | |
| 362 | - | require.NoError(t, err) | |
| 363 | - | ||
| 364 | - | // Try to update non-existent route | |
| 365 | - | err = cfg.UpdateRoute("nonexistent.example.com", "app:v1") | |
| 366 | - | require.Error(t, err) | |
| 367 | - | assert.Contains(t, err.Error(), "route nonexistent.example.com does not exist") | |
| 368 | - | } | |
| 369 | - | ||
| 370 | - | func TestValidateEnvConfig(t *testing.T) { | |
| 371 | - | testCases := []struct { | |
| 372 | - | name string | |
| 373 | - | envConfig EnvConfig | |
| 374 | - | expectError bool | |
| 375 | - | errorText string | |
| 376 | - | }{ | |
| 377 | - | { | |
| 378 | - | name: "valid config", | |
| 379 | - | envConfig: EnvConfig{ | |
| 380 | - | Dir: "/tmp/env", | |
| 381 | - | Providers: []string{"pass", "sops"}, | |
| 382 | - | }, | |
| 383 | - | expectError: false, | |
| 384 | - | }, | |
| 385 | - | { | |
| 386 | - | name: "empty provider", | |
| 387 | - | envConfig: EnvConfig{ | |
| 388 | - | Dir: "/tmp/env", | |
| 389 | - | Providers: []string{"pass", "", "sops"}, | |
| 390 | - | }, | |
| 391 | - | expectError: true, | |
| 392 | - | errorText: "empty provider name", | |
| 393 | - | }, | |
| 394 | - | { | |
| 395 | - | name: "invalid provider", | |
| 396 | - | envConfig: EnvConfig{ | |
| 397 | - | Dir: "/tmp/env", | |
| 398 | - | Providers: []string{"pass", "invalid", "sops"}, | |
| 399 | - | }, | |
| 400 | - | expectError: true, | |
| 401 | - | errorText: "unsupported provider 'invalid'", | |
| 402 | - | }, | |
| 403 | - | { | |
| 404 | - | name: "non-existent directory file conflict", | |
| 405 | - | envConfig: EnvConfig{ | |
| 406 | - | Dir: "/dev/null", // This exists but is not a directory | |
| 407 | - | Providers: []string{"pass"}, | |
| 408 | - | }, | |
| 409 | - | expectError: true, | |
| 410 | - | errorText: "not a directory", | |
| 411 | - | }, | |
| 412 | - | } | |
| 413 | - | ||
| 414 | - | for _, tc := range testCases { | |
| 415 | - | t.Run(tc.name, func(t *testing.T) { | |
| 416 | - | err := validateEnvConfig(&tc.envConfig) | |
| 417 | - | if tc.expectError { | |
| 418 | - | require.Error(t, err) | |
| 419 | - | assert.Contains(t, err.Error(), tc.errorText) | |
| 420 | - | } else { | |
| 421 | - | require.NoError(t, err) | |
| 422 | - | } | |
| 423 | - | }) | |
| 424 | - | } | |
| 425 | - | } | |
| 426 | - | ||
| 427 | - | func TestGetDefaultDataDir(t *testing.T) { | |
| 428 | - | // Test should not fail regardless of environment | |
| 429 | - | dataDir := getDefaultDataDir() | |
| 430 | - | assert.NotEmpty(t, dataDir) | |
| 431 | - | ||
| 432 | - | // Should be either absolute path (for user home) or relative "./data" | |
| 433 | - | assert.True(t, filepath.IsAbs(dataDir) || dataDir == "./data") | |
| 434 | - | } | |
| 435 | - | ||
| 436 | - | func TestConfig_Load_NetworkIsolation(t *testing.T) { | |
| 437 | - | configContent := `[server] | |
| 438 | - | registry_domain = "registry.example.com" | |
| 439 | - | runtime = "docker" | |
| 440 | - | ||
| 441 | - | [registry_auth] | |
| 442 | - | enabled = false | |
| 443 | - | ||
| 444 | - | [network_isolation] | |
| 445 | - | enabled = true | |
| 446 | - | network_prefix = "custom" | |
| 447 | - | dns_suffix = ".local"` | |
| 448 | - | ||
| 449 | - | tempDir := t.TempDir() | |
| 450 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 451 | - | ||
| 452 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 453 | - | require.NoError(t, err) | |
| 454 | - | ||
| 455 | - | viper.Reset() | |
| 456 | - | viper.SetConfigFile(configFile) | |
| 457 | - | err = viper.ReadInConfig() | |
| 458 | - | require.NoError(t, err) | |
| 459 | - | ||
| 460 | - | cfg, err := Load() | |
| 461 | - | require.NoError(t, err) | |
| 462 | - | ||
| 463 | - | assert.True(t, cfg.NetworkIsolation.Enabled) | |
| 464 | - | assert.Equal(t, "custom", cfg.NetworkIsolation.NetworkPrefix) | |
| 465 | - | assert.Equal(t, ".local", cfg.NetworkIsolation.DNSSuffix) | |
| 466 | - | } | |
| 467 | - | ||
| 468 | - | func TestConfig_Load_AttachmentsAndNetworkGroups(t *testing.T) { | |
| 469 | - | configContent := `[server] | |
| 470 | - | registry_domain = "registry.example.com" | |
| 471 | - | runtime = "docker" | |
| 472 | - | ||
| 473 | - | [registry_auth] | |
| 474 | - | enabled = false | |
| 475 | - | ||
| 476 | - | [attachments] | |
| 477 | - | "app.example.com" = ["postgres:14", "redis:6"] | |
| 478 | - | "backend" = ["postgres:14"] | |
| 479 | - | ||
| 480 | - | [network_groups] | |
| 481 | - | "backend" = ["api.example.com", "worker.example.com"]` | |
| 482 | - | ||
| 483 | - | tempDir := t.TempDir() | |
| 484 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 485 | - | ||
| 486 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 487 | - | require.NoError(t, err) | |
| 488 | - | ||
| 489 | - | viper.Reset() | |
| 490 | - | viper.SetConfigFile(configFile) | |
| 491 | - | err = viper.ReadInConfig() | |
| 492 | - | require.NoError(t, err) | |
| 493 | - | ||
| 494 | - | cfg, err := Load() | |
| 495 | - | require.NoError(t, err) | |
| 496 | - | ||
| 497 | - | // Verify attachments | |
| 498 | - | assert.Len(t, cfg.Attachments, 2) | |
| 499 | - | assert.Equal(t, []string{"postgres:14", "redis:6"}, cfg.Attachments["app.example.com"]) | |
| 500 | - | assert.Equal(t, []string{"postgres:14"}, cfg.Attachments["backend"]) | |
| 501 | - | ||
| 502 | - | // Verify network groups | |
| 503 | - | assert.Len(t, cfg.NetworkGroups, 1) | |
| 504 | - | assert.Equal(t, []string{"api.example.com", "worker.example.com"}, cfg.NetworkGroups["backend"]) | |
| 505 | - | } | |
| 506 | - | ||
| 507 | - | func TestConfig_Load_EmptyConfiguration(t *testing.T) { | |
| 508 | - | // Test loading with completely empty config file but needs minimal valid config | |
| 509 | - | configContent := `[registry_auth] | |
| 510 | - | enabled = false` | |
| 511 | - | ||
| 512 | - | tempDir := t.TempDir() | |
| 513 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 514 | - | ||
| 515 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 516 | - | require.NoError(t, err) | |
| 517 | - | ||
| 518 | - | viper.Reset() | |
| 519 | - | viper.SetConfigFile(configFile) | |
| 520 | - | err = viper.ReadInConfig() | |
| 521 | - | require.NoError(t, err) | |
| 522 | - | ||
| 523 | - | cfg, err := Load() | |
| 524 | - | require.NoError(t, err) | |
| 525 | - | ||
| 526 | - | // Should have all defaults except what we explicitly set | |
| 527 | - | assert.Equal(t, 8080, cfg.Server.Port) | |
| 528 | - | assert.Equal(t, 5000, cfg.Server.RegistryPort) | |
| 529 | - | assert.Equal(t, "auto", cfg.Server.Runtime) | |
| 530 | - | assert.False(t, cfg.RegistryAuth.Enabled) // explicitly disabled | |
| 531 | - | assert.True(t, cfg.Volumes.AutoCreate) | |
| 532 | - | assert.Equal(t, "gordon", cfg.Volumes.Prefix) | |
| 533 | - | assert.True(t, cfg.Volumes.Preserve) | |
| 534 | - | assert.True(t, cfg.NetworkIsolation.Enabled) | |
| 535 | - | assert.Equal(t, "gordon", cfg.NetworkIsolation.NetworkPrefix) | |
| 536 | - | assert.Equal(t, ".internal", cfg.NetworkIsolation.DNSSuffix) | |
| 537 | - | } | |
| 538 | - | ||
| 539 | - | func TestConfig_AddRoute_NoConfigFile(t *testing.T) { | |
| 540 | - | // Clear viper state to ensure no config file is set | |
| 541 | - | viper.Reset() | |
| 542 | - | ||
| 543 | - | cfg := &Config{ | |
| 544 | - | Routes: make(map[string]string), | |
| 545 | - | } | |
| 546 | - | ||
| 547 | - | // Try to add route when no config file is available | |
| 548 | - | err := cfg.AddRoute("test.example.com", "test:latest") | |
| 549 | - | require.Error(t, err) | |
| 550 | - | assert.Contains(t, err.Error(), "no config file path available") | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | func TestConfig_AddRoute_FileReadError(t *testing.T) { | |
| 554 | - | // Create a config file in a non-existent directory | |
| 555 | - | configFile := "/nonexistent/directory/gordon.toml" | |
| 556 | - | ||
| 557 | - | viper.Reset() | |
| 558 | - | viper.SetConfigFile(configFile) | |
| 559 | - | ||
| 560 | - | cfg := &Config{ | |
| 561 | - | Routes: make(map[string]string), | |
| 562 | - | } | |
| 563 | - | ||
| 564 | - | err := cfg.AddRoute("test.example.com", "test:latest") | |
| 565 | - | require.Error(t, err) | |
| 566 | - | assert.Contains(t, err.Error(), "failed to read config file") | |
| 567 | - | } | |
| 568 | - | ||
| 569 | - | func TestConfig_UpdateRoute_FileReadError(t *testing.T) { | |
| 570 | - | // Create a config file in a non-existent directory | |
| 571 | - | configFile := "/nonexistent/directory/gordon.toml" | |
| 572 | - | ||
| 573 | - | viper.Reset() | |
| 574 | - | viper.SetConfigFile(configFile) | |
| 575 | - | ||
| 576 | - | cfg := &Config{ | |
| 577 | - | Routes: map[string]string{ | |
| 578 | - | "test.example.com": "test:v1", | |
| 579 | - | }, | |
| 580 | - | } | |
| 581 | - | ||
| 582 | - | err := cfg.UpdateRoute("test.example.com", "test:v2") | |
| 583 | - | require.Error(t, err) | |
| 584 | - | assert.Contains(t, err.Error(), "failed to read config file") | |
| 585 | - | } | |
| 586 | - | ||
| 587 | - | func TestConfig_AddRoute_RouteAtEndOfFile(t *testing.T) { | |
| 588 | - | configContent := `[server] | |
| 589 | - | registry_domain = "registry.example.com" | |
| 590 | - | runtime = "docker" | |
| 591 | - | ||
| 592 | - | [registry_auth] | |
| 593 | - | enabled = false | |
| 594 | - | ||
| 595 | - | [routes] | |
| 596 | - | "existing.example.com" = "existing:latest"` | |
| 597 | - | ||
| 598 | - | tempDir := t.TempDir() | |
| 599 | - | configFile := filepath.Join(tempDir, "gordon.toml") | |
| 600 | - | ||
| 601 | - | err := os.WriteFile(configFile, []byte(configContent), 0644) | |
| 602 | - | require.NoError(t, err) | |
| 603 | - | ||
| 604 | - | viper.Reset() | |
| 605 | - | viper.SetConfigFile(configFile) | |
| 606 | - | err = viper.ReadInConfig() | |
| 607 | - | require.NoError(t, err) | |
| 608 | - | ||
| 609 | - | cfg, err := Load() | |
| 610 | - | require.NoError(t, err) | |
| 611 | - | ||
| 612 | - | // Add a new route at the end of the routes section | |
| 613 | - | err = cfg.AddRoute("end.example.com", "endapp:v1") | |
| 614 | - | require.NoError(t, err) | |
| 615 | - | ||
| 616 | - | // Verify route was added to memory | |
| 617 | - | assert.Equal(t, "endapp:v1", cfg.Routes["end.example.com"]) | |
| 618 | - | ||
| 619 | - | // Verify config file was updated | |
| 620 | - | updatedContent, err := os.ReadFile(configFile) | |
| 621 | - | require.NoError(t, err) | |
| 622 | - | assert.Contains(t, string(updatedContent), `"end.example.com" = "endapp:v1"`) | |
| 623 | - | } | |
| 624 | - | ||
| 625 | - | func TestValidateEnvConfig_RelativePath(t *testing.T) { | |
| 626 | - | envConfig := EnvConfig{ | |
| 627 | - | Dir: "./relative/path", // relative path should not cause error | |
| 628 | - | Providers: []string{"pass"}, | |
| 629 | - | } | |
| 630 | - | ||
| 631 | - | err := validateEnvConfig(&envConfig) | |
| 632 | - | require.NoError(t, err) | |
| 633 | - | } | |
| 634 | - | ||
| 635 | - | func TestValidateEnvConfig_EmptyDir(t *testing.T) { | |
| 636 | - | envConfig := EnvConfig{ | |
| 637 | - | Dir: "", // empty dir should not cause error | |
| 638 | - | Providers: []string{"pass"}, | |
| 639 | - | } | |
| 640 | - | ||
| 641 | - | err := validateEnvConfig(&envConfig) | |
| 642 | - | require.NoError(t, err) | |
| 643 | - | } | |
| 644 | - | ||
| 645 | - | func TestGetDefaultDataDir_Coverage(t *testing.T) { | |
| 646 | - | // This test mainly exists to ensure coverage of getDefaultDataDir branches | |
| 647 | - | // We can't easily test root vs non-root scenarios, but we can test the function | |
| 648 | - | dataDir := getDefaultDataDir() | |
| 649 | - | assert.NotEmpty(t, dataDir) | |
| 650 | - | }⏎̸ | |
D internal/container/docker.go +0 -681
| @@ -1,681 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "encoding/base64" | |
| 6 | - | "encoding/json" | |
| 7 | - | "fmt" | |
| 8 | - | "gordon/pkg/runtime" | |
| 9 | - | "io" | |
| 10 | - | "strconv" | |
| 11 | - | "strings" | |
| 12 | - | ||
| 13 | - | "github.com/docker/docker/api/types/container" | |
| 14 | - | "github.com/docker/docker/api/types/image" | |
| 15 | - | "github.com/docker/docker/api/types/network" | |
| 16 | - | "github.com/docker/docker/api/types/registry" | |
| 17 | - | "github.com/docker/docker/api/types/volume" | |
| 18 | - | "github.com/docker/docker/client" | |
| 19 | - | "github.com/docker/docker/errdefs" | |
| 20 | - | "github.com/docker/go-connections/nat" | |
| 21 | - | "github.com/rs/zerolog/log" | |
| 22 | - | ) | |
| 23 | - | ||
| 24 | - | // DockerRuntime implements the Runtime interface using Docker API | |
| 25 | - | type DockerRuntime struct { | |
| 26 | - | client *client.Client | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | // NewDockerRuntime creates a new Docker runtime instance | |
| 30 | - | func NewDockerRuntime() (*DockerRuntime, error) { | |
| 31 | - | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 32 | - | if err != nil { | |
| 33 | - | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | return &DockerRuntime{ | |
| 37 | - | client: cli, | |
| 38 | - | }, nil | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | // NewDockerRuntimeWithClient creates a new Docker runtime instance with a custom client (for testing) | |
| 42 | - | func NewDockerRuntimeWithClient(cli *client.Client) *DockerRuntime { | |
| 43 | - | return &DockerRuntime{ | |
| 44 | - | client: cli, | |
| 45 | - | } | |
| 46 | - | } | |
| 47 | - | ||
| 48 | - | // CreateContainer creates a new container | |
| 49 | - | func (d *DockerRuntime) CreateContainer(ctx context.Context, config *runtime.ContainerConfig) (*runtime.Container, error) { | |
| 50 | - | // Convert ports to Docker format | |
| 51 | - | exposedPorts := make(nat.PortSet) | |
| 52 | - | portBindings := make(nat.PortMap) | |
| 53 | - | ||
| 54 | - | for _, port := range config.Ports { | |
| 55 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", port)) | |
| 56 | - | exposedPorts[containerPort] = struct{}{} | |
| 57 | - | ||
| 58 | - | // Bind to random available port on host | |
| 59 | - | portBindings[containerPort] = []nat.PortBinding{ | |
| 60 | - | { | |
| 61 | - | HostIP: "0.0.0.0", | |
| 62 | - | HostPort: "0", // Docker will assign a random available port | |
| 63 | - | }, | |
| 64 | - | } | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // Convert volumes to Docker format | |
| 68 | - | var binds []string | |
| 69 | - | if config.Volumes != nil { | |
| 70 | - | for containerPath, volumeName := range config.Volumes { | |
| 71 | - | bind := fmt.Sprintf("%s:%s", volumeName, containerPath) | |
| 72 | - | binds = append(binds, bind) | |
| 73 | - | log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("Adding volume mount") | |
| 74 | - | } | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | // Create container configuration | |
| 78 | - | containerConfig := &container.Config{ | |
| 79 | - | Image: config.Image, | |
| 80 | - | Env: config.Env, | |
| 81 | - | ExposedPorts: exposedPorts, | |
| 82 | - | WorkingDir: config.WorkingDir, | |
| 83 | - | Cmd: config.Cmd, | |
| 84 | - | Labels: config.Labels, | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | hostConfig := &container.HostConfig{ | |
| 88 | - | PortBindings: portBindings, | |
| 89 | - | AutoRemove: config.AutoRemove, | |
| 90 | - | Binds: binds, | |
| 91 | - | NetworkMode: container.NetworkMode(config.NetworkMode), | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | // Create network configuration for container | |
| 95 | - | var networkConfig *network.NetworkingConfig | |
| 96 | - | if config.NetworkMode != "" && config.NetworkMode != "default" && config.NetworkMode != "bridge" { | |
| 97 | - | networkConfig = &network.NetworkingConfig{ | |
| 98 | - | EndpointsConfig: map[string]*network.EndpointSettings{ | |
| 99 | - | config.NetworkMode: { | |
| 100 | - | Aliases: config.Aliases, | |
| 101 | - | }, | |
| 102 | - | }, | |
| 103 | - | } | |
| 104 | - | if config.Hostname != "" { | |
| 105 | - | networkConfig.EndpointsConfig[config.NetworkMode].NetworkID = config.NetworkMode | |
| 106 | - | } | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | // Create the container | |
| 110 | - | resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, config.Name) | |
| 111 | - | if err != nil { | |
| 112 | - | return nil, fmt.Errorf("failed to create container: %w", err) | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | log.Info().Str("id", resp.ID).Str("name", config.Name).Str("image", config.Image).Msg("Container created") | |
| 116 | - | ||
| 117 | - | // Inspect the created container to get full details | |
| 118 | - | return d.InspectContainer(ctx, resp.ID) | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | // StartContainer starts a container | |
| 122 | - | func (d *DockerRuntime) StartContainer(ctx context.Context, containerID string) error { | |
| 123 | - | err := d.client.ContainerStart(ctx, containerID, container.StartOptions{}) | |
| 124 | - | if err != nil { | |
| 125 | - | return fmt.Errorf("failed to start container %s: %w", containerID, err) | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | log.Info().Str("id", containerID).Msg("Container started") | |
| 129 | - | return nil | |
| 130 | - | } | |
| 131 | - | ||
| 132 | - | // StopContainer stops a container | |
| 133 | - | func (d *DockerRuntime) StopContainer(ctx context.Context, containerID string) error { | |
| 134 | - | timeout := 30 // 30 seconds timeout | |
| 135 | - | err := d.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 136 | - | if err != nil { | |
| 137 | - | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | log.Info().Str("id", containerID).Msg("Container stopped") | |
| 141 | - | return nil | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | // RestartContainer restarts a container | |
| 145 | - | func (d *DockerRuntime) RestartContainer(ctx context.Context, containerID string) error { | |
| 146 | - | timeout := 30 // 30 seconds timeout | |
| 147 | - | err := d.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) | |
| 148 | - | if err != nil { | |
| 149 | - | return fmt.Errorf("failed to restart container %s: %w", containerID, err) | |
| 150 | - | } | |
| 151 | - | ||
| 152 | - | log.Info().Str("id", containerID).Msg("Container restarted") | |
| 153 | - | return nil | |
| 154 | - | } | |
| 155 | - | ||
| 156 | - | // RemoveContainer removes a container | |
| 157 | - | func (d *DockerRuntime) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |
| 158 | - | err := d.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) | |
| 159 | - | if err != nil { | |
| 160 | - | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | log.Info().Str("id", containerID).Bool("force", force).Msg("Container removed") | |
| 164 | - | return nil | |
| 165 | - | } | |
| 166 | - | ||
| 167 | - | // ListContainers lists containers | |
| 168 | - | func (d *DockerRuntime) ListContainers(ctx context.Context, all bool) ([]*runtime.Container, error) { | |
| 169 | - | containers, err := d.client.ContainerList(ctx, container.ListOptions{All: all}) | |
| 170 | - | if err != nil { | |
| 171 | - | return nil, fmt.Errorf("failed to list containers: %w", err) | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | var result []*runtime.Container | |
| 175 | - | for _, c := range containers { | |
| 176 | - | // Extract ports | |
| 177 | - | var ports []int | |
| 178 | - | for _, port := range c.Ports { | |
| 179 | - | if port.PublicPort > 0 { | |
| 180 | - | ports = append(ports, int(port.PublicPort)) | |
| 181 | - | } | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | // Get the primary name (remove leading slash) | |
| 185 | - | name := "" | |
| 186 | - | if len(c.Names) > 0 { | |
| 187 | - | name = strings.TrimPrefix(c.Names[0], "/") | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | result = append(result, &runtime.Container{ | |
| 191 | - | ID: c.ID, | |
| 192 | - | Image: c.Image, | |
| 193 | - | Name: name, | |
| 194 | - | Status: c.Status, | |
| 195 | - | Ports: ports, | |
| 196 | - | Labels: c.Labels, | |
| 197 | - | }) | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | return result, nil | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | // InspectContainer inspects a container | |
| 204 | - | func (d *DockerRuntime) InspectContainer(ctx context.Context, containerID string) (*runtime.Container, error) { | |
| 205 | - | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 206 | - | if err != nil { | |
| 207 | - | return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | // Extract published ports | |
| 211 | - | var ports []int | |
| 212 | - | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 213 | - | for _, bindings := range resp.NetworkSettings.Ports { | |
| 214 | - | for _, binding := range bindings { | |
| 215 | - | if binding.HostPort != "" { | |
| 216 | - | if port, err := strconv.Atoi(binding.HostPort); err == nil { | |
| 217 | - | ports = append(ports, port) | |
| 218 | - | } | |
| 219 | - | } | |
| 220 | - | } | |
| 221 | - | } | |
| 222 | - | } | |
| 223 | - | ||
| 224 | - | // Get container name (remove leading slash) | |
| 225 | - | name := strings.TrimPrefix(resp.Name, "/") | |
| 226 | - | ||
| 227 | - | return &runtime.Container{ | |
| 228 | - | ID: resp.ID, | |
| 229 | - | Image: resp.Config.Image, | |
| 230 | - | Name: name, | |
| 231 | - | Status: resp.State.Status, | |
| 232 | - | Ports: ports, | |
| 233 | - | Labels: resp.Config.Labels, | |
| 234 | - | }, nil | |
| 235 | - | } | |
| 236 | - | ||
| 237 | - | // GetContainerLogs gets container logs | |
| 238 | - | func (d *DockerRuntime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 239 | - | logs, err := d.client.ContainerLogs(ctx, containerID, container.LogsOptions{ | |
| 240 | - | ShowStdout: true, | |
| 241 | - | ShowStderr: true, | |
| 242 | - | Follow: follow, | |
| 243 | - | Timestamps: true, | |
| 244 | - | }) | |
| 245 | - | if err != nil { | |
| 246 | - | return nil, fmt.Errorf("failed to get logs for container %s: %w", containerID, err) | |
| 247 | - | } | |
| 248 | - | ||
| 249 | - | return logs, nil | |
| 250 | - | } | |
| 251 | - | ||
| 252 | - | // PullImage pulls an image | |
| 253 | - | func (d *DockerRuntime) PullImage(ctx context.Context, imageRef string) error { | |
| 254 | - | log.Info().Str("image", imageRef).Msg("Pulling image") | |
| 255 | - | ||
| 256 | - | reader, err := d.client.ImagePull(ctx, imageRef, image.PullOptions{}) | |
| 257 | - | if err != nil { | |
| 258 | - | return fmt.Errorf("failed to pull image %s: %w", imageRef, err) | |
| 259 | - | } | |
| 260 | - | defer reader.Close() | |
| 261 | - | ||
| 262 | - | // Read the response to completion (this is required for the pull to complete) | |
| 263 | - | _, err = io.Copy(io.Discard, reader) | |
| 264 | - | if err != nil { | |
| 265 | - | return fmt.Errorf("failed to read pull response for image %s: %w", imageRef, err) | |
| 266 | - | } | |
| 267 | - | ||
| 268 | - | log.Info().Str("image", imageRef).Msg("Image pulled successfully") | |
| 269 | - | return nil | |
| 270 | - | } | |
| 271 | - | ||
| 272 | - | // PullImageWithAuth pulls an image with authentication | |
| 273 | - | func (d *DockerRuntime) PullImageWithAuth(ctx context.Context, imageRef, username, password string) error { | |
| 274 | - | log.Info().Str("image", imageRef).Str("username", username).Msg("Pulling image with authentication") | |
| 275 | - | ||
| 276 | - | // Create authentication configuration | |
| 277 | - | authConfig := registry.AuthConfig{ | |
| 278 | - | Username: username, | |
| 279 | - | Password: password, | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | // Encode authentication to base64 JSON | |
| 283 | - | authConfigBytes, err := json.Marshal(authConfig) | |
| 284 | - | if err != nil { | |
| 285 | - | return fmt.Errorf("failed to marshal auth config: %w", err) | |
| 286 | - | } | |
| 287 | - | authStr := base64.URLEncoding.EncodeToString(authConfigBytes) | |
| 288 | - | ||
| 289 | - | // Pull with authentication | |
| 290 | - | reader, err := d.client.ImagePull(ctx, imageRef, image.PullOptions{ | |
| 291 | - | RegistryAuth: authStr, | |
| 292 | - | }) | |
| 293 | - | if err != nil { | |
| 294 | - | return fmt.Errorf("failed to pull image %s with auth: %w", imageRef, err) | |
| 295 | - | } | |
| 296 | - | defer reader.Close() | |
| 297 | - | ||
| 298 | - | // Read the response to completion (this is required for the pull to complete) | |
| 299 | - | _, err = io.Copy(io.Discard, reader) | |
| 300 | - | if err != nil { | |
| 301 | - | return fmt.Errorf("failed to read pull response for image %s: %w", imageRef, err) | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | log.Info().Str("image", imageRef).Msg("Image pulled successfully with authentication") | |
| 305 | - | return nil | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | // RemoveImage removes an image | |
| 309 | - | func (d *DockerRuntime) RemoveImage(ctx context.Context, imageRef string, force bool) error { | |
| 310 | - | _, err := d.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force}) | |
| 311 | - | if err != nil { | |
| 312 | - | return fmt.Errorf("failed to remove image %s: %w", imageRef, err) | |
| 313 | - | } | |
| 314 | - | ||
| 315 | - | log.Info().Str("image", imageRef).Bool("force", force).Msg("Image removed") | |
| 316 | - | return nil | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | // ListImages lists images | |
| 320 | - | func (d *DockerRuntime) ListImages(ctx context.Context) ([]string, error) { | |
| 321 | - | images, err := d.client.ImageList(ctx, image.ListOptions{}) | |
| 322 | - | if err != nil { | |
| 323 | - | return nil, fmt.Errorf("failed to list images: %w", err) | |
| 324 | - | } | |
| 325 | - | ||
| 326 | - | var result []string | |
| 327 | - | for _, img := range images { | |
| 328 | - | for _, tag := range img.RepoTags { | |
| 329 | - | if tag != "<none>:<none>" { | |
| 330 | - | result = append(result, tag) | |
| 331 | - | } | |
| 332 | - | } | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | return result, nil | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | // Ping checks if Docker is responsive | |
| 339 | - | func (d *DockerRuntime) Ping(ctx context.Context) error { | |
| 340 | - | _, err := d.client.Ping(ctx) | |
| 341 | - | if err != nil { | |
| 342 | - | return fmt.Errorf("Docker ping failed: %w", err) | |
| 343 | - | } | |
| 344 | - | return nil | |
| 345 | - | } | |
| 346 | - | ||
| 347 | - | // Version returns Docker version | |
| 348 | - | func (d *DockerRuntime) Version(ctx context.Context) (string, error) { | |
| 349 | - | version, err := d.client.ServerVersion(ctx) | |
| 350 | - | if err != nil { | |
| 351 | - | return "", fmt.Errorf("failed to get Docker version: %w", err) | |
| 352 | - | } | |
| 353 | - | return version.Version, nil | |
| 354 | - | } | |
| 355 | - | ||
| 356 | - | // IsContainerRunning checks if a container is running | |
| 357 | - | func (d *DockerRuntime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) { | |
| 358 | - | container, err := d.InspectContainer(ctx, containerID) | |
| 359 | - | if err != nil { | |
| 360 | - | return false, err | |
| 361 | - | } | |
| 362 | - | return container.Status == "running", nil | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | // GetContainerPort gets the host port for a container's internal port | |
| 366 | - | func (d *DockerRuntime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) { | |
| 367 | - | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 368 | - | if err != nil { | |
| 369 | - | return 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { | |
| 373 | - | return 0, fmt.Errorf("no port mappings found for container %s", containerID) | |
| 374 | - | } | |
| 375 | - | ||
| 376 | - | containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort)) | |
| 377 | - | bindings, exists := resp.NetworkSettings.Ports[containerPort] | |
| 378 | - | if !exists || len(bindings) == 0 { | |
| 379 | - | return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) | |
| 380 | - | } | |
| 381 | - | ||
| 382 | - | hostPort, err := strconv.Atoi(bindings[0].HostPort) | |
| 383 | - | if err != nil { | |
| 384 | - | return 0, fmt.Errorf("invalid host port for container %s: %w", containerID, err) | |
| 385 | - | } | |
| 386 | - | ||
| 387 | - | return hostPort, nil | |
| 388 | - | } | |
| 389 | - | ||
| 390 | - | // GetImageExposedPorts gets the exposed ports from an image | |
| 391 | - | func (d *DockerRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { | |
| 392 | - | imageInspect, _, err := d.client.ImageInspectWithRaw(ctx, imageRef) | |
| 393 | - | if err != nil { | |
| 394 | - | return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) | |
| 395 | - | } | |
| 396 | - | ||
| 397 | - | var ports []int | |
| 398 | - | if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil { | |
| 399 | - | for portSpec := range imageInspect.Config.ExposedPorts { | |
| 400 | - | // Parse port from format like "80/tcp" | |
| 401 | - | portStr := strings.Split(string(portSpec), "/")[0] | |
| 402 | - | if port, err := strconv.Atoi(portStr); err == nil { | |
| 403 | - | ports = append(ports, port) | |
| 404 | - | } | |
| 405 | - | } | |
| 406 | - | } | |
| 407 | - | ||
| 408 | - | if len(ports) == 0 { | |
| 409 | - | return nil, fmt.Errorf("no EXPOSE directives found in image %s - please add EXPOSE directive to Dockerfile", imageRef) | |
| 410 | - | } | |
| 411 | - | ||
| 412 | - | log.Info().Str("image", imageRef).Ints("exposed_ports", ports).Msg("Found exposed ports in image") | |
| 413 | - | return ports, nil | |
| 414 | - | } | |
| 415 | - | ||
| 416 | - | // GetContainerExposedPorts gets all exposed ports from a running container | |
| 417 | - | func (d *DockerRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { | |
| 418 | - | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 419 | - | if err != nil { | |
| 420 | - | return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 421 | - | } | |
| 422 | - | ||
| 423 | - | var ports []int | |
| 424 | - | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 425 | - | for portSpec := range resp.NetworkSettings.Ports { | |
| 426 | - | // Parse port from format like "80/tcp" | |
| 427 | - | portStr := strings.Split(string(portSpec), "/")[0] | |
| 428 | - | if port, err := strconv.Atoi(portStr); err == nil { | |
| 429 | - | ports = append(ports, port) | |
| 430 | - | } | |
| 431 | - | } | |
| 432 | - | } | |
| 433 | - | ||
| 434 | - | return ports, nil | |
| 435 | - | } | |
| 436 | - | ||
| 437 | - | // GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use | |
| 438 | - | func (d *DockerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |
| 439 | - | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 440 | - | if err != nil { | |
| 441 | - | return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 442 | - | } | |
| 443 | - | ||
| 444 | - | // Get container's internal IP address | |
| 445 | - | var containerIP string | |
| 446 | - | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | |
| 447 | - | // Use the first available network (usually bridge or custom network) | |
| 448 | - | for _, network := range resp.NetworkSettings.Networks { | |
| 449 | - | if network.IPAddress != "" { | |
| 450 | - | containerIP = network.IPAddress | |
| 451 | - | break | |
| 452 | - | } | |
| 453 | - | } | |
| 454 | - | } | |
| 455 | - | ||
| 456 | - | if containerIP == "" { | |
| 457 | - | return "", 0, fmt.Errorf("no IP address found for container %s", containerID) | |
| 458 | - | } | |
| 459 | - | ||
| 460 | - | // Get exposed ports from container | |
| 461 | - | var exposedPorts []int | |
| 462 | - | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 463 | - | for portSpec := range resp.NetworkSettings.Ports { | |
| 464 | - | // Parse port from format like "80/tcp" | |
| 465 | - | portStr := strings.Split(string(portSpec), "/")[0] | |
| 466 | - | if port, err := strconv.Atoi(portStr); err == nil { | |
| 467 | - | exposedPorts = append(exposedPorts, port) | |
| 468 | - | } | |
| 469 | - | } | |
| 470 | - | } | |
| 471 | - | ||
| 472 | - | if len(exposedPorts) == 0 { | |
| 473 | - | return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID) | |
| 474 | - | } | |
| 475 | - | ||
| 476 | - | // Use the first exposed port | |
| 477 | - | selectedPort := exposedPorts[0] | |
| 478 | - | ||
| 479 | - | log.Info(). | |
| 480 | - | Str("container_id", containerID). | |
| 481 | - | Str("ip", containerIP). | |
| 482 | - | Int("selected_port", selectedPort). | |
| 483 | - | Ints("exposed_ports", exposedPorts). | |
| 484 | - | Msg("Container network info detected") | |
| 485 | - | ||
| 486 | - | return containerIP, selectedPort, nil | |
| 487 | - | } | |
| 488 | - | ||
| 489 | - | // InspectImageVolumes gets the volume mount points declared in the image | |
| 490 | - | func (d *DockerRuntime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | |
| 491 | - | imageInspect, _, err := d.client.ImageInspectWithRaw(ctx, imageRef) | |
| 492 | - | if err != nil { | |
| 493 | - | return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) | |
| 494 | - | } | |
| 495 | - | ||
| 496 | - | var volumes []string | |
| 497 | - | if imageInspect.Config != nil && imageInspect.Config.Volumes != nil { | |
| 498 | - | for volumePath := range imageInspect.Config.Volumes { | |
| 499 | - | volumes = append(volumes, volumePath) | |
| 500 | - | } | |
| 501 | - | } | |
| 502 | - | ||
| 503 | - | if len(volumes) > 0 { | |
| 504 | - | log.Info().Str("image", imageRef).Strs("volumes", volumes).Msg("Found VOLUME directives in image") | |
| 505 | - | } else { | |
| 506 | - | log.Debug().Str("image", imageRef).Msg("No VOLUME directives found in image") | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | return volumes, nil | |
| 510 | - | } | |
| 511 | - | ||
| 512 | - | // VolumeExists checks if a Docker volume exists | |
| 513 | - | func (d *DockerRuntime) VolumeExists(ctx context.Context, volumeName string) (bool, error) { | |
| 514 | - | _, err := d.client.VolumeInspect(ctx, volumeName) | |
| 515 | - | if err != nil { | |
| 516 | - | if errdefs.IsNotFound(err) { | |
| 517 | - | return false, nil | |
| 518 | - | } | |
| 519 | - | return false, fmt.Errorf("failed to inspect volume %s: %w", volumeName, err) | |
| 520 | - | } | |
| 521 | - | return true, nil | |
| 522 | - | } | |
| 523 | - | ||
| 524 | - | // CreateVolume creates a new Docker volume | |
| 525 | - | func (d *DockerRuntime) CreateVolume(ctx context.Context, volumeName string) error { | |
| 526 | - | _, err := d.client.VolumeCreate(ctx, volume.CreateOptions{ | |
| 527 | - | Name: volumeName, | |
| 528 | - | Labels: map[string]string{ | |
| 529 | - | "gordon.managed": "true", | |
| 530 | - | "gordon.created": "auto", | |
| 531 | - | }, | |
| 532 | - | }) | |
| 533 | - | if err != nil { | |
| 534 | - | return fmt.Errorf("failed to create volume %s: %w", volumeName, err) | |
| 535 | - | } | |
| 536 | - | ||
| 537 | - | log.Info().Str("volume", volumeName).Msg("Volume created") | |
| 538 | - | return nil | |
| 539 | - | } | |
| 540 | - | ||
| 541 | - | // RemoveVolume removes a Docker volume | |
| 542 | - | func (d *DockerRuntime) RemoveVolume(ctx context.Context, volumeName string, force bool) error { | |
| 543 | - | err := d.client.VolumeRemove(ctx, volumeName, force) | |
| 544 | - | if err != nil { | |
| 545 | - | if errdefs.IsNotFound(err) { | |
| 546 | - | log.Debug().Str("volume", volumeName).Msg("Volume not found, already removed") | |
| 547 | - | return nil | |
| 548 | - | } | |
| 549 | - | return fmt.Errorf("failed to remove volume %s: %w", volumeName, err) | |
| 550 | - | } | |
| 551 | - | ||
| 552 | - | log.Info().Str("volume", volumeName).Msg("Volume removed") | |
| 553 | - | return nil | |
| 554 | - | } | |
| 555 | - | ||
| 556 | - | // InspectImageEnv gets the environment variables declared in the image | |
| 557 | - | func (d *DockerRuntime) InspectImageEnv(ctx context.Context, imageRef string) ([]string, error) { | |
| 558 | - | imageInspect, _, err := d.client.ImageInspectWithRaw(ctx, imageRef) | |
| 559 | - | if err != nil { | |
| 560 | - | return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) | |
| 561 | - | } | |
| 562 | - | ||
| 563 | - | var envVars []string | |
| 564 | - | if imageInspect.Config != nil && imageInspect.Config.Env != nil { | |
| 565 | - | envVars = imageInspect.Config.Env | |
| 566 | - | } | |
| 567 | - | ||
| 568 | - | if len(envVars) > 0 { | |
| 569 | - | log.Debug().Str("image", imageRef).Strs("env_vars", envVars).Msg("Found ENV directives in image") | |
| 570 | - | } else { | |
| 571 | - | log.Debug().Str("image", imageRef).Msg("No ENV directives found in image") | |
| 572 | - | } | |
| 573 | - | ||
| 574 | - | return envVars, nil | |
| 575 | - | } | |
| 576 | - | ||
| 577 | - | // CreateNetwork creates a new Docker network | |
| 578 | - | func (d *DockerRuntime) CreateNetwork(ctx context.Context, name string, options map[string]string) error { | |
| 579 | - | // Set default driver to bridge if not specified | |
| 580 | - | driver := "bridge" | |
| 581 | - | if driverOption, exists := options["driver"]; exists { | |
| 582 | - | driver = driverOption | |
| 583 | - | } | |
| 584 | - | ||
| 585 | - | createOptions := network.CreateOptions{ | |
| 586 | - | Driver: driver, | |
| 587 | - | Labels: map[string]string{ | |
| 588 | - | "gordon.managed": "true", | |
| 589 | - | }, | |
| 590 | - | } | |
| 591 | - | ||
| 592 | - | // Add any additional options to labels | |
| 593 | - | for key, value := range options { | |
| 594 | - | if key != "driver" { | |
| 595 | - | createOptions.Labels["gordon."+key] = value | |
| 596 | - | } | |
| 597 | - | } | |
| 598 | - | ||
| 599 | - | _, err := d.client.NetworkCreate(ctx, name, createOptions) | |
| 600 | - | if err != nil { | |
| 601 | - | return fmt.Errorf("failed to create network %s: %w", name, err) | |
| 602 | - | } | |
| 603 | - | ||
| 604 | - | log.Info().Str("network", name).Str("driver", driver).Msg("Network created") | |
| 605 | - | return nil | |
| 606 | - | } | |
| 607 | - | ||
| 608 | - | // RemoveNetwork removes a Docker network | |
| 609 | - | func (d *DockerRuntime) RemoveNetwork(ctx context.Context, name string) error { | |
| 610 | - | err := d.client.NetworkRemove(ctx, name) | |
| 611 | - | if err != nil { | |
| 612 | - | if errdefs.IsNotFound(err) { | |
| 613 | - | log.Debug().Str("network", name).Msg("Network not found, already removed") | |
| 614 | - | return nil | |
| 615 | - | } | |
| 616 | - | return fmt.Errorf("failed to remove network %s: %w", name, err) | |
| 617 | - | } | |
| 618 | - | ||
| 619 | - | log.Info().Str("network", name).Msg("Network removed") | |
| 620 | - | return nil | |
| 621 | - | } | |
| 622 | - | ||
| 623 | - | // ListNetworks lists all Docker networks | |
| 624 | - | func (d *DockerRuntime) ListNetworks(ctx context.Context) ([]*runtime.NetworkInfo, error) { | |
| 625 | - | networks, err := d.client.NetworkList(ctx, network.ListOptions{}) | |
| 626 | - | if err != nil { | |
| 627 | - | return nil, fmt.Errorf("failed to list networks: %w", err) | |
| 628 | - | } | |
| 629 | - | ||
| 630 | - | var result []*runtime.NetworkInfo | |
| 631 | - | for _, net := range networks { | |
| 632 | - | var containers []string | |
| 633 | - | for containerID := range net.Containers { | |
| 634 | - | containers = append(containers, containerID) | |
| 635 | - | } | |
| 636 | - | ||
| 637 | - | result = append(result, &runtime.NetworkInfo{ | |
| 638 | - | ID: net.ID, | |
| 639 | - | Name: net.Name, | |
| 640 | - | Driver: net.Driver, | |
| 641 | - | Containers: containers, | |
| 642 | - | Labels: net.Labels, | |
| 643 | - | }) | |
| 644 | - | } | |
| 645 | - | ||
| 646 | - | return result, nil | |
| 647 | - | } | |
| 648 | - | ||
| 649 | - | // NetworkExists checks if a Docker network exists | |
| 650 | - | func (d *DockerRuntime) NetworkExists(ctx context.Context, name string) (bool, error) { | |
| 651 | - | _, err := d.client.NetworkInspect(ctx, name, network.InspectOptions{}) | |
| 652 | - | if err != nil { | |
| 653 | - | if errdefs.IsNotFound(err) { | |
| 654 | - | return false, nil | |
| 655 | - | } | |
| 656 | - | return false, fmt.Errorf("failed to inspect network %s: %w", name, err) | |
| 657 | - | } | |
| 658 | - | return true, nil | |
| 659 | - | } | |
| 660 | - | ||
| 661 | - | // ConnectContainerToNetwork connects a container to a network | |
| 662 | - | func (d *DockerRuntime) ConnectContainerToNetwork(ctx context.Context, containerName, networkName string) error { | |
| 663 | - | err := d.client.NetworkConnect(ctx, networkName, containerName, &network.EndpointSettings{}) | |
| 664 | - | if err != nil { | |
| 665 | - | return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err) | |
| 666 | - | } | |
| 667 | - | ||
| 668 | - | log.Info().Str("container", containerName).Str("network", networkName).Msg("Container connected to network") | |
| 669 | - | return nil | |
| 670 | - | } | |
| 671 | - | ||
| 672 | - | // DisconnectContainerFromNetwork disconnects a container from a network | |
| 673 | - | func (d *DockerRuntime) DisconnectContainerFromNetwork(ctx context.Context, containerName, networkName string) error { | |
| 674 | - | err := d.client.NetworkDisconnect(ctx, networkName, containerName, false) | |
| 675 | - | if err != nil { | |
| 676 | - | return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err) | |
| 677 | - | } | |
| 678 | - | ||
| 679 | - | log.Info().Str("container", containerName).Str("network", networkName).Msg("Container disconnected from network") | |
| 680 | - | return nil | |
| 681 | - | } | |
D internal/container/factory.go +0 -263
| @@ -1,263 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "os" | |
| 7 | - | "os/user" | |
| 8 | - | "strings" | |
| 9 | - | ||
| 10 | - | "github.com/docker/docker/client" | |
| 11 | - | "github.com/rs/zerolog/log" | |
| 12 | - | ||
| 13 | - | "gordon/internal/config" | |
| 14 | - | "gordon/pkg/runtime" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | // RuntimeType represents the container runtime type | |
| 18 | - | type RuntimeType string | |
| 19 | - | ||
| 20 | - | const ( | |
| 21 | - | RuntimeAuto RuntimeType = "auto" | |
| 22 | - | RuntimeDocker RuntimeType = "docker" | |
| 23 | - | RuntimePodman RuntimeType = "podman" | |
| 24 | - | RuntimePodmanRootless RuntimeType = "podman-rootless" | |
| 25 | - | ) | |
| 26 | - | ||
| 27 | - | // RuntimeInfo contains information about a detected runtime | |
| 28 | - | type RuntimeInfo struct { | |
| 29 | - | Type RuntimeType | |
| 30 | - | SocketPath string | |
| 31 | - | Version string | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | // CreateRuntime creates a container runtime based on configuration | |
| 35 | - | func CreateRuntime(ctx context.Context, cfg *config.Config) (runtime.Runtime, error) { | |
| 36 | - | runtimeType := RuntimeType(cfg.Server.Runtime) | |
| 37 | - | ||
| 38 | - | // Get socket path from config or environment | |
| 39 | - | socketPath := getSocketPath(cfg.Server.SocketPath) | |
| 40 | - | ||
| 41 | - | switch runtimeType { | |
| 42 | - | case RuntimeAuto: | |
| 43 | - | return createAutoRuntime(ctx, socketPath) | |
| 44 | - | case RuntimeDocker: | |
| 45 | - | return createDockerRuntime(ctx, socketPath) | |
| 46 | - | case RuntimePodman, RuntimePodmanRootless: | |
| 47 | - | return createPodmanRuntime(ctx, socketPath, runtimeType == RuntimePodmanRootless) | |
| 48 | - | default: | |
| 49 | - | return nil, fmt.Errorf("unsupported runtime: %s", runtimeType) | |
| 50 | - | } | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // getSocketPath determines the socket path to use | |
| 54 | - | func getSocketPath(configPath string) string { | |
| 55 | - | // 1. Use config if provided | |
| 56 | - | if configPath != "" { | |
| 57 | - | log.Debug().Str("path", configPath).Msg("Using socket path from config") | |
| 58 | - | return configPath | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | // 2. Check CONTAINER_HOST environment variable | |
| 62 | - | if envPath := os.Getenv("CONTAINER_HOST"); envPath != "" { | |
| 63 | - | log.Debug().Str("path", envPath).Msg("Using socket path from CONTAINER_HOST") | |
| 64 | - | return envPath | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // 3. Return empty string for auto-detection | |
| 68 | - | return "" | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | // createAutoRuntime automatically detects and creates the best available runtime | |
| 72 | - | func createAutoRuntime(ctx context.Context, customSocketPath string) (runtime.Runtime, error) { | |
| 73 | - | var candidatePaths []string | |
| 74 | - | ||
| 75 | - | if customSocketPath != "" { | |
| 76 | - | // If custom path provided, try it first | |
| 77 | - | candidatePaths = []string{customSocketPath} | |
| 78 | - | } else { | |
| 79 | - | // Auto-detect socket paths | |
| 80 | - | candidatePaths = getDefaultSocketPaths() | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | // Try each socket path | |
| 84 | - | for _, socketPath := range candidatePaths { | |
| 85 | - | if rt, info, err := tryCreateRuntime(ctx, socketPath); err == nil { | |
| 86 | - | log.Info(). | |
| 87 | - | Str("runtime", string(info.Type)). | |
| 88 | - | Str("socket", info.SocketPath). | |
| 89 | - | Str("version", info.Version). | |
| 90 | - | Msg("Container runtime auto-detected") | |
| 91 | - | return rt, nil | |
| 92 | - | } | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | return nil, fmt.Errorf("no container runtime found. Tried paths: %v", candidatePaths) | |
| 96 | - | } | |
| 97 | - | ||
| 98 | - | // createDockerRuntime creates a Docker runtime with optional custom socket | |
| 99 | - | func createDockerRuntime(ctx context.Context, socketPath string) (runtime.Runtime, error) { | |
| 100 | - | var opts []client.Opt | |
| 101 | - | opts = append(opts, client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 102 | - | ||
| 103 | - | if socketPath != "" { | |
| 104 | - | opts = append(opts, client.WithHost(socketPath)) | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | cli, err := client.NewClientWithOpts(opts...) | |
| 108 | - | if err != nil { | |
| 109 | - | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | // Test connectivity | |
| 113 | - | if _, err := cli.Ping(ctx); err != nil { | |
| 114 | - | return nil, fmt.Errorf("Docker ping failed: %w", err) | |
| 115 | - | } | |
| 116 | - | ||
| 117 | - | return &DockerRuntime{client: cli}, nil | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | // createPodmanRuntime creates a Podman runtime with optional custom socket | |
| 121 | - | func createPodmanRuntime(ctx context.Context, socketPath string, rootless bool) (runtime.Runtime, error) { | |
| 122 | - | if socketPath == "" { | |
| 123 | - | socketPath = getDefaultPodmanSocket(rootless) | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | return createPodmanRuntimeWithSocket(ctx, socketPath) | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | // tryCreateRuntime attempts to create a runtime for a given socket path | |
| 130 | - | func tryCreateRuntime(ctx context.Context, socketPath string) (runtime.Runtime, *RuntimeInfo, error) { | |
| 131 | - | // Try to create client | |
| 132 | - | opts := []client.Opt{ | |
| 133 | - | client.WithHost(socketPath), | |
| 134 | - | client.WithAPIVersionNegotiation(), | |
| 135 | - | } | |
| 136 | - | ||
| 137 | - | cli, err := client.NewClientWithOpts(opts...) | |
| 138 | - | if err != nil { | |
| 139 | - | return nil, nil, fmt.Errorf("failed to create client: %w", err) | |
| 140 | - | } | |
| 141 | - | ||
| 142 | - | // Test connectivity and get version | |
| 143 | - | ping, err := cli.Ping(ctx) | |
| 144 | - | if err != nil { | |
| 145 | - | return nil, nil, fmt.Errorf("ping failed: %w", err) | |
| 146 | - | } | |
| 147 | - | ||
| 148 | - | // Determine runtime type based on socket path and server info | |
| 149 | - | runtimeType := detectRuntimeType(socketPath) | |
| 150 | - | ||
| 151 | - | // Since Podman uses Docker-compatible API, we can use DockerRuntime for both | |
| 152 | - | rt := &DockerRuntime{client: cli} | |
| 153 | - | ||
| 154 | - | info := &RuntimeInfo{ | |
| 155 | - | Type: runtimeType, | |
| 156 | - | SocketPath: socketPath, | |
| 157 | - | Version: ping.APIVersion, | |
| 158 | - | } | |
| 159 | - | ||
| 160 | - | return rt, info, nil | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // detectRuntimeType determines if the socket is Docker or Podman | |
| 164 | - | func detectRuntimeType(socketPath string) RuntimeType { | |
| 165 | - | // Check socket path patterns | |
| 166 | - | if strings.Contains(socketPath, "podman") { | |
| 167 | - | if strings.Contains(socketPath, "/run/user/") || strings.Contains(socketPath, "$XDG_RUNTIME_DIR") { | |
| 168 | - | return RuntimePodmanRootless | |
| 169 | - | } | |
| 170 | - | return RuntimePodman | |
| 171 | - | } | |
| 172 | - | ||
| 173 | - | if strings.Contains(socketPath, "docker") { | |
| 174 | - | return RuntimeDocker | |
| 175 | - | } | |
| 176 | - | ||
| 177 | - | // Default socket paths | |
| 178 | - | switch socketPath { | |
| 179 | - | case "/var/run/docker.sock", "unix:///var/run/docker.sock": | |
| 180 | - | return RuntimeDocker | |
| 181 | - | case "/run/podman/podman.sock", "unix:///run/podman/podman.sock": | |
| 182 | - | return RuntimePodman | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | // Check if it's in user runtime directory (rootless) | |
| 186 | - | if strings.Contains(socketPath, "/run/user/") { | |
| 187 | - | return RuntimePodmanRootless | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | // Default to Docker for unknown paths | |
| 191 | - | return RuntimeDocker | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | // getDefaultSocketPaths returns the default socket paths to try | |
| 195 | - | func getDefaultSocketPaths() []string { | |
| 196 | - | paths := []string{ | |
| 197 | - | "unix:///var/run/docker.sock", // Docker | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | // Add Podman rootless socket first (preferred) | |
| 201 | - | if rootlessSocket := getDefaultPodmanSocket(true); rootlessSocket != "" { | |
| 202 | - | paths = append(paths, rootlessSocket) | |
| 203 | - | } | |
| 204 | - | ||
| 205 | - | // Add Podman root socket last | |
| 206 | - | paths = append(paths, "unix:///run/podman/podman.sock") | |
| 207 | - | ||
| 208 | - | return paths | |
| 209 | - | } | |
| 210 | - | ||
| 211 | - | // getDefaultPodmanSocket returns the default Podman socket path | |
| 212 | - | func getDefaultPodmanSocket(rootless bool) string { | |
| 213 | - | if !rootless { | |
| 214 | - | return "unix:///run/podman/podman.sock" | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | // Get user runtime directory for rootless Podman | |
| 218 | - | if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" { | |
| 219 | - | return fmt.Sprintf("unix://%s/podman/podman.sock", xdgRuntimeDir) | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | // Fallback: construct from user ID | |
| 223 | - | if currentUser, err := user.Current(); err == nil { | |
| 224 | - | return fmt.Sprintf("unix:///run/user/%s/podman/podman.sock", currentUser.Uid) | |
| 225 | - | } | |
| 226 | - | ||
| 227 | - | log.Warn().Msg("Could not determine rootless Podman socket path") | |
| 228 | - | return "" | |
| 229 | - | } | |
| 230 | - | ||
| 231 | - | // createPodmanRuntimeWithSocket creates a Podman runtime for a specific socket | |
| 232 | - | func createPodmanRuntimeWithSocket(ctx context.Context, socketPath string) (runtime.Runtime, error) { | |
| 233 | - | opts := []client.Opt{ | |
| 234 | - | client.WithHost(socketPath), | |
| 235 | - | client.WithAPIVersionNegotiation(), | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | cli, err := client.NewClientWithOpts(opts...) | |
| 239 | - | if err != nil { | |
| 240 | - | return nil, fmt.Errorf("failed to create Podman client: %w", err) | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | // Test connectivity | |
| 244 | - | if _, err := cli.Ping(ctx); err != nil { | |
| 245 | - | return nil, fmt.Errorf("Podman ping failed: %w", err) | |
| 246 | - | } | |
| 247 | - | ||
| 248 | - | return &DockerRuntime{client: cli}, nil | |
| 249 | - | } | |
| 250 | - | ||
| 251 | - | // GetAvailableRuntimes returns information about all available runtimes | |
| 252 | - | func GetAvailableRuntimes() []RuntimeInfo { | |
| 253 | - | var runtimes []RuntimeInfo | |
| 254 | - | ctx := context.Background() | |
| 255 | - | ||
| 256 | - | for _, socketPath := range getDefaultSocketPaths() { | |
| 257 | - | if _, info, err := tryCreateRuntime(ctx, socketPath); err == nil { | |
| 258 | - | runtimes = append(runtimes, *info) | |
| 259 | - | } | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | return runtimes | |
| 263 | - | } | |
D internal/container/factory_test.go +0 -337
| @@ -1,337 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "os" | |
| 6 | - | "path/filepath" | |
| 7 | - | "strings" | |
| 8 | - | "testing" | |
| 9 | - | ||
| 10 | - | "github.com/stretchr/testify/assert" | |
| 11 | - | "github.com/stretchr/testify/require" | |
| 12 | - | ||
| 13 | - | "gordon/internal/config" | |
| 14 | - | ) | |
| 15 | - | ||
| 16 | - | func TestCreateRuntime(t *testing.T) { | |
| 17 | - | ctx := context.Background() | |
| 18 | - | ||
| 19 | - | tests := []struct { | |
| 20 | - | name string | |
| 21 | - | config *config.Config | |
| 22 | - | expectError bool | |
| 23 | - | expectedError string | |
| 24 | - | }{ | |
| 25 | - | { | |
| 26 | - | name: "docker runtime", | |
| 27 | - | config: &config.Config{ | |
| 28 | - | Server: config.ServerConfig{ | |
| 29 | - | Runtime: "docker", | |
| 30 | - | SocketPath: "/var/run/docker.sock", | |
| 31 | - | }, | |
| 32 | - | }, | |
| 33 | - | expectError: true, // Will fail because Docker isn't running in test | |
| 34 | - | }, | |
| 35 | - | { | |
| 36 | - | name: "podman runtime", | |
| 37 | - | config: &config.Config{ | |
| 38 | - | Server: config.ServerConfig{ | |
| 39 | - | Runtime: "podman", | |
| 40 | - | SocketPath: "/run/user/1000/podman/podman.sock", | |
| 41 | - | }, | |
| 42 | - | }, | |
| 43 | - | expectError: true, // Will fail because Podman isn't running in test | |
| 44 | - | }, | |
| 45 | - | { | |
| 46 | - | name: "auto runtime", | |
| 47 | - | config: &config.Config{ | |
| 48 | - | Server: config.ServerConfig{ | |
| 49 | - | Runtime: "auto", | |
| 50 | - | }, | |
| 51 | - | }, | |
| 52 | - | expectError: false, // May succeed if runtime is available | |
| 53 | - | }, | |
| 54 | - | { | |
| 55 | - | name: "invalid runtime", | |
| 56 | - | config: &config.Config{ | |
| 57 | - | Server: config.ServerConfig{ | |
| 58 | - | Runtime: "invalid", | |
| 59 | - | }, | |
| 60 | - | }, | |
| 61 | - | expectError: true, | |
| 62 | - | expectedError: "unsupported runtime", | |
| 63 | - | }, | |
| 64 | - | } | |
| 65 | - | ||
| 66 | - | for _, tt := range tests { | |
| 67 | - | t.Run(tt.name, func(t *testing.T) { | |
| 68 | - | runtime, err := CreateRuntime(ctx, tt.config) | |
| 69 | - | ||
| 70 | - | if tt.name == "auto runtime" { | |
| 71 | - | // Auto runtime may succeed or fail depending on environment | |
| 72 | - | if err != nil { | |
| 73 | - | assert.Nil(t, runtime) | |
| 74 | - | } else { | |
| 75 | - | assert.NotNil(t, runtime) | |
| 76 | - | } | |
| 77 | - | } else if tt.expectError { | |
| 78 | - | assert.Error(t, err) | |
| 79 | - | if tt.expectedError != "" { | |
| 80 | - | assert.Contains(t, err.Error(), tt.expectedError) | |
| 81 | - | } | |
| 82 | - | assert.Nil(t, runtime) | |
| 83 | - | } else { | |
| 84 | - | assert.NoError(t, err) | |
| 85 | - | assert.NotNil(t, runtime) | |
| 86 | - | } | |
| 87 | - | }) | |
| 88 | - | } | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | func TestGetSocketPath(t *testing.T) { | |
| 92 | - | tests := []struct { | |
| 93 | - | name string | |
| 94 | - | configPath string | |
| 95 | - | setupEnv func() | |
| 96 | - | cleanupEnv func() | |
| 97 | - | expected string | |
| 98 | - | }{ | |
| 99 | - | { | |
| 100 | - | name: "with provided config path", | |
| 101 | - | configPath: "/custom/docker.sock", | |
| 102 | - | setupEnv: func() {}, | |
| 103 | - | cleanupEnv: func() {}, | |
| 104 | - | expected: "/custom/docker.sock", | |
| 105 | - | }, | |
| 106 | - | { | |
| 107 | - | name: "with CONTAINER_HOST env var", | |
| 108 | - | configPath: "", | |
| 109 | - | setupEnv: func() { | |
| 110 | - | os.Setenv("CONTAINER_HOST", "/env/container.sock") | |
| 111 | - | }, | |
| 112 | - | cleanupEnv: func() { | |
| 113 | - | os.Unsetenv("CONTAINER_HOST") | |
| 114 | - | }, | |
| 115 | - | expected: "/env/container.sock", | |
| 116 | - | }, | |
| 117 | - | { | |
| 118 | - | name: "no config or env - returns empty", | |
| 119 | - | configPath: "", | |
| 120 | - | setupEnv: func() { | |
| 121 | - | os.Unsetenv("CONTAINER_HOST") | |
| 122 | - | }, | |
| 123 | - | cleanupEnv: func() {}, | |
| 124 | - | expected: "", | |
| 125 | - | }, | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | for _, tt := range tests { | |
| 129 | - | t.Run(tt.name, func(t *testing.T) { | |
| 130 | - | tt.setupEnv() | |
| 131 | - | defer tt.cleanupEnv() | |
| 132 | - | ||
| 133 | - | result := getSocketPath(tt.configPath) | |
| 134 | - | assert.Equal(t, tt.expected, result) | |
| 135 | - | }) | |
| 136 | - | } | |
| 137 | - | } | |
| 138 | - | ||
| 139 | - | func TestCreateAutoRuntime(t *testing.T) { | |
| 140 | - | ctx := context.Background() | |
| 141 | - | ||
| 142 | - | // Test that createAutoRuntime tries different runtimes | |
| 143 | - | runtime, err := createAutoRuntime(ctx, "") | |
| 144 | - | // In test environment, this may succeed or fail depending on available runtimes | |
| 145 | - | if err != nil { | |
| 146 | - | // If it fails, should be because no runtime is available | |
| 147 | - | assert.Nil(t, runtime) | |
| 148 | - | assert.Contains(t, err.Error(), "no container runtime available") | |
| 149 | - | } else { | |
| 150 | - | // If it succeeds, should return a runtime | |
| 151 | - | assert.NotNil(t, runtime) | |
| 152 | - | } | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | func TestCreateDockerRuntime(t *testing.T) { | |
| 156 | - | ctx := context.Background() | |
| 157 | - | ||
| 158 | - | tests := []struct { | |
| 159 | - | name string | |
| 160 | - | socketPath string | |
| 161 | - | expectError bool | |
| 162 | - | }{ | |
| 163 | - | { | |
| 164 | - | name: "default docker socket", | |
| 165 | - | socketPath: "/var/run/docker.sock", | |
| 166 | - | expectError: true, // Will fail because Docker isn't running | |
| 167 | - | }, | |
| 168 | - | { | |
| 169 | - | name: "custom socket path", | |
| 170 | - | socketPath: "/custom/docker.sock", | |
| 171 | - | expectError: true, // Will fail because socket doesn't exist | |
| 172 | - | }, | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | for _, tt := range tests { | |
| 176 | - | t.Run(tt.name, func(t *testing.T) { | |
| 177 | - | runtime, err := createDockerRuntime(ctx, tt.socketPath) | |
| 178 | - | ||
| 179 | - | if tt.expectError { | |
| 180 | - | assert.Error(t, err) | |
| 181 | - | assert.Nil(t, runtime) | |
| 182 | - | } else { | |
| 183 | - | assert.NoError(t, err) | |
| 184 | - | assert.NotNil(t, runtime) | |
| 185 | - | } | |
| 186 | - | }) | |
| 187 | - | } | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | func TestCreatePodmanRuntime(t *testing.T) { | |
| 191 | - | ctx := context.Background() | |
| 192 | - | ||
| 193 | - | tests := []struct { | |
| 194 | - | name string | |
| 195 | - | socketPath string | |
| 196 | - | rootless bool | |
| 197 | - | expectError bool | |
| 198 | - | }{ | |
| 199 | - | { | |
| 200 | - | name: "custom podman socket", | |
| 201 | - | socketPath: "/custom/podman.sock", | |
| 202 | - | rootless: false, | |
| 203 | - | expectError: true, // Will fail because socket doesn't exist | |
| 204 | - | }, | |
| 205 | - | { | |
| 206 | - | name: "rootless podman socket", | |
| 207 | - | socketPath: "/custom/podman.sock", | |
| 208 | - | rootless: true, | |
| 209 | - | expectError: true, // Will fail because socket doesn't exist | |
| 210 | - | }, | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | for _, tt := range tests { | |
| 214 | - | t.Run(tt.name, func(t *testing.T) { | |
| 215 | - | runtime, err := createPodmanRuntime(ctx, tt.socketPath, tt.rootless) | |
| 216 | - | ||
| 217 | - | if tt.expectError { | |
| 218 | - | assert.Error(t, err) | |
| 219 | - | assert.Nil(t, runtime) | |
| 220 | - | } else { | |
| 221 | - | assert.NoError(t, err) | |
| 222 | - | assert.NotNil(t, runtime) | |
| 223 | - | } | |
| 224 | - | }) | |
| 225 | - | } | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | func TestDetectRuntimeType(t *testing.T) { | |
| 229 | - | tests := []struct { | |
| 230 | - | name string | |
| 231 | - | socketPath string | |
| 232 | - | expected RuntimeType | |
| 233 | - | }{ | |
| 234 | - | { | |
| 235 | - | name: "docker socket", | |
| 236 | - | socketPath: "/var/run/docker.sock", | |
| 237 | - | expected: RuntimeDocker, | |
| 238 | - | }, | |
| 239 | - | { | |
| 240 | - | name: "docker unix socket", | |
| 241 | - | socketPath: "unix:///var/run/docker.sock", | |
| 242 | - | expected: RuntimeDocker, | |
| 243 | - | }, | |
| 244 | - | { | |
| 245 | - | name: "podman socket", | |
| 246 | - | socketPath: "/run/podman/podman.sock", | |
| 247 | - | expected: RuntimePodman, | |
| 248 | - | }, | |
| 249 | - | { | |
| 250 | - | name: "rootless podman socket", | |
| 251 | - | socketPath: "/run/user/1000/podman/podman.sock", | |
| 252 | - | expected: RuntimePodmanRootless, | |
| 253 | - | }, | |
| 254 | - | { | |
| 255 | - | name: "unknown socket defaults to docker", | |
| 256 | - | socketPath: "/unknown/socket.sock", | |
| 257 | - | expected: RuntimeDocker, | |
| 258 | - | }, | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | for _, tt := range tests { | |
| 262 | - | t.Run(tt.name, func(t *testing.T) { | |
| 263 | - | result := detectRuntimeType(tt.socketPath) | |
| 264 | - | assert.Equal(t, tt.expected, result) | |
| 265 | - | }) | |
| 266 | - | } | |
| 267 | - | } | |
| 268 | - | ||
| 269 | - | func TestGetDefaultSocketPaths(t *testing.T) { | |
| 270 | - | paths := getDefaultSocketPaths() | |
| 271 | - | ||
| 272 | - | // Should return common socket paths | |
| 273 | - | assert.True(t, len(paths) > 0) | |
| 274 | - | ||
| 275 | - | // Should contain at least one Docker socket path | |
| 276 | - | found := false | |
| 277 | - | for _, path := range paths { | |
| 278 | - | if strings.Contains(path, "docker") { | |
| 279 | - | found = true | |
| 280 | - | break | |
| 281 | - | } | |
| 282 | - | } | |
| 283 | - | assert.True(t, found, "Should contain a Docker socket path") | |
| 284 | - | } | |
| 285 | - | ||
| 286 | - | func TestGetDefaultPodmanSocket(t *testing.T) { | |
| 287 | - | socket := getDefaultPodmanSocket(false) // Regular mode | |
| 288 | - | ||
| 289 | - | // Should return a valid path | |
| 290 | - | assert.NotEmpty(t, socket) | |
| 291 | - | ||
| 292 | - | // Should contain "podman" in the path | |
| 293 | - | assert.Contains(t, socket, "podman") | |
| 294 | - | ||
| 295 | - | // Test rootless mode | |
| 296 | - | rootlessSocket := getDefaultPodmanSocket(true) | |
| 297 | - | assert.NotEmpty(t, rootlessSocket) | |
| 298 | - | assert.Contains(t, rootlessSocket, "podman") | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | func TestGetAvailableRuntimes(t *testing.T) { | |
| 302 | - | runtimes := GetAvailableRuntimes() | |
| 303 | - | ||
| 304 | - | // Should return a slice of runtime info | |
| 305 | - | assert.IsType(t, []RuntimeInfo{}, runtimes) | |
| 306 | - | ||
| 307 | - | // Should not be empty | |
| 308 | - | assert.True(t, len(runtimes) > 0) | |
| 309 | - | ||
| 310 | - | // Each runtime should have basic info | |
| 311 | - | for _, rt := range runtimes { | |
| 312 | - | assert.NotEmpty(t, rt.Type) | |
| 313 | - | assert.NotEmpty(t, rt.SocketPath) | |
| 314 | - | // Version may be empty if runtime not actually available | |
| 315 | - | } | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | // Test socket existence checking (private function behavior via public interface) | |
| 319 | - | func TestSocketExistenceChecking(t *testing.T) { | |
| 320 | - | ctx := context.Background() | |
| 321 | - | ||
| 322 | - | // Create a temporary socket file | |
| 323 | - | tmpDir := t.TempDir() | |
| 324 | - | socketPath := filepath.Join(tmpDir, "test.sock") | |
| 325 | - | ||
| 326 | - | // Create an empty file to simulate a socket | |
| 327 | - | file, err := os.Create(socketPath) | |
| 328 | - | require.NoError(t, err) | |
| 329 | - | file.Close() | |
| 330 | - | ||
| 331 | - | // Test with existing file (won't be a real socket, but tests file existence) | |
| 332 | - | _, err = createDockerRuntime(ctx, socketPath) | |
| 333 | - | // This will still fail because it's not a real Docker socket, but for a different reason | |
| 334 | - | assert.Error(t, err) | |
| 335 | - | // Error should not be about file not existing | |
| 336 | - | assert.NotContains(t, err.Error(), "no such file") | |
| 337 | - | }⏎̸ | |
D internal/container/helpers.go +0 -87
| @@ -1,87 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "crypto/sha256" | |
| 5 | - | "fmt" | |
| 6 | - | "regexp" | |
| 7 | - | "strings" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // generateVolumeName creates a deterministic volume name for a domain and path | |
| 11 | - | func generateVolumeName(prefix, domain, path string) string { | |
| 12 | - | // Sanitize domain name for use in volume name | |
| 13 | - | sanitizedDomain := sanitizeDomainForVolume(domain) | |
| 14 | - | ||
| 15 | - | // Create a short hash of the path to handle long paths and special characters | |
| 16 | - | hasher := sha256.New() | |
| 17 | - | hasher.Write([]byte(path)) | |
| 18 | - | pathHash := fmt.Sprintf("%x", hasher.Sum(nil))[:8] | |
| 19 | - | ||
| 20 | - | return fmt.Sprintf("%s-%s-%s", prefix, sanitizedDomain, pathHash) | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | // sanitizeDomainForVolume converts domain to volume-safe format | |
| 24 | - | func sanitizeDomainForVolume(domain string) string { | |
| 25 | - | // Replace dots and other special characters with hyphens | |
| 26 | - | reg := regexp.MustCompile(`[^a-zA-Z0-9\-]`) | |
| 27 | - | sanitized := reg.ReplaceAllString(domain, "-") | |
| 28 | - | ||
| 29 | - | // Remove leading/trailing hyphens and convert to lowercase | |
| 30 | - | sanitized = strings.Trim(sanitized, "-") | |
| 31 | - | sanitized = strings.ToLower(sanitized) | |
| 32 | - | ||
| 33 | - | return sanitized | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | // normalizeImageRef normalizes image references for comparison | |
| 37 | - | func normalizeImageRef(imageRef string) string { | |
| 38 | - | // Split image and tag | |
| 39 | - | parts := strings.Split(imageRef, ":") | |
| 40 | - | image := parts[0] | |
| 41 | - | tag := "latest" | |
| 42 | - | if len(parts) > 1 { | |
| 43 | - | tag = parts[1] | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | // Normalize Docker Hub references | |
| 47 | - | if !strings.Contains(image, "/") { | |
| 48 | - | // Official library image (e.g., "nginx" -> "docker.io/library/nginx") | |
| 49 | - | image = "docker.io/library/" + image | |
| 50 | - | } else if strings.Count(image, "/") == 1 && !strings.Contains(strings.Split(image, "/")[0], ".") { | |
| 51 | - | // User repository (e.g., "user/repo" -> "docker.io/user/repo") | |
| 52 | - | image = "docker.io/" + image | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | return image + ":" + tag | |
| 56 | - | } | |
| 57 | - | ||
| 58 | - | // mergeEnvironmentVariables merges Dockerfile ENV directives with user-provided env vars | |
| 59 | - | // User-provided env vars take precedence over Dockerfile ENV directives | |
| 60 | - | func mergeEnvironmentVariables(dockerfileEnv, userEnv []string) []string { | |
| 61 | - | // Create a map to store environment variables by key | |
| 62 | - | envMap := make(map[string]string) | |
| 63 | - | ||
| 64 | - | // First, add Dockerfile ENV directives | |
| 65 | - | for _, env := range dockerfileEnv { | |
| 66 | - | parts := strings.SplitN(env, "=", 2) | |
| 67 | - | if len(parts) == 2 { | |
| 68 | - | envMap[parts[0]] = parts[1] | |
| 69 | - | } | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | // Then, add user-provided env vars (these override Dockerfile ENV) | |
| 73 | - | for _, env := range userEnv { | |
| 74 | - | parts := strings.SplitN(env, "=", 2) | |
| 75 | - | if len(parts) == 2 { | |
| 76 | - | envMap[parts[0]] = parts[1] | |
| 77 | - | } | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | // Convert back to slice format | |
| 81 | - | var result []string | |
| 82 | - | for key, value := range envMap { | |
| 83 | - | result = append(result, fmt.Sprintf("%s=%s", key, value)) | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | return result | |
| 87 | - | } | |
D internal/container/helpers_test.go +0 -198
| @@ -1,198 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "strings" | |
| 5 | - | "testing" | |
| 6 | - | ||
| 7 | - | "github.com/stretchr/testify/assert" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | func TestNormalizeImageRef(t *testing.T) { | |
| 11 | - | testCases := []struct { | |
| 12 | - | name string | |
| 13 | - | input string | |
| 14 | - | expected string | |
| 15 | - | }{ | |
| 16 | - | { | |
| 17 | - | name: "simple image with tag", | |
| 18 | - | input: "nginx:latest", | |
| 19 | - | expected: "docker.io/library/nginx:latest", | |
| 20 | - | }, | |
| 21 | - | { | |
| 22 | - | name: "image without tag", | |
| 23 | - | input: "nginx", | |
| 24 | - | expected: "docker.io/library/nginx:latest", | |
| 25 | - | }, | |
| 26 | - | { | |
| 27 | - | name: "registry with image and tag", | |
| 28 | - | input: "registry.example.com/nginx:v1.0", | |
| 29 | - | expected: "registry.example.com/nginx:v1.0", | |
| 30 | - | }, | |
| 31 | - | { | |
| 32 | - | name: "dockerhub image with user", | |
| 33 | - | input: "user/repo:tag", | |
| 34 | - | expected: "docker.io/user/repo:tag", | |
| 35 | - | }, | |
| 36 | - | { | |
| 37 | - | name: "official library image without tag", | |
| 38 | - | input: "postgres", | |
| 39 | - | expected: "docker.io/library/postgres:latest", | |
| 40 | - | }, | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | for _, tc := range testCases { | |
| 44 | - | t.Run(tc.name, func(t *testing.T) { | |
| 45 | - | result := normalizeImageRef(tc.input) | |
| 46 | - | assert.Equal(t, tc.expected, result) | |
| 47 | - | }) | |
| 48 | - | } | |
| 49 | - | } | |
| 50 | - | ||
| 51 | - | func TestGenerateVolumeName(t *testing.T) { | |
| 52 | - | testCases := []struct { | |
| 53 | - | name string | |
| 54 | - | prefix string | |
| 55 | - | domain string | |
| 56 | - | path string | |
| 57 | - | }{ | |
| 58 | - | { | |
| 59 | - | name: "basic volume name", | |
| 60 | - | prefix: "gordon", | |
| 61 | - | domain: "app.example.com", | |
| 62 | - | path: "/data", | |
| 63 | - | }, | |
| 64 | - | { | |
| 65 | - | name: "complex path", | |
| 66 | - | prefix: "test", | |
| 67 | - | domain: "my-app.domain.com", | |
| 68 | - | path: "/var/www/html/uploads", | |
| 69 | - | }, | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | for _, tc := range testCases { | |
| 73 | - | t.Run(tc.name, func(t *testing.T) { | |
| 74 | - | result := generateVolumeName(tc.prefix, tc.domain, tc.path) | |
| 75 | - | ||
| 76 | - | // Check that it starts with prefix | |
| 77 | - | assert.True(t, strings.HasPrefix(result, tc.prefix+"-")) | |
| 78 | - | ||
| 79 | - | // Check that it contains sanitized domain | |
| 80 | - | sanitizedDomain := strings.ReplaceAll(strings.ToLower(tc.domain), ".", "-") | |
| 81 | - | assert.Contains(t, result, sanitizedDomain) | |
| 82 | - | ||
| 83 | - | // Volume names should be consistent for same inputs | |
| 84 | - | result2 := generateVolumeName(tc.prefix, tc.domain, tc.path) | |
| 85 | - | assert.Equal(t, result, result2) | |
| 86 | - | ||
| 87 | - | // Volume names should be different for different paths | |
| 88 | - | result3 := generateVolumeName(tc.prefix, tc.domain, tc.path+"/different") | |
| 89 | - | assert.NotEqual(t, result, result3) | |
| 90 | - | }) | |
| 91 | - | } | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | func TestMergeEnvironmentVariables(t *testing.T) { | |
| 95 | - | testCases := []struct { | |
| 96 | - | name string | |
| 97 | - | dockerfileEnv []string | |
| 98 | - | userEnv []string | |
| 99 | - | expected []string | |
| 100 | - | }{ | |
| 101 | - | { | |
| 102 | - | name: "user env overrides dockerfile env", | |
| 103 | - | dockerfileEnv: []string{"NODE_ENV=development", "PORT=3000"}, | |
| 104 | - | userEnv: []string{"NODE_ENV=production", "API_KEY=secret"}, | |
| 105 | - | expected: []string{"NODE_ENV=production", "PORT=3000", "API_KEY=secret"}, | |
| 106 | - | }, | |
| 107 | - | { | |
| 108 | - | name: "only dockerfile env", | |
| 109 | - | dockerfileEnv: []string{"NODE_ENV=development", "PORT=3000"}, | |
| 110 | - | userEnv: []string{}, | |
| 111 | - | expected: []string{"NODE_ENV=development", "PORT=3000"}, | |
| 112 | - | }, | |
| 113 | - | { | |
| 114 | - | name: "only user env", | |
| 115 | - | dockerfileEnv: []string{}, | |
| 116 | - | userEnv: []string{"API_KEY=secret", "DEBUG=true"}, | |
| 117 | - | expected: []string{"API_KEY=secret", "DEBUG=true"}, | |
| 118 | - | }, | |
| 119 | - | { | |
| 120 | - | name: "empty environment", | |
| 121 | - | dockerfileEnv: []string{}, | |
| 122 | - | userEnv: []string{}, | |
| 123 | - | expected: []string{}, | |
| 124 | - | }, | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | for _, tc := range testCases { | |
| 128 | - | t.Run(tc.name, func(t *testing.T) { | |
| 129 | - | result := mergeEnvironmentVariables(tc.dockerfileEnv, tc.userEnv) | |
| 130 | - | assert.Equal(t, len(tc.expected), len(result)) | |
| 131 | - | ||
| 132 | - | // Convert to map for easier comparison | |
| 133 | - | resultMap := make(map[string]string) | |
| 134 | - | for _, env := range result { | |
| 135 | - | parts := splitEnvVar(env) | |
| 136 | - | if len(parts) == 2 { | |
| 137 | - | resultMap[parts[0]] = parts[1] | |
| 138 | - | } | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | expectedMap := make(map[string]string) | |
| 142 | - | for _, env := range tc.expected { | |
| 143 | - | parts := splitEnvVar(env) | |
| 144 | - | if len(parts) == 2 { | |
| 145 | - | expectedMap[parts[0]] = parts[1] | |
| 146 | - | } | |
| 147 | - | } | |
| 148 | - | ||
| 149 | - | assert.Equal(t, expectedMap, resultMap) | |
| 150 | - | }) | |
| 151 | - | } | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // Helper function to split environment variable string | |
| 155 | - | func splitEnvVar(env string) []string { | |
| 156 | - | for i, c := range env { | |
| 157 | - | if c == '=' { | |
| 158 | - | return []string{env[:i], env[i+1:]} | |
| 159 | - | } | |
| 160 | - | } | |
| 161 | - | return []string{env} | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | func TestSanitizeDomainForVolume(t *testing.T) { | |
| 165 | - | testCases := []struct { | |
| 166 | - | name string | |
| 167 | - | input string | |
| 168 | - | expected string | |
| 169 | - | }{ | |
| 170 | - | { | |
| 171 | - | name: "basic domain", | |
| 172 | - | input: "app.example.com", | |
| 173 | - | expected: "app-example-com", | |
| 174 | - | }, | |
| 175 | - | { | |
| 176 | - | name: "domain with port", | |
| 177 | - | input: "localhost:3000", | |
| 178 | - | expected: "localhost-3000", | |
| 179 | - | }, | |
| 180 | - | { | |
| 181 | - | name: "domain with special chars", | |
| 182 | - | input: "my_app@example.com", | |
| 183 | - | expected: "my-app-example-com", | |
| 184 | - | }, | |
| 185 | - | { | |
| 186 | - | name: "uppercase domain", | |
| 187 | - | input: "API.EXAMPLE.COM", | |
| 188 | - | expected: "api-example-com", | |
| 189 | - | }, | |
| 190 | - | } | |
| 191 | - | ||
| 192 | - | for _, tc := range testCases { | |
| 193 | - | t.Run(tc.name, func(t *testing.T) { | |
| 194 | - | result := sanitizeDomainForVolume(tc.input) | |
| 195 | - | assert.Equal(t, tc.expected, result) | |
| 196 | - | }) | |
| 197 | - | } | |
| 198 | - | }⏎̸ | |
D internal/container/interfaces.go +0 -29
| @@ -1,29 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | ||
| 6 | - | "gordon/internal/config" | |
| 7 | - | "gordon/pkg/runtime" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // ManagerInterface defines the interface for container management operations | |
| 11 | - | // This interface is used to enable testing with mocks | |
| 12 | - | type ManagerInterface interface { | |
| 13 | - | DeployContainer(ctx context.Context, route config.Route) (*runtime.Container, error) | |
| 14 | - | StopContainer(ctx context.Context, containerID string) error | |
| 15 | - | RemoveContainer(ctx context.Context, containerID string, force bool) error | |
| 16 | - | ListContainers() map[string]*runtime.Container | |
| 17 | - | GetContainer(domain string) (*runtime.Container, bool) | |
| 18 | - | StopContainerByDomain(ctx context.Context, domain string) error | |
| 19 | - | Runtime() runtime.Runtime | |
| 20 | - | HealthCheck(ctx context.Context) map[string]bool | |
| 21 | - | SyncContainers(ctx context.Context) error | |
| 22 | - | GetContainerPort(ctx context.Context, domain string, internalPort int) (int, error) | |
| 23 | - | GetNetworkForApp(domain string) string | |
| 24 | - | AutoStartContainers(ctx context.Context) error | |
| 25 | - | StopAllManagedContainers(ctx context.Context) error | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // Ensure Manager implements the interface | |
| 29 | - | var _ ManagerInterface = (*Manager)(nil)⏎̸ | |
D internal/container/manager.go +0 -1106
| @@ -1,1106 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "strings" | |
| 7 | - | "sync" | |
| 8 | - | ||
| 9 | - | "github.com/rs/zerolog/log" | |
| 10 | - | ||
| 11 | - | "gordon/internal/config" | |
| 12 | - | "gordon/internal/env" | |
| 13 | - | "gordon/internal/logging" | |
| 14 | - | "gordon/pkg/runtime" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | // Manager handles container lifecycle and management | |
| 18 | - | type Manager struct { | |
| 19 | - | runtime runtime.Runtime | |
| 20 | - | config *config.Config | |
| 21 | - | containers map[string]*runtime.Container // map[domain] -> container | |
| 22 | - | envLoader *env.Loader | |
| 23 | - | logManager *logging.LogManager | |
| 24 | - | mu sync.RWMutex | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | // NewManager creates a new container manager | |
| 28 | - | func NewManager(cfg *config.Config) (*Manager, error) { | |
| 29 | - | // Test runtime connectivity with background context | |
| 30 | - | ctx := context.Background() | |
| 31 | - | ||
| 32 | - | // Create runtime using the factory | |
| 33 | - | rt, err := CreateRuntime(ctx, cfg) | |
| 34 | - | if err != nil { | |
| 35 | - | return nil, fmt.Errorf("failed to create container runtime: %w", err) | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | // Test runtime connectivity | |
| 39 | - | if err := rt.Ping(ctx); err != nil { | |
| 40 | - | return nil, fmt.Errorf("runtime not available: %w", err) | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | version, err := rt.Version(ctx) | |
| 44 | - | if err != nil { | |
| 45 | - | log.Warn().Err(err).Msg("Could not get runtime version") | |
| 46 | - | } else { | |
| 47 | - | log.Info().Str("runtime", cfg.Server.Runtime).Str("version", version).Msg("Container runtime connected") | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | // Create environment loader | |
| 51 | - | envLoader := env.NewLoader(cfg) | |
| 52 | - | ||
| 53 | - | // Register secret providers based on config | |
| 54 | - | for _, providerName := range cfg.Env.Providers { | |
| 55 | - | switch providerName { | |
| 56 | - | case "pass": | |
| 57 | - | envLoader.RegisterSecretProvider("pass", env.NewPassProvider()) | |
| 58 | - | case "sops": | |
| 59 | - | envLoader.RegisterSecretProvider("sops", env.NewSopsProvider()) | |
| 60 | - | } | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | // Ensure env directory exists | |
| 64 | - | if err := envLoader.EnsureEnvDir(); err != nil { | |
| 65 | - | log.Warn().Err(err).Msg("Failed to ensure env directory exists") | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | // Create empty env files for all configured routes (if they don't exist) | |
| 69 | - | if err := envLoader.CreateEnvFilesForRoutes(); err != nil { | |
| 70 | - | log.Warn().Err(err).Msg("Failed to create env files for routes") | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | // Create log manager | |
| 74 | - | logManager := logging.NewLogManager(cfg, rt) | |
| 75 | - | ||
| 76 | - | return &Manager{ | |
| 77 | - | runtime: rt, | |
| 78 | - | config: cfg, | |
| 79 | - | containers: make(map[string]*runtime.Container), | |
| 80 | - | envLoader: envLoader, | |
| 81 | - | logManager: logManager, | |
| 82 | - | }, nil | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | // DeployContainer deploys a container for a specific route | |
| 86 | - | func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*runtime.Container, error) { | |
| 87 | - | m.mu.Lock() | |
| 88 | - | defer m.mu.Unlock() | |
| 89 | - | ||
| 90 | - | // Check if container already exists for this domain | |
| 91 | - | if existing, exists := m.containers[route.Domain]; exists { | |
| 92 | - | log.Info().Str("domain", route.Domain).Str("container", existing.ID).Msg("Container already exists, restarting") | |
| 93 | - | ||
| 94 | - | // Stop and remove existing container | |
| 95 | - | if err := m.runtime.StopContainer(ctx, existing.ID); err != nil { | |
| 96 | - | log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to stop existing container") | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | if err := m.runtime.RemoveContainer(ctx, existing.ID, true); err != nil { | |
| 100 | - | log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to remove existing container") | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | delete(m.containers, route.Domain) | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | // Also check for orphaned containers with the same name in the runtime | |
| 107 | - | expectedContainerName := fmt.Sprintf("gordon-%s", route.Domain) | |
| 108 | - | allContainers, err := m.runtime.ListContainers(ctx, true) // include stopped containers | |
| 109 | - | if err != nil { | |
| 110 | - | log.Warn().Err(err).Msg("Failed to list all containers, proceeding with deployment") | |
| 111 | - | } else { | |
| 112 | - | for _, container := range allContainers { | |
| 113 | - | if container.Name == expectedContainerName { | |
| 114 | - | log.Info(). | |
| 115 | - | Str("domain", route.Domain). | |
| 116 | - | Str("container", container.ID). | |
| 117 | - | Str("status", container.Status). | |
| 118 | - | Msg("Found orphaned container with same name, removing") | |
| 119 | - | ||
| 120 | - | // Stop if running | |
| 121 | - | if err := m.runtime.StopContainer(ctx, container.ID); err != nil { | |
| 122 | - | log.Warn().Err(err).Str("container", container.ID).Msg("Failed to stop orphaned container") | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | // Force remove | |
| 126 | - | if err := m.runtime.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 127 | - | log.Warn().Err(err).Str("container", container.ID).Msg("Failed to remove orphaned container") | |
| 128 | - | } else { | |
| 129 | - | log.Info().Str("container", container.ID).Msg("Successfully removed orphaned container") | |
| 130 | - | } | |
| 131 | - | } | |
| 132 | - | } | |
| 133 | - | } | |
| 134 | - | ||
| 135 | - | // Construct the full image reference | |
| 136 | - | imageRef := route.Image | |
| 137 | - | ||
| 138 | - | // If registry auth is enabled and image doesn't already start with our registry domain, prepend it | |
| 139 | - | if m.config.RegistryAuth.Enabled && m.config.Server.RegistryDomain != "" { | |
| 140 | - | repoPart := strings.Split(imageRef, ":")[0] | |
| 141 | - | // Don't prepend if: | |
| 142 | - | // 1. Image already starts with our registry domain | |
| 143 | - | // 2. Image looks like an external registry (contains both dots and slashes, indicating registry.com/repo format) | |
| 144 | - | if !strings.HasPrefix(repoPart, m.config.Server.RegistryDomain+"/") && | |
| 145 | - | !(strings.Contains(repoPart, ".") && strings.Contains(repoPart, "/")) { | |
| 146 | - | imageRef = fmt.Sprintf("%s/%s", m.config.Server.RegistryDomain, route.Image) | |
| 147 | - | } | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | // Check if image exists locally first | |
| 151 | - | imageAvailable := false | |
| 152 | - | localImages, err := m.runtime.ListImages(ctx) | |
| 153 | - | if err != nil { | |
| 154 | - | log.Warn().Err(err).Msg("Failed to list local images, will attempt to pull from registry") | |
| 155 | - | } else { | |
| 156 | - | // Normalize image references for comparison | |
| 157 | - | normalizedImageRef := normalizeImageRef(imageRef) | |
| 158 | - | ||
| 159 | - | for _, localImage := range localImages { | |
| 160 | - | if normalizeImageRef(localImage) == normalizedImageRef { | |
| 161 | - | imageAvailable = true | |
| 162 | - | log.Info(). | |
| 163 | - | Str("image", imageRef). | |
| 164 | - | Msg("Image found locally, skipping pull") | |
| 165 | - | break | |
| 166 | - | } | |
| 167 | - | } | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | // Pull the image only if not available locally | |
| 171 | - | if !imageAvailable { | |
| 172 | - | log.Info().Str("image", imageRef).Msg("Image not found locally, pulling from registry") | |
| 173 | - | ||
| 174 | - | // Use authenticated pull if registry auth is configured | |
| 175 | - | if m.config.RegistryAuth.Enabled { | |
| 176 | - | log.Info(). | |
| 177 | - | Str("image", imageRef). | |
| 178 | - | Str("username", m.config.RegistryAuth.Username). | |
| 179 | - | Msg("Pulling image with authentication") | |
| 180 | - | ||
| 181 | - | err := m.runtime.PullImageWithAuth(ctx, imageRef, m.config.RegistryAuth.Username, m.config.RegistryAuth.Password) | |
| 182 | - | if err != nil { | |
| 183 | - | // Provide better error message with context | |
| 184 | - | availableImages := make([]string, 0, len(localImages)) | |
| 185 | - | for _, img := range localImages { | |
| 186 | - | availableImages = append(availableImages, img) | |
| 187 | - | } | |
| 188 | - | ||
| 189 | - | return nil, fmt.Errorf("failed to pull image '%s' from registry '%s' with authentication. "+ | |
| 190 | - | "Please check: 1) Image name spelling, 2) Registry credentials, 3) Image exists in registry. "+ | |
| 191 | - | "Available local images: %v. Error: %w", | |
| 192 | - | imageRef, m.config.Server.RegistryDomain, availableImages, err) | |
| 193 | - | } | |
| 194 | - | log.Info().Str("image", imageRef).Msg("Image pulled successfully with authentication") | |
| 195 | - | } else { | |
| 196 | - | log.Info().Str("image", imageRef).Msg("Pulling image without authentication") | |
| 197 | - | ||
| 198 | - | err := m.runtime.PullImage(ctx, imageRef) | |
| 199 | - | if err != nil { | |
| 200 | - | // Provide better error message with context | |
| 201 | - | availableImages := make([]string, 0, len(localImages)) | |
| 202 | - | for _, img := range localImages { | |
| 203 | - | availableImages = append(availableImages, img) | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | return nil, fmt.Errorf("failed to pull image '%s' from public registry. "+ | |
| 207 | - | "Please check: 1) Image name spelling, 2) Image exists in registry. "+ | |
| 208 | - | "Available local images: %v. Error: %w", | |
| 209 | - | imageRef, availableImages, err) | |
| 210 | - | } | |
| 211 | - | log.Info().Str("image", imageRef).Msg("Image pulled successfully") | |
| 212 | - | } | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | // Get exposed ports from the image | |
| 216 | - | exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, imageRef) | |
| 217 | - | if err != nil { | |
| 218 | - | log.Warn().Err(err).Str("image", imageRef).Msg("Failed to get exposed ports from image, using defaults") | |
| 219 | - | exposedPorts = []int{80, 8080, 3000} // Fallback to common web server ports | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | // Load environment variables for this route | |
| 223 | - | userEnvVars, err := m.envLoader.LoadEnvForRoute(route.Domain) | |
| 224 | - | if err != nil { | |
| 225 | - | log.Error().Err(err).Str("domain", route.Domain).Msg("Failed to load environment variables for route") | |
| 226 | - | return nil, fmt.Errorf("failed to load environment variables for %s: %w", route.Domain, err) | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | // Get environment variables from image ENV directives | |
| 230 | - | dockerfileEnvVars, err := m.runtime.InspectImageEnv(ctx, imageRef) | |
| 231 | - | if err != nil { | |
| 232 | - | log.Warn().Err(err).Str("image", imageRef).Msg("Failed to inspect image environment variables, proceeding without them") | |
| 233 | - | dockerfileEnvVars = []string{} | |
| 234 | - | } else if len(dockerfileEnvVars) > 0 { | |
| 235 | - | log.Info().Str("image", imageRef).Strs("dockerfile_env", dockerfileEnvVars).Msg("Found ENV directives in image") | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | // Merge Dockerfile ENV with user-provided env vars (user env takes precedence) | |
| 239 | - | envVars := mergeEnvironmentVariables(dockerfileEnvVars, userEnvVars) | |
| 240 | - | ||
| 241 | - | // Handle volume auto-creation if enabled | |
| 242 | - | volumes := make(map[string]string) | |
| 243 | - | if m.config.Volumes.AutoCreate { | |
| 244 | - | // Get volume paths from image VOLUME directives | |
| 245 | - | volumePaths, err := m.runtime.InspectImageVolumes(ctx, imageRef) | |
| 246 | - | if err != nil { | |
| 247 | - | log.Warn().Err(err).Str("image", imageRef).Msg("Failed to inspect image volumes, proceeding without volumes") | |
| 248 | - | } else if len(volumePaths) > 0 { | |
| 249 | - | log.Info().Str("image", imageRef).Strs("volume_paths", volumePaths).Msg("Found VOLUME directives in image") | |
| 250 | - | ||
| 251 | - | for _, volumePath := range volumePaths { | |
| 252 | - | volumeName := generateVolumeName(m.config.Volumes.Prefix, route.Domain, volumePath) | |
| 253 | - | ||
| 254 | - | // Check if volume exists, create if it doesn't | |
| 255 | - | exists, err := m.runtime.VolumeExists(ctx, volumeName) | |
| 256 | - | if err != nil { | |
| 257 | - | log.Warn().Err(err).Str("volume", volumeName).Msg("Failed to check if volume exists") | |
| 258 | - | continue | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | if !exists { | |
| 262 | - | if err := m.runtime.CreateVolume(ctx, volumeName); err != nil { | |
| 263 | - | log.Error().Err(err).Str("volume", volumeName).Msg("Failed to create volume") | |
| 264 | - | continue | |
| 265 | - | } | |
| 266 | - | log.Info().Str("volume", volumeName).Str("path", volumePath).Msg("Created volume for container") | |
| 267 | - | } else { | |
| 268 | - | log.Debug().Str("volume", volumeName).Str("path", volumePath).Msg("Volume already exists, reusing") | |
| 269 | - | } | |
| 270 | - | ||
| 271 | - | volumes[volumePath] = volumeName | |
| 272 | - | } | |
| 273 | - | } | |
| 274 | - | } | |
| 275 | - | ||
| 276 | - | // Determine which network to use for this app | |
| 277 | - | networkName := m.GetNetworkForApp(route.Domain) | |
| 278 | - | ||
| 279 | - | // Create network if it doesn't exist | |
| 280 | - | if err := m.CreateNetworkIfNeeded(ctx, networkName); err != nil { | |
| 281 | - | return nil, fmt.Errorf("failed to create network for %s: %w", route.Domain, err) | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | // Create container configuration | |
| 285 | - | containerConfig := &runtime.ContainerConfig{ | |
| 286 | - | Image: imageRef, | |
| 287 | - | Name: fmt.Sprintf("gordon-%s", route.Domain), | |
| 288 | - | Ports: exposedPorts, | |
| 289 | - | Env: envVars, | |
| 290 | - | Volumes: volumes, | |
| 291 | - | NetworkMode: networkName, | |
| 292 | - | Hostname: route.Domain, | |
| 293 | - | Labels: map[string]string{ | |
| 294 | - | "gordon.domain": route.Domain, | |
| 295 | - | "gordon.image": route.Image, | |
| 296 | - | "gordon.managed": "true", | |
| 297 | - | "gordon.route": route.Domain, | |
| 298 | - | }, | |
| 299 | - | AutoRemove: false, // Keep containers for inspection | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | // Create the container | |
| 303 | - | container, err := m.runtime.CreateContainer(ctx, containerConfig) | |
| 304 | - | if err != nil { | |
| 305 | - | return nil, fmt.Errorf("failed to create container for %s: %w", route.Domain, err) | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | // Start the container | |
| 309 | - | if err := m.runtime.StartContainer(ctx, container.ID); err != nil { | |
| 310 | - | // Clean up the created container | |
| 311 | - | m.runtime.RemoveContainer(ctx, container.ID, true) | |
| 312 | - | return nil, fmt.Errorf("failed to start container for %s: %w", route.Domain, err) | |
| 313 | - | } | |
| 314 | - | ||
| 315 | - | // Re-inspect to get updated port information | |
| 316 | - | container, err = m.runtime.InspectContainer(ctx, container.ID) | |
| 317 | - | if err != nil { | |
| 318 | - | return nil, fmt.Errorf("failed to inspect started container: %w", err) | |
| 319 | - | } | |
| 320 | - | ||
| 321 | - | // Store the container mapping | |
| 322 | - | m.containers[route.Domain] = container | |
| 323 | - | ||
| 324 | - | // Start log collection for the container | |
| 325 | - | containerName := container.Name | |
| 326 | - | if containerName == "" { | |
| 327 | - | containerName = route.Domain | |
| 328 | - | } | |
| 329 | - | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |
| 330 | - | log.Warn(). | |
| 331 | - | Err(err). | |
| 332 | - | Str("container", container.ID). | |
| 333 | - | Str("domain", route.Domain). | |
| 334 | - | Msg("Failed to start log collection for container") | |
| 335 | - | } | |
| 336 | - | ||
| 337 | - | log.Info(). | |
| 338 | - | Str("domain", route.Domain). | |
| 339 | - | Str("image", route.Image). | |
| 340 | - | Str("container", container.ID). | |
| 341 | - | Ints("ports", container.Ports). | |
| 342 | - | Str("network", networkName). | |
| 343 | - | Msg("Container deployed successfully") | |
| 344 | - | ||
| 345 | - | // Deploy attachments for this specific app | |
| 346 | - | if attachments, ok := m.config.Attachments[route.Domain]; ok { | |
| 347 | - | for _, serviceImage := range attachments { | |
| 348 | - | if err := m.DeployAttachedService(ctx, route.Domain, serviceImage); err != nil { | |
| 349 | - | log.Error().Err(err).Str("service", serviceImage).Str("domain", route.Domain).Msg("Failed to deploy attachment") | |
| 350 | - | // Don't fail the main deployment if attachment fails | |
| 351 | - | } | |
| 352 | - | } | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | // Check if app is part of a network group with attachments | |
| 356 | - | for groupName, domains := range m.config.NetworkGroups { | |
| 357 | - | for _, d := range domains { | |
| 358 | - | if d == route.Domain { | |
| 359 | - | // Deploy group attachments if not already deployed | |
| 360 | - | if attachments, ok := m.config.Attachments[groupName]; ok { | |
| 361 | - | for _, serviceImage := range attachments { | |
| 362 | - | if err := m.DeployAttachedService(ctx, groupName, serviceImage); err != nil { | |
| 363 | - | log.Error().Err(err).Str("service", serviceImage).Str("group", groupName).Msg("Failed to deploy group attachment") | |
| 364 | - | // Don't fail the main deployment if attachment fails | |
| 365 | - | } | |
| 366 | - | } | |
| 367 | - | } | |
| 368 | - | break | |
| 369 | - | } | |
| 370 | - | } | |
| 371 | - | } | |
| 372 | - | ||
| 373 | - | return container, nil | |
| 374 | - | } | |
| 375 | - | ||
| 376 | - | // GetContainer returns the container for a domain | |
| 377 | - | func (m *Manager) GetContainer(domain string) (*runtime.Container, bool) { | |
| 378 | - | m.mu.RLock() | |
| 379 | - | defer m.mu.RUnlock() | |
| 380 | - | ||
| 381 | - | container, exists := m.containers[domain] | |
| 382 | - | return container, exists | |
| 383 | - | } | |
| 384 | - | ||
| 385 | - | // GetContainerPort returns the host port for a domain's container | |
| 386 | - | func (m *Manager) GetContainerPort(ctx context.Context, domain string, internalPort int) (int, error) { | |
| 387 | - | m.mu.RLock() | |
| 388 | - | container, exists := m.containers[domain] | |
| 389 | - | m.mu.RUnlock() | |
| 390 | - | ||
| 391 | - | if !exists { | |
| 392 | - | return 0, fmt.Errorf("no container found for domain %s", domain) | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | return m.runtime.GetContainerPort(ctx, container.ID, internalPort) | |
| 396 | - | } | |
| 397 | - | ||
| 398 | - | // ListContainers returns all managed containers | |
| 399 | - | func (m *Manager) ListContainers() map[string]*runtime.Container { | |
| 400 | - | m.mu.RLock() | |
| 401 | - | defer m.mu.RUnlock() | |
| 402 | - | ||
| 403 | - | // Return a copy of the map to avoid race conditions | |
| 404 | - | result := make(map[string]*runtime.Container) | |
| 405 | - | for domain, container := range m.containers { | |
| 406 | - | result[domain] = container | |
| 407 | - | } | |
| 408 | - | return result | |
| 409 | - | } | |
| 410 | - | ||
| 411 | - | // FindContainerByDomain returns the container ID for a domain | |
| 412 | - | func (m *Manager) FindContainerByDomain(domain string) (string, bool) { | |
| 413 | - | m.mu.RLock() | |
| 414 | - | defer m.mu.RUnlock() | |
| 415 | - | ||
| 416 | - | if container, exists := m.containers[domain]; exists { | |
| 417 | - | return container.ID, true | |
| 418 | - | } | |
| 419 | - | return "", false | |
| 420 | - | } | |
| 421 | - | ||
| 422 | - | // FindDomainByContainerID returns the domain for a container ID | |
| 423 | - | func (m *Manager) FindDomainByContainerID(containerID string) (string, bool) { | |
| 424 | - | m.mu.RLock() | |
| 425 | - | defer m.mu.RUnlock() | |
| 426 | - | ||
| 427 | - | for domain, container := range m.containers { | |
| 428 | - | if container.ID == containerID { | |
| 429 | - | return domain, true | |
| 430 | - | } | |
| 431 | - | } | |
| 432 | - | return "", false | |
| 433 | - | } | |
| 434 | - | ||
| 435 | - | // StopContainer stops a container by ID | |
| 436 | - | func (m *Manager) StopContainer(ctx context.Context, containerID string) error { | |
| 437 | - | if err := m.runtime.StopContainer(ctx, containerID); err != nil { | |
| 438 | - | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 439 | - | } | |
| 440 | - | ||
| 441 | - | // Stop log collection for the container | |
| 442 | - | m.logManager.StopCollection(containerID) | |
| 443 | - | ||
| 444 | - | log.Info().Str("container", containerID).Msg("Container stopped") | |
| 445 | - | return nil | |
| 446 | - | } | |
| 447 | - | ||
| 448 | - | // StopContainerByDomain stops a container for a domain | |
| 449 | - | func (m *Manager) StopContainerByDomain(ctx context.Context, domain string) error { | |
| 450 | - | m.mu.RLock() | |
| 451 | - | container, exists := m.containers[domain] | |
| 452 | - | m.mu.RUnlock() | |
| 453 | - | ||
| 454 | - | if !exists { | |
| 455 | - | return fmt.Errorf("no container found for domain %s", domain) | |
| 456 | - | } | |
| 457 | - | ||
| 458 | - | return m.StopContainer(ctx, container.ID) | |
| 459 | - | } | |
| 460 | - | ||
| 461 | - | // RemoveContainer removes a container by ID | |
| 462 | - | func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |
| 463 | - | // Find the domain for this container before removal for volume cleanup | |
| 464 | - | var containerDomain string | |
| 465 | - | m.mu.RLock() | |
| 466 | - | for domain, container := range m.containers { | |
| 467 | - | if container.ID == containerID { | |
| 468 | - | containerDomain = domain | |
| 469 | - | break | |
| 470 | - | } | |
| 471 | - | } | |
| 472 | - | m.mu.RUnlock() | |
| 473 | - | ||
| 474 | - | if err := m.runtime.RemoveContainer(ctx, containerID, force); err != nil { | |
| 475 | - | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | |
| 476 | - | } | |
| 477 | - | ||
| 478 | - | // Stop log collection for the container | |
| 479 | - | m.logManager.StopCollection(containerID) | |
| 480 | - | ||
| 481 | - | // Clean up volumes if preserve is disabled and we found the domain | |
| 482 | - | if containerDomain != "" && !m.config.Volumes.Preserve { | |
| 483 | - | if err := m.cleanupVolumesForDomain(ctx, containerDomain); err != nil { | |
| 484 | - | log.Warn().Err(err).Str("domain", containerDomain).Msg("Failed to cleanup volumes during container removal") | |
| 485 | - | } | |
| 486 | - | } | |
| 487 | - | ||
| 488 | - | // Remove from our tracking map and cleanup network if needed | |
| 489 | - | m.mu.Lock() | |
| 490 | - | var removedDomain string | |
| 491 | - | for domain, container := range m.containers { | |
| 492 | - | if container.ID == containerID { | |
| 493 | - | delete(m.containers, domain) | |
| 494 | - | removedDomain = domain | |
| 495 | - | log.Info().Str("domain", domain).Str("container", containerID).Msg("Container removed") | |
| 496 | - | break | |
| 497 | - | } | |
| 498 | - | } | |
| 499 | - | m.mu.Unlock() | |
| 500 | - | ||
| 501 | - | // If we removed an app container, check if we need to cleanup its network | |
| 502 | - | if removedDomain != "" && m.config.NetworkIsolation.Enabled { | |
| 503 | - | networkName := m.GetNetworkForApp(removedDomain) | |
| 504 | - | if err := m.cleanupNetworkIfEmpty(ctx, networkName); err != nil { | |
| 505 | - | log.Warn().Err(err).Str("network", networkName).Msg("Failed to cleanup network after container removal") | |
| 506 | - | } | |
| 507 | - | } | |
| 508 | - | ||
| 509 | - | return nil | |
| 510 | - | } | |
| 511 | - | ||
| 512 | - | // RemoveContainerByDomain removes a container for a domain | |
| 513 | - | func (m *Manager) RemoveContainerByDomain(ctx context.Context, domain string, force bool) error { | |
| 514 | - | m.mu.RLock() | |
| 515 | - | container, exists := m.containers[domain] | |
| 516 | - | m.mu.RUnlock() | |
| 517 | - | ||
| 518 | - | if !exists { | |
| 519 | - | return fmt.Errorf("no container found for domain %s", domain) | |
| 520 | - | } | |
| 521 | - | ||
| 522 | - | return m.RemoveContainer(ctx, container.ID, force) | |
| 523 | - | } | |
| 524 | - | ||
| 525 | - | // SyncContainers synchronizes the internal state with the actual running containers | |
| 526 | - | func (m *Manager) SyncContainers(ctx context.Context) error { | |
| 527 | - | m.mu.Lock() | |
| 528 | - | defer m.mu.Unlock() | |
| 529 | - | ||
| 530 | - | // Get all containers from the runtime | |
| 531 | - | allContainers, err := m.runtime.ListContainers(ctx, false) // only running containers | |
| 532 | - | if err != nil { | |
| 533 | - | return fmt.Errorf("failed to list containers: %w", err) | |
| 534 | - | } | |
| 535 | - | ||
| 536 | - | // Find containers managed by Gordon | |
| 537 | - | managedContainers := make(map[string]*runtime.Container) | |
| 538 | - | for _, container := range allContainers { | |
| 539 | - | if container.Labels != nil { | |
| 540 | - | if domain, exists := container.Labels["gordon.domain"]; exists && container.Labels["gordon.managed"] == "true" { | |
| 541 | - | managedContainers[domain] = container | |
| 542 | - | } | |
| 543 | - | } | |
| 544 | - | } | |
| 545 | - | ||
| 546 | - | // Update our internal state | |
| 547 | - | m.containers = managedContainers | |
| 548 | - | ||
| 549 | - | log.Info().Int("count", len(managedContainers)).Msg("Container state synchronized") | |
| 550 | - | return nil | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | // AutoStartContainers automatically starts containers that match configured routes | |
| 554 | - | func (m *Manager) AutoStartContainers(ctx context.Context) error { | |
| 555 | - | routes := m.config.GetRoutes() | |
| 556 | - | if len(routes) == 0 { | |
| 557 | - | log.Info().Msg("No routes configured, skipping auto-start") | |
| 558 | - | return nil | |
| 559 | - | } | |
| 560 | - | ||
| 561 | - | // Get all containers (including stopped ones) to check for matches | |
| 562 | - | allContainers, err := m.runtime.ListContainers(ctx, true) | |
| 563 | - | if err != nil { | |
| 564 | - | return fmt.Errorf("failed to list all containers: %w", err) | |
| 565 | - | } | |
| 566 | - | ||
| 567 | - | // Create maps for quick lookup | |
| 568 | - | gordonManagedContainers := make(map[string]*runtime.Container) // domain -> container | |
| 569 | - | containersByImage := make(map[string][]*runtime.Container) // image -> containers | |
| 570 | - | ||
| 571 | - | for _, container := range allContainers { | |
| 572 | - | // Track Gordon-managed containers by domain | |
| 573 | - | if container.Labels != nil && container.Labels["gordon.managed"] == "true" { | |
| 574 | - | if domain, exists := container.Labels["gordon.domain"]; exists { | |
| 575 | - | gordonManagedContainers[domain] = container | |
| 576 | - | } | |
| 577 | - | } | |
| 578 | - | ||
| 579 | - | // Track all containers by image (normalize image name) | |
| 580 | - | normalizedImage := m.normalizeImageName(container.Image) | |
| 581 | - | containersByImage[normalizedImage] = append(containersByImage[normalizedImage], container) | |
| 582 | - | } | |
| 583 | - | ||
| 584 | - | startedCount := 0 | |
| 585 | - | for _, route := range routes { | |
| 586 | - | // Build the expected image reference | |
| 587 | - | expectedImage := route.Image | |
| 588 | - | if m.config.RegistryAuth.Enabled && m.config.Server.RegistryDomain != "" { | |
| 589 | - | if !strings.Contains(strings.Split(expectedImage, ":")[0], ".") { | |
| 590 | - | expectedImage = fmt.Sprintf("%s/%s", m.config.Server.RegistryDomain, route.Image) | |
| 591 | - | } | |
| 592 | - | } | |
| 593 | - | ||
| 594 | - | // First, check if we already have a Gordon-managed container for this domain | |
| 595 | - | if existingContainer, exists := gordonManagedContainers[route.Domain]; exists { | |
| 596 | - | isRunning, err := m.runtime.IsContainerRunning(ctx, existingContainer.ID) | |
| 597 | - | if err != nil { | |
| 598 | - | log.Warn().Err(err).Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Failed to check container status") | |
| 599 | - | continue | |
| 600 | - | } | |
| 601 | - | ||
| 602 | - | if isRunning { | |
| 603 | - | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Gordon-managed container already running") | |
| 604 | - | m.mu.Lock() | |
| 605 | - | m.containers[route.Domain] = existingContainer | |
| 606 | - | m.mu.Unlock() | |
| 607 | - | continue | |
| 608 | - | } | |
| 609 | - | ||
| 610 | - | // Container exists but is stopped, start it | |
| 611 | - | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Starting existing Gordon-managed container") | |
| 612 | - | if err := m.runtime.StartContainer(ctx, existingContainer.ID); err != nil { | |
| 613 | - | log.Error().Err(err).Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Failed to start existing container") | |
| 614 | - | continue | |
| 615 | - | } | |
| 616 | - | ||
| 617 | - | // Re-inspect to get updated information | |
| 618 | - | container, err := m.runtime.InspectContainer(ctx, existingContainer.ID) | |
| 619 | - | if err != nil { | |
| 620 | - | log.Error().Err(err).Str("container", existingContainer.ID).Msg("Failed to inspect started container") | |
| 621 | - | continue | |
| 622 | - | } | |
| 623 | - | ||
| 624 | - | m.mu.Lock() | |
| 625 | - | m.containers[route.Domain] = container | |
| 626 | - | m.mu.Unlock() | |
| 627 | - | startedCount++ | |
| 628 | - | ||
| 629 | - | // Start log collection for the container | |
| 630 | - | containerName := container.Name | |
| 631 | - | if containerName == "" { | |
| 632 | - | containerName = route.Domain | |
| 633 | - | } | |
| 634 | - | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |
| 635 | - | log.Warn(). | |
| 636 | - | Err(err). | |
| 637 | - | Str("container", container.ID). | |
| 638 | - | Str("domain", route.Domain). | |
| 639 | - | Msg("Failed to start log collection for auto-started container") | |
| 640 | - | } | |
| 641 | - | ||
| 642 | - | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Existing Gordon-managed container started successfully") | |
| 643 | - | continue | |
| 644 | - | } | |
| 645 | - | ||
| 646 | - | // Second, check if there are existing containers with the same image and clean them up | |
| 647 | - | normalizedExpectedImage := m.normalizeImageName(expectedImage) | |
| 648 | - | if containers, exists := containersByImage[normalizedExpectedImage]; exists { | |
| 649 | - | // Stop and remove existing containers with the same image to avoid conflicts | |
| 650 | - | for _, container := range containers { | |
| 651 | - | // Skip Gordon-managed containers (already handled above) | |
| 652 | - | if container.Labels != nil && container.Labels["gordon.managed"] == "true" { | |
| 653 | - | continue | |
| 654 | - | } | |
| 655 | - | ||
| 656 | - | log.Info().Str("domain", route.Domain).Str("container", container.ID).Str("image", container.Image).Msg("Stopping existing container with same image to avoid conflicts") | |
| 657 | - | ||
| 658 | - | // Stop the container if it's running | |
| 659 | - | isRunning, err := m.runtime.IsContainerRunning(ctx, container.ID) | |
| 660 | - | if err != nil { | |
| 661 | - | log.Warn().Err(err).Str("container", container.ID).Msg("Failed to check container status") | |
| 662 | - | continue | |
| 663 | - | } | |
| 664 | - | ||
| 665 | - | if isRunning { | |
| 666 | - | if err := m.runtime.StopContainer(ctx, container.ID); err != nil { | |
| 667 | - | log.Warn().Err(err).Str("container", container.ID).Msg("Failed to stop existing container") | |
| 668 | - | continue | |
| 669 | - | } | |
| 670 | - | } | |
| 671 | - | ||
| 672 | - | // Remove the container to clean up | |
| 673 | - | if err := m.runtime.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 674 | - | log.Warn().Err(err).Str("container", container.ID).Msg("Failed to remove existing container") | |
| 675 | - | } else { | |
| 676 | - | log.Info().Str("container", container.ID).Msg("Removed existing container with same image") | |
| 677 | - | } | |
| 678 | - | } | |
| 679 | - | } | |
| 680 | - | ||
| 681 | - | // No existing container found, deploy a new one | |
| 682 | - | log.Info().Str("domain", route.Domain).Str("image", route.Image).Msg("Deploying new container for route") | |
| 683 | - | ||
| 684 | - | _, err := m.DeployContainer(ctx, route) | |
| 685 | - | if err != nil { | |
| 686 | - | log.Error().Err(err).Str("domain", route.Domain).Str("image", route.Image).Msg("Failed to deploy container for route") | |
| 687 | - | continue | |
| 688 | - | } | |
| 689 | - | startedCount++ | |
| 690 | - | } | |
| 691 | - | ||
| 692 | - | log.Info().Int("started", startedCount).Int("total_routes", len(routes)).Msg("Auto-start completed") | |
| 693 | - | return nil | |
| 694 | - | } | |
| 695 | - | ||
| 696 | - | // normalizeImageName normalizes image names for comparison | |
| 697 | - | func (m *Manager) normalizeImageName(image string) string { | |
| 698 | - | // Remove tag if present, keep only repository name | |
| 699 | - | parts := strings.Split(image, ":") | |
| 700 | - | repo := parts[0] | |
| 701 | - | ||
| 702 | - | // If no registry domain and it's a simple name, it's from Docker Hub | |
| 703 | - | if !strings.Contains(repo, "/") { | |
| 704 | - | return "docker.io/library/" + repo | |
| 705 | - | } | |
| 706 | - | ||
| 707 | - | // If it has one slash and no domain, it's a user repo on Docker Hub | |
| 708 | - | if strings.Count(repo, "/") == 1 && !strings.Contains(strings.Split(repo, "/")[0], ".") { | |
| 709 | - | return "docker.io/" + repo | |
| 710 | - | } | |
| 711 | - | ||
| 712 | - | return repo | |
| 713 | - | } | |
| 714 | - | ||
| 715 | - | // StopAllManagedContainers stops all containers managed by Gordon | |
| 716 | - | func (m *Manager) StopAllManagedContainers(ctx context.Context) error { | |
| 717 | - | m.mu.RLock() | |
| 718 | - | containers := make(map[string]*runtime.Container) | |
| 719 | - | for domain, container := range m.containers { | |
| 720 | - | containers[domain] = container | |
| 721 | - | } | |
| 722 | - | m.mu.RUnlock() | |
| 723 | - | ||
| 724 | - | if len(containers) == 0 { | |
| 725 | - | log.Info().Msg("No managed containers to stop") | |
| 726 | - | return nil | |
| 727 | - | } | |
| 728 | - | ||
| 729 | - | log.Info().Int("count", len(containers)).Msg("Stopping all managed containers...") | |
| 730 | - | ||
| 731 | - | var errors []error | |
| 732 | - | for domain, container := range containers { | |
| 733 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Stopping managed container") | |
| 734 | - | ||
| 735 | - | if err := m.runtime.StopContainer(ctx, container.ID); err != nil { | |
| 736 | - | log.Error().Err(err).Str("domain", domain).Str("container", container.ID).Msg("Failed to stop managed container") | |
| 737 | - | errors = append(errors, fmt.Errorf("failed to stop container %s for domain %s: %w", container.ID, domain, err)) | |
| 738 | - | continue | |
| 739 | - | } | |
| 740 | - | ||
| 741 | - | // Stop log collection for the container | |
| 742 | - | m.logManager.StopCollection(container.ID) | |
| 743 | - | ||
| 744 | - | // Remove from tracking | |
| 745 | - | m.mu.Lock() | |
| 746 | - | delete(m.containers, domain) | |
| 747 | - | m.mu.Unlock() | |
| 748 | - | ||
| 749 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Managed container stopped successfully") | |
| 750 | - | } | |
| 751 | - | ||
| 752 | - | if len(errors) > 0 { | |
| 753 | - | return fmt.Errorf("failed to stop %d containers: %v", len(errors), errors) | |
| 754 | - | } | |
| 755 | - | ||
| 756 | - | log.Info().Msg("All managed containers stopped successfully") | |
| 757 | - | return nil | |
| 758 | - | } | |
| 759 | - | ||
| 760 | - | // HealthCheck checks the health of all managed containers | |
| 761 | - | func (m *Manager) HealthCheck(ctx context.Context) map[string]bool { | |
| 762 | - | m.mu.RLock() | |
| 763 | - | defer m.mu.RUnlock() | |
| 764 | - | ||
| 765 | - | health := make(map[string]bool) | |
| 766 | - | for domain, container := range m.containers { | |
| 767 | - | running, err := m.runtime.IsContainerRunning(ctx, container.ID) | |
| 768 | - | if err != nil { | |
| 769 | - | log.Warn().Err(err).Str("domain", domain).Str("container", container.ID).Msg("Failed to check container health") | |
| 770 | - | health[domain] = false | |
| 771 | - | } else { | |
| 772 | - | health[domain] = running | |
| 773 | - | } | |
| 774 | - | } | |
| 775 | - | ||
| 776 | - | return health | |
| 777 | - | } | |
| 778 | - | ||
| 779 | - | // Runtime returns the underlying runtime interface | |
| 780 | - | func (m *Manager) Runtime() runtime.Runtime { | |
| 781 | - | return m.runtime | |
| 782 | - | } | |
| 783 | - | ||
| 784 | - | // UpdateConfig updates the manager's configuration and creates env files for new routes | |
| 785 | - | func (m *Manager) UpdateConfig(cfg *config.Config) { | |
| 786 | - | m.config = cfg | |
| 787 | - | ||
| 788 | - | // Update env loader config | |
| 789 | - | m.envLoader.UpdateConfig(cfg) | |
| 790 | - | ||
| 791 | - | // Create env files for any new routes | |
| 792 | - | if err := m.envLoader.CreateEnvFilesForRoutes(); err != nil { | |
| 793 | - | log.Warn().Err(err).Msg("Failed to create env files for new routes during config update") | |
| 794 | - | } | |
| 795 | - | } | |
| 796 | - | ||
| 797 | - | // Shutdown gracefully shuts down the container manager | |
| 798 | - | func (m *Manager) Shutdown(ctx context.Context) error { | |
| 799 | - | log.Info().Msg("Shutting down container manager...") | |
| 800 | - | ||
| 801 | - | // Stop all managed containers (this also stops log collection for each) | |
| 802 | - | if err := m.StopAllManagedContainers(ctx); err != nil { | |
| 803 | - | log.Error().Err(err).Msg("Failed to stop all managed containers during shutdown") | |
| 804 | - | } | |
| 805 | - | ||
| 806 | - | // Stop all remaining log collection | |
| 807 | - | m.logManager.StopAll() | |
| 808 | - | ||
| 809 | - | log.Info().Msg("Container manager shutdown complete") | |
| 810 | - | return nil | |
| 811 | - | } | |
| 812 | - | ||
| 813 | - | // cleanupVolumesForDomain removes all volumes associated with a domain | |
| 814 | - | func (m *Manager) cleanupVolumesForDomain(ctx context.Context, domain string) error { | |
| 815 | - | log.Info().Str("domain", domain).Msg("Cleaning up volumes for domain") | |
| 816 | - | ||
| 817 | - | // We need to get the route to know what image was used to determine volume paths | |
| 818 | - | var route *config.Route | |
| 819 | - | for _, r := range m.config.GetRoutes() { | |
| 820 | - | if r.Domain == domain { | |
| 821 | - | route = &r | |
| 822 | - | break | |
| 823 | - | } | |
| 824 | - | } | |
| 825 | - | ||
| 826 | - | if route == nil { | |
| 827 | - | log.Debug().Str("domain", domain).Msg("No route found for domain, skipping volume cleanup") | |
| 828 | - | return nil | |
| 829 | - | } | |
| 830 | - | ||
| 831 | - | // Construct the full image reference like we do in DeployContainer | |
| 832 | - | imageRef := route.Image | |
| 833 | - | if m.config.RegistryAuth.Enabled && m.config.Server.RegistryDomain != "" { | |
| 834 | - | if !strings.Contains(strings.Split(imageRef, ":")[0], ".") { | |
| 835 | - | imageRef = fmt.Sprintf("%s/%s", m.config.Server.RegistryDomain, route.Image) | |
| 836 | - | } | |
| 837 | - | } | |
| 838 | - | ||
| 839 | - | // Get volume paths from image VOLUME directives | |
| 840 | - | volumePaths, err := m.runtime.InspectImageVolumes(ctx, imageRef) | |
| 841 | - | if err != nil { | |
| 842 | - | log.Warn().Err(err).Str("image", imageRef).Msg("Failed to inspect image volumes during cleanup") | |
| 843 | - | return err | |
| 844 | - | } | |
| 845 | - | ||
| 846 | - | if len(volumePaths) == 0 { | |
| 847 | - | log.Debug().Str("domain", domain).Msg("No volumes found for cleanup") | |
| 848 | - | return nil | |
| 849 | - | } | |
| 850 | - | ||
| 851 | - | var errors []error | |
| 852 | - | for _, volumePath := range volumePaths { | |
| 853 | - | volumeName := generateVolumeName(m.config.Volumes.Prefix, domain, volumePath) | |
| 854 | - | ||
| 855 | - | // Check if volume exists before trying to remove | |
| 856 | - | exists, err := m.runtime.VolumeExists(ctx, volumeName) | |
| 857 | - | if err != nil { | |
| 858 | - | log.Warn().Err(err).Str("volume", volumeName).Msg("Failed to check if volume exists during cleanup") | |
| 859 | - | continue | |
| 860 | - | } | |
| 861 | - | ||
| 862 | - | if exists { | |
| 863 | - | if err := m.runtime.RemoveVolume(ctx, volumeName, true); err != nil { | |
| 864 | - | log.Error().Err(err).Str("volume", volumeName).Str("domain", domain).Msg("Failed to remove volume during cleanup") | |
| 865 | - | errors = append(errors, err) | |
| 866 | - | } else { | |
| 867 | - | log.Info().Str("volume", volumeName).Str("domain", domain).Msg("Volume cleaned up successfully") | |
| 868 | - | } | |
| 869 | - | } | |
| 870 | - | } | |
| 871 | - | ||
| 872 | - | if len(errors) > 0 { | |
| 873 | - | return fmt.Errorf("failed to cleanup %d volumes for domain %s", len(errors), domain) | |
| 874 | - | } | |
| 875 | - | ||
| 876 | - | return nil | |
| 877 | - | } | |
| 878 | - | ||
| 879 | - | // cleanupNetworkIfEmpty removes a network if it has no containers | |
| 880 | - | func (m *Manager) cleanupNetworkIfEmpty(ctx context.Context, networkName string) error { | |
| 881 | - | if networkName == "bridge" || networkName == "default" { | |
| 882 | - | return nil // Don't try to remove default networks | |
| 883 | - | } | |
| 884 | - | ||
| 885 | - | networks, err := m.runtime.ListNetworks(ctx) | |
| 886 | - | if err != nil { | |
| 887 | - | return fmt.Errorf("failed to list networks: %w", err) | |
| 888 | - | } | |
| 889 | - | ||
| 890 | - | for _, network := range networks { | |
| 891 | - | if network.Name == networkName { | |
| 892 | - | // Check if network has any containers | |
| 893 | - | if len(network.Containers) == 0 { | |
| 894 | - | // Network is empty, remove it | |
| 895 | - | if err := m.runtime.RemoveNetwork(ctx, networkName); err != nil { | |
| 896 | - | log.Warn().Err(err).Str("network", networkName).Msg("Failed to cleanup empty network") | |
| 897 | - | return err | |
| 898 | - | } | |
| 899 | - | log.Info().Str("network", networkName).Msg("Cleaned up empty network") | |
| 900 | - | } | |
| 901 | - | break | |
| 902 | - | } | |
| 903 | - | } | |
| 904 | - | ||
| 905 | - | return nil | |
| 906 | - | } | |
| 907 | - | ||
| 908 | - | // DeployAttachedService deploys a service attached to an app or network group | |
| 909 | - | func (m *Manager) DeployAttachedService(ctx context.Context, identifier, serviceImage string) error { | |
| 910 | - | networkName := m.generateNetworkName(identifier) | |
| 911 | - | ||
| 912 | - | // Extract service name from image for container naming | |
| 913 | - | serviceName := strings.Split(serviceImage, ":")[0] | |
| 914 | - | if strings.Contains(serviceName, "/") { | |
| 915 | - | parts := strings.Split(serviceName, "/") | |
| 916 | - | serviceName = parts[len(parts)-1] | |
| 917 | - | } | |
| 918 | - | ||
| 919 | - | // For network groups, we need a different container naming scheme | |
| 920 | - | apps := m.GetAppsForNetwork(identifier) | |
| 921 | - | var containerName string | |
| 922 | - | ||
| 923 | - | if len(apps) > 1 { | |
| 924 | - | // Shared service for network group | |
| 925 | - | containerName = fmt.Sprintf("%s-shared-%s", m.config.NetworkIsolation.NetworkPrefix, serviceName) | |
| 926 | - | } else { | |
| 927 | - | // App-specific service | |
| 928 | - | containerName = fmt.Sprintf("%s-%s", strings.ReplaceAll(identifier, ".", "-"), serviceName) | |
| 929 | - | } | |
| 930 | - | ||
| 931 | - | // Check if service is already running | |
| 932 | - | containers, err := m.runtime.ListContainers(ctx, false) | |
| 933 | - | if err != nil { | |
| 934 | - | return fmt.Errorf("failed to list containers: %w", err) | |
| 935 | - | } | |
| 936 | - | ||
| 937 | - | for _, container := range containers { | |
| 938 | - | if container.Name == containerName { | |
| 939 | - | log.Info().Str("service", containerName).Msg("Service already running") | |
| 940 | - | return nil | |
| 941 | - | } | |
| 942 | - | } | |
| 943 | - | ||
| 944 | - | // Get exposed ports from the service image | |
| 945 | - | exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, serviceImage) | |
| 946 | - | if err != nil { | |
| 947 | - | log.Warn().Err(err).Str("image", serviceImage).Msg("Failed to get exposed ports from service image, using empty ports") | |
| 948 | - | exposedPorts = []int{} // Services don't need external ports by default | |
| 949 | - | } | |
| 950 | - | ||
| 951 | - | // Load environment for service (inherit from first app in group or specific app) | |
| 952 | - | var envVars []string | |
| 953 | - | if len(apps) > 0 { | |
| 954 | - | userEnvVars, err := m.envLoader.LoadEnvForRoute(apps[0]) | |
| 955 | - | if err != nil { | |
| 956 | - | log.Warn().Err(err).Str("domain", apps[0]).Msg("Failed to load environment for service, using empty env") | |
| 957 | - | userEnvVars = []string{} | |
| 958 | - | } | |
| 959 | - | ||
| 960 | - | // Get environment variables from image ENV directives | |
| 961 | - | dockerfileEnvVars, err := m.runtime.InspectImageEnv(ctx, serviceImage) | |
| 962 | - | if err != nil { | |
| 963 | - | log.Warn().Err(err).Str("image", serviceImage).Msg("Failed to inspect service image environment variables") | |
| 964 | - | dockerfileEnvVars = []string{} | |
| 965 | - | } | |
| 966 | - | ||
| 967 | - | // Merge environments | |
| 968 | - | envVars = mergeEnvironmentVariables(dockerfileEnvVars, userEnvVars) | |
| 969 | - | } | |
| 970 | - | ||
| 971 | - | // Handle volumes for the service | |
| 972 | - | volumes := make(map[string]string) | |
| 973 | - | if m.config.Volumes.AutoCreate { | |
| 974 | - | volumePaths, err := m.runtime.InspectImageVolumes(ctx, serviceImage) | |
| 975 | - | if err != nil { | |
| 976 | - | log.Warn().Err(err).Str("image", serviceImage).Msg("Failed to inspect service image volumes") | |
| 977 | - | } else if len(volumePaths) > 0 { | |
| 978 | - | for _, volumePath := range volumePaths { | |
| 979 | - | volumeName := fmt.Sprintf("%s-%s-%s", | |
| 980 | - | m.config.Volumes.Prefix, | |
| 981 | - | strings.ReplaceAll(identifier, ".", "-"), | |
| 982 | - | strings.ReplaceAll(strings.Trim(volumePath, "/"), "/", "-")) | |
| 983 | - | ||
| 984 | - | // Create volume if it doesn't exist | |
| 985 | - | exists, err := m.runtime.VolumeExists(ctx, volumeName) | |
| 986 | - | if err != nil { | |
| 987 | - | log.Warn().Err(err).Str("volume", volumeName).Msg("Failed to check service volume") | |
| 988 | - | continue | |
| 989 | - | } | |
| 990 | - | ||
| 991 | - | if !exists { | |
| 992 | - | if err := m.runtime.CreateVolume(ctx, volumeName); err != nil { | |
| 993 | - | log.Error().Err(err).Str("volume", volumeName).Msg("Failed to create service volume") | |
| 994 | - | continue | |
| 995 | - | } | |
| 996 | - | log.Info().Str("volume", volumeName).Str("path", volumePath).Msg("Created volume for service") | |
| 997 | - | } | |
| 998 | - | ||
| 999 | - | volumes[volumePath] = volumeName | |
| 1000 | - | } | |
| 1001 | - | } | |
| 1002 | - | } | |
| 1003 | - | ||
| 1004 | - | // Create service container configuration | |
| 1005 | - | serviceConfig := &runtime.ContainerConfig{ | |
| 1006 | - | Image: serviceImage, | |
| 1007 | - | Name: containerName, | |
| 1008 | - | Ports: exposedPorts, | |
| 1009 | - | Env: envVars, | |
| 1010 | - | Volumes: volumes, | |
| 1011 | - | NetworkMode: networkName, | |
| 1012 | - | Hostname: serviceName, | |
| 1013 | - | Labels: map[string]string{ | |
| 1014 | - | "gordon.managed": "true", | |
| 1015 | - | "gordon.service": "true", | |
| 1016 | - | "gordon.attached": identifier, | |
| 1017 | - | "gordon.image": serviceImage, | |
| 1018 | - | }, | |
| 1019 | - | AutoRemove: false, | |
| 1020 | - | } | |
| 1021 | - | ||
| 1022 | - | // Create and start the service container | |
| 1023 | - | container, err := m.runtime.CreateContainer(ctx, serviceConfig) | |
| 1024 | - | if err != nil { | |
| 1025 | - | return fmt.Errorf("failed to create service container %s: %w", containerName, err) | |
| 1026 | - | } | |
| 1027 | - | ||
| 1028 | - | if err := m.runtime.StartContainer(ctx, container.ID); err != nil { | |
| 1029 | - | m.runtime.RemoveContainer(ctx, container.ID, true) | |
| 1030 | - | return fmt.Errorf("failed to start service container %s: %w", containerName, err) | |
| 1031 | - | } | |
| 1032 | - | ||
| 1033 | - | log.Info(). | |
| 1034 | - | Str("service", containerName). | |
| 1035 | - | Str("image", serviceImage). | |
| 1036 | - | Str("network", networkName). | |
| 1037 | - | Str("attached_to", identifier). | |
| 1038 | - | Msg("Service deployed successfully") | |
| 1039 | - | ||
| 1040 | - | return nil | |
| 1041 | - | } | |
| 1042 | - | ||
| 1043 | - | // GetNetworkForApp determines which network an app should use | |
| 1044 | - | func (m *Manager) GetNetworkForApp(domain string) string { | |
| 1045 | - | if !m.config.NetworkIsolation.Enabled { | |
| 1046 | - | return "bridge" // Use default Docker bridge network | |
| 1047 | - | } | |
| 1048 | - | ||
| 1049 | - | // Check if domain is part of a network group | |
| 1050 | - | for groupName, domains := range m.config.NetworkGroups { | |
| 1051 | - | for _, d := range domains { | |
| 1052 | - | if d == domain { | |
| 1053 | - | return m.generateNetworkName(groupName) | |
| 1054 | - | } | |
| 1055 | - | } | |
| 1056 | - | } | |
| 1057 | - | ||
| 1058 | - | // Default: app gets its own network | |
| 1059 | - | return m.generateNetworkName(domain) | |
| 1060 | - | } | |
| 1061 | - | ||
| 1062 | - | // generateNetworkName creates a network name from an identifier | |
| 1063 | - | func (m *Manager) generateNetworkName(identifier string) string { | |
| 1064 | - | // "myapp.example.com" -> "gordon-myapp-example-com" | |
| 1065 | - | // "backend" -> "gordon-backend" | |
| 1066 | - | return fmt.Sprintf("%s-%s", | |
| 1067 | - | m.config.NetworkIsolation.NetworkPrefix, | |
| 1068 | - | strings.ReplaceAll(identifier, ".", "-")) | |
| 1069 | - | } | |
| 1070 | - | ||
| 1071 | - | // GetAppsForNetwork returns all apps that should have access to a network | |
| 1072 | - | func (m *Manager) GetAppsForNetwork(identifier string) []string { | |
| 1073 | - | // Check if it's a network group | |
| 1074 | - | if apps, ok := m.config.NetworkGroups[identifier]; ok { | |
| 1075 | - | return apps | |
| 1076 | - | } | |
| 1077 | - | ||
| 1078 | - | // Single app network | |
| 1079 | - | return []string{identifier} | |
| 1080 | - | } | |
| 1081 | - | ||
| 1082 | - | // CreateNetworkIfNeeded creates a network if it doesn't exist | |
| 1083 | - | func (m *Manager) CreateNetworkIfNeeded(ctx context.Context, networkName string) error { | |
| 1084 | - | if networkName == "bridge" || networkName == "default" { | |
| 1085 | - | return nil // Don't try to create default networks | |
| 1086 | - | } | |
| 1087 | - | ||
| 1088 | - | exists, err := m.runtime.NetworkExists(ctx, networkName) | |
| 1089 | - | if err != nil { | |
| 1090 | - | return fmt.Errorf("failed to check if network exists: %w", err) | |
| 1091 | - | } | |
| 1092 | - | ||
| 1093 | - | if !exists { | |
| 1094 | - | options := map[string]string{ | |
| 1095 | - | "driver": "bridge", | |
| 1096 | - | } | |
| 1097 | - | ||
| 1098 | - | if err := m.runtime.CreateNetwork(ctx, networkName, options); err != nil { | |
| 1099 | - | return fmt.Errorf("failed to create network %s: %w", networkName, err) | |
| 1100 | - | } | |
| 1101 | - | ||
| 1102 | - | log.Info().Str("network", networkName).Msg("Created network for app isolation") | |
| 1103 | - | } | |
| 1104 | - | ||
| 1105 | - | return nil | |
| 1106 | - | } | |
D internal/container/manager_autostart_test.go +0 -253
| @@ -1,253 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | "github.com/stretchr/testify/assert" | |
| 7 | - | "github.com/stretchr/testify/require" | |
| 8 | - | "go.uber.org/mock/gomock" | |
| 9 | - | ||
| 10 | - | "gordon/internal/config" | |
| 11 | - | "gordon/internal/testutils" | |
| 12 | - | "gordon/internal/testutils/mocks" | |
| 13 | - | "gordon/pkg/runtime" | |
| 14 | - | ) | |
| 15 | - | ||
| 16 | - | func TestManager_AutoStartContainers(t *testing.T) { | |
| 17 | - | ctrl := gomock.NewController(t) | |
| 18 | - | defer ctrl.Finish() | |
| 19 | - | ||
| 20 | - | ctx := testutils.TestContext(t) | |
| 21 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 22 | - | ||
| 23 | - | cfg := &config.Config{ | |
| 24 | - | Server: config.ServerConfig{ | |
| 25 | - | Runtime: "docker", | |
| 26 | - | }, | |
| 27 | - | Routes: map[string]string{ | |
| 28 | - | "app1.example.com": "nginx:latest", | |
| 29 | - | "app2.example.com": "apache:latest", | |
| 30 | - | }, | |
| 31 | - | Volumes: config.VolumeConfig{ | |
| 32 | - | AutoCreate: false, | |
| 33 | - | }, | |
| 34 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 35 | - | Enabled: false, | |
| 36 | - | }, | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 40 | - | require.NoError(t, err) | |
| 41 | - | ||
| 42 | - | // Mock existing containers - one Gordon-managed running, one stopped | |
| 43 | - | existingContainers := []*runtime.Container{ | |
| 44 | - | { | |
| 45 | - | ID: "running-container", | |
| 46 | - | Name: "gordon-app1.example.com", | |
| 47 | - | Image: "nginx:latest", | |
| 48 | - | Labels: map[string]string{ | |
| 49 | - | "gordon.managed": "true", | |
| 50 | - | "gordon.domain": "app1.example.com", | |
| 51 | - | }, | |
| 52 | - | }, | |
| 53 | - | { | |
| 54 | - | ID: "stopped-container", | |
| 55 | - | Name: "gordon-app2.example.com", | |
| 56 | - | Image: "apache:latest", | |
| 57 | - | Labels: map[string]string{ | |
| 58 | - | "gordon.managed": "true", | |
| 59 | - | "gordon.domain": "app2.example.com", | |
| 60 | - | }, | |
| 61 | - | }, | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return(existingContainers, nil) | |
| 65 | - | ||
| 66 | - | // First container is already running | |
| 67 | - | mockRuntime.EXPECT().IsContainerRunning(ctx, "running-container").Return(true, nil) | |
| 68 | - | ||
| 69 | - | // Second container is stopped, should be started | |
| 70 | - | mockRuntime.EXPECT().IsContainerRunning(ctx, "stopped-container").Return(false, nil) | |
| 71 | - | mockRuntime.EXPECT().StartContainer(ctx, "stopped-container").Return(nil) | |
| 72 | - | ||
| 73 | - | // Mock inspection of restarted container | |
| 74 | - | startedContainer := &runtime.Container{ | |
| 75 | - | ID: "stopped-container", | |
| 76 | - | Name: "gordon-app2.example.com", | |
| 77 | - | Image: "apache:latest", | |
| 78 | - | Ports: []int{80}, | |
| 79 | - | } | |
| 80 | - | mockRuntime.EXPECT().InspectContainer(ctx, "stopped-container").Return(startedContainer, nil) | |
| 81 | - | ||
| 82 | - | err = manager.AutoStartContainers(ctx) | |
| 83 | - | ||
| 84 | - | assert.NoError(t, err) | |
| 85 | - | assert.Len(t, manager.containers, 2) | |
| 86 | - | assert.Contains(t, manager.containers, "app1.example.com") | |
| 87 | - | assert.Contains(t, manager.containers, "app2.example.com") | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | func TestManager_AutoStartContainers_DeployNew(t *testing.T) { | |
| 91 | - | ctrl := gomock.NewController(t) | |
| 92 | - | defer ctrl.Finish() | |
| 93 | - | ||
| 94 | - | ctx := testutils.TestContext(t) | |
| 95 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 96 | - | ||
| 97 | - | cfg := &config.Config{ | |
| 98 | - | Server: config.ServerConfig{ | |
| 99 | - | Runtime: "docker", | |
| 100 | - | }, | |
| 101 | - | Routes: map[string]string{ | |
| 102 | - | "new-app.example.com": "redis:latest", | |
| 103 | - | }, | |
| 104 | - | Volumes: config.VolumeConfig{ | |
| 105 | - | AutoCreate: false, | |
| 106 | - | }, | |
| 107 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 108 | - | Enabled: false, | |
| 109 | - | }, | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 113 | - | require.NoError(t, err) | |
| 114 | - | ||
| 115 | - | // No existing containers | |
| 116 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 117 | - | ||
| 118 | - | // Should deploy new container | |
| 119 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 120 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"redis:latest"}, nil) | |
| 121 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 122 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil) | |
| 123 | - | ||
| 124 | - | newContainer := &runtime.Container{ | |
| 125 | - | ID: "new-container", | |
| 126 | - | Name: "gordon-new-app.example.com", | |
| 127 | - | Image: "redis:latest", | |
| 128 | - | Ports: []int{6379}, | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil) | |
| 132 | - | mockRuntime.EXPECT().StartContainer(ctx, "new-container").Return(nil) | |
| 133 | - | mockRuntime.EXPECT().InspectContainer(ctx, "new-container").Return(newContainer, nil) | |
| 134 | - | ||
| 135 | - | err = manager.AutoStartContainers(ctx) | |
| 136 | - | ||
| 137 | - | assert.NoError(t, err) | |
| 138 | - | assert.Len(t, manager.containers, 1) | |
| 139 | - | assert.Contains(t, manager.containers, "new-app.example.com") | |
| 140 | - | } | |
| 141 | - | ||
| 142 | - | func TestManager_AutoStartContainers_CleanupConflicts(t *testing.T) { | |
| 143 | - | ctrl := gomock.NewController(t) | |
| 144 | - | defer ctrl.Finish() | |
| 145 | - | ||
| 146 | - | ctx := testutils.TestContext(t) | |
| 147 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 148 | - | ||
| 149 | - | cfg := &config.Config{ | |
| 150 | - | Server: config.ServerConfig{ | |
| 151 | - | Runtime: "docker", | |
| 152 | - | }, | |
| 153 | - | Routes: map[string]string{ | |
| 154 | - | "app.example.com": "nginx:latest", | |
| 155 | - | }, | |
| 156 | - | Volumes: config.VolumeConfig{ | |
| 157 | - | AutoCreate: false, | |
| 158 | - | }, | |
| 159 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 160 | - | Enabled: false, | |
| 161 | - | }, | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 165 | - | require.NoError(t, err) | |
| 166 | - | ||
| 167 | - | // Existing containers include a non-Gordon container with same image | |
| 168 | - | existingContainers := []*runtime.Container{ | |
| 169 | - | { | |
| 170 | - | ID: "unmanaged-container", | |
| 171 | - | Name: "some-nginx-container", | |
| 172 | - | Image: "nginx:latest", | |
| 173 | - | // No Gordon labels - not managed | |
| 174 | - | }, | |
| 175 | - | } | |
| 176 | - | ||
| 177 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return(existingContainers, nil) | |
| 178 | - | ||
| 179 | - | // Should stop and remove the conflicting container | |
| 180 | - | mockRuntime.EXPECT().IsContainerRunning(ctx, "unmanaged-container").Return(true, nil) | |
| 181 | - | mockRuntime.EXPECT().StopContainer(ctx, "unmanaged-container").Return(nil) | |
| 182 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "unmanaged-container", true).Return(nil) | |
| 183 | - | ||
| 184 | - | // Then deploy new Gordon-managed container | |
| 185 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 186 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"nginx:latest"}, nil) | |
| 187 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "nginx:latest").Return([]int{80}, nil) | |
| 188 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "nginx:latest").Return([]string{}, nil) | |
| 189 | - | ||
| 190 | - | newContainer := &runtime.Container{ | |
| 191 | - | ID: "gordon-container", | |
| 192 | - | Name: "gordon-app.example.com", | |
| 193 | - | Image: "nginx:latest", | |
| 194 | - | Ports: []int{80}, | |
| 195 | - | } | |
| 196 | - | ||
| 197 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil) | |
| 198 | - | mockRuntime.EXPECT().StartContainer(ctx, "gordon-container").Return(nil) | |
| 199 | - | mockRuntime.EXPECT().InspectContainer(ctx, "gordon-container").Return(newContainer, nil) | |
| 200 | - | ||
| 201 | - | err = manager.AutoStartContainers(ctx) | |
| 202 | - | ||
| 203 | - | assert.NoError(t, err) | |
| 204 | - | assert.Len(t, manager.containers, 1) | |
| 205 | - | assert.Contains(t, manager.containers, "app.example.com") | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | func TestManager_AutoStartContainers_NoRoutes(t *testing.T) { | |
| 209 | - | ctrl := gomock.NewController(t) | |
| 210 | - | defer ctrl.Finish() | |
| 211 | - | ||
| 212 | - | ctx := testutils.TestContext(t) | |
| 213 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 214 | - | ||
| 215 | - | cfg := &config.Config{ | |
| 216 | - | Server: config.ServerConfig{ | |
| 217 | - | Runtime: "docker", | |
| 218 | - | }, | |
| 219 | - | // No routes configured | |
| 220 | - | Routes: map[string]string{}, | |
| 221 | - | } | |
| 222 | - | ||
| 223 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 224 | - | require.NoError(t, err) | |
| 225 | - | ||
| 226 | - | err = manager.AutoStartContainers(ctx) | |
| 227 | - | ||
| 228 | - | assert.NoError(t, err) | |
| 229 | - | assert.Empty(t, manager.containers) | |
| 230 | - | } | |
| 231 | - | ||
| 232 | - | func TestManager_normalizeImageName(t *testing.T) { | |
| 233 | - | manager := &Manager{} | |
| 234 | - | ||
| 235 | - | tests := []struct { | |
| 236 | - | input string | |
| 237 | - | expected string | |
| 238 | - | }{ | |
| 239 | - | {"nginx", "docker.io/library/nginx"}, | |
| 240 | - | {"nginx:latest", "docker.io/library/nginx"}, | |
| 241 | - | {"user/repo", "docker.io/user/repo"}, | |
| 242 | - | {"user/repo:tag", "docker.io/user/repo"}, | |
| 243 | - | {"registry.com/user/repo", "registry.com/user/repo"}, | |
| 244 | - | {"registry.com/user/repo:tag", "registry.com/user/repo"}, | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | for _, tt := range tests { | |
| 248 | - | t.Run(tt.input, func(t *testing.T) { | |
| 249 | - | result := manager.normalizeImageName(tt.input) | |
| 250 | - | assert.Equal(t, tt.expected, result) | |
| 251 | - | }) | |
| 252 | - | } | |
| 253 | - | }⏎̸ | |
D internal/container/manager_deploy_test.go +0 -394
| @@ -1,394 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "testing" | |
| 6 | - | ||
| 7 | - | "github.com/stretchr/testify/assert" | |
| 8 | - | "github.com/stretchr/testify/require" | |
| 9 | - | "go.uber.org/mock/gomock" | |
| 10 | - | ||
| 11 | - | "gordon/internal/config" | |
| 12 | - | "gordon/internal/testutils" | |
| 13 | - | "gordon/internal/testutils/mocks" | |
| 14 | - | "gordon/pkg/runtime" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | func TestManager_DeployContainer_ImagePull(t *testing.T) { | |
| 18 | - | ctrl := gomock.NewController(t) | |
| 19 | - | defer ctrl.Finish() | |
| 20 | - | ||
| 21 | - | ctx := testutils.TestContext(t) | |
| 22 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 23 | - | ||
| 24 | - | cfg := &config.Config{ | |
| 25 | - | Server: config.ServerConfig{ | |
| 26 | - | Runtime: "docker", | |
| 27 | - | }, | |
| 28 | - | Volumes: config.VolumeConfig{ | |
| 29 | - | AutoCreate: false, | |
| 30 | - | }, | |
| 31 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 32 | - | Enabled: false, | |
| 33 | - | }, | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 37 | - | require.NoError(t, err) | |
| 38 | - | ||
| 39 | - | route := config.Route{ | |
| 40 | - | Domain: "test.example.com", | |
| 41 | - | Image: "alpine:latest", | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | // Image not found locally, should pull | |
| 45 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 46 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{}, nil) | |
| 47 | - | mockRuntime.EXPECT().PullImage(ctx, "alpine:latest").Return(nil) | |
| 48 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "alpine:latest").Return([]int{}, nil) | |
| 49 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "alpine:latest").Return([]string{}, nil) | |
| 50 | - | ||
| 51 | - | expectedContainer := &runtime.Container{ | |
| 52 | - | ID: "container456", | |
| 53 | - | Name: "gordon-test.example.com", | |
| 54 | - | Image: "alpine:latest", | |
| 55 | - | Status: "running", | |
| 56 | - | Ports: []int{}, | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 60 | - | mockRuntime.EXPECT().StartContainer(ctx, "container456").Return(nil) | |
| 61 | - | mockRuntime.EXPECT().InspectContainer(ctx, "container456").Return(expectedContainer, nil) | |
| 62 | - | ||
| 63 | - | container, err := manager.DeployContainer(ctx, route) | |
| 64 | - | ||
| 65 | - | assert.NoError(t, err) | |
| 66 | - | assert.Equal(t, expectedContainer, container) | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | func TestManager_DeployContainer_WithAuth(t *testing.T) { | |
| 70 | - | ctrl := gomock.NewController(t) | |
| 71 | - | defer ctrl.Finish() | |
| 72 | - | ||
| 73 | - | ctx := testutils.TestContext(t) | |
| 74 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 75 | - | ||
| 76 | - | cfg := &config.Config{ | |
| 77 | - | Server: config.ServerConfig{ | |
| 78 | - | Runtime: "docker", | |
| 79 | - | RegistryDomain: "registry.example.com", | |
| 80 | - | }, | |
| 81 | - | RegistryAuth: config.RegistryAuthConfig{ | |
| 82 | - | Enabled: true, | |
| 83 | - | Username: "testuser", | |
| 84 | - | Password: "testpass", | |
| 85 | - | }, | |
| 86 | - | Volumes: config.VolumeConfig{ | |
| 87 | - | AutoCreate: false, | |
| 88 | - | }, | |
| 89 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 90 | - | Enabled: false, | |
| 91 | - | }, | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 95 | - | require.NoError(t, err) | |
| 96 | - | ||
| 97 | - | route := config.Route{ | |
| 98 | - | Domain: "test.example.com", | |
| 99 | - | Image: "myapp:latest", | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | // Should use authenticated pull with registry domain prefix | |
| 103 | - | expectedImageRef := "registry.example.com/myapp:latest" | |
| 104 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 105 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{}, nil) | |
| 106 | - | mockRuntime.EXPECT().PullImageWithAuth(ctx, expectedImageRef, "testuser", "testpass").Return(nil) | |
| 107 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, expectedImageRef).Return([]int{8080}, nil) | |
| 108 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, expectedImageRef).Return([]string{}, nil) | |
| 109 | - | ||
| 110 | - | expectedContainer := &runtime.Container{ | |
| 111 | - | ID: "container789", | |
| 112 | - | Name: "gordon-test.example.com", | |
| 113 | - | Image: expectedImageRef, | |
| 114 | - | Status: "running", | |
| 115 | - | Ports: []int{8080}, | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 119 | - | mockRuntime.EXPECT().StartContainer(ctx, "container789").Return(nil) | |
| 120 | - | mockRuntime.EXPECT().InspectContainer(ctx, "container789").Return(expectedContainer, nil) | |
| 121 | - | ||
| 122 | - | container, err := manager.DeployContainer(ctx, route) | |
| 123 | - | ||
| 124 | - | assert.NoError(t, err) | |
| 125 | - | assert.Equal(t, expectedContainer, container) | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | func TestManager_DeployContainer_ReplaceExisting(t *testing.T) { | |
| 129 | - | ctrl := gomock.NewController(t) | |
| 130 | - | defer ctrl.Finish() | |
| 131 | - | ||
| 132 | - | ctx := testutils.TestContext(t) | |
| 133 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 134 | - | ||
| 135 | - | cfg := &config.Config{ | |
| 136 | - | Server: config.ServerConfig{ | |
| 137 | - | Runtime: "docker", | |
| 138 | - | }, | |
| 139 | - | Volumes: config.VolumeConfig{ | |
| 140 | - | AutoCreate: false, | |
| 141 | - | }, | |
| 142 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 143 | - | Enabled: false, | |
| 144 | - | }, | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 148 | - | require.NoError(t, err) | |
| 149 | - | ||
| 150 | - | // Add existing container | |
| 151 | - | existingContainer := &runtime.Container{ | |
| 152 | - | ID: "old-container", | |
| 153 | - | Name: "gordon-test.example.com", | |
| 154 | - | Status: "running", | |
| 155 | - | } | |
| 156 | - | manager.containers["test.example.com"] = existingContainer | |
| 157 | - | ||
| 158 | - | route := config.Route{ | |
| 159 | - | Domain: "test.example.com", | |
| 160 | - | Image: "nginx:latest", | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // Should stop and remove existing container | |
| 164 | - | mockRuntime.EXPECT().StopContainer(ctx, "old-container").Return(nil) | |
| 165 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "old-container", true).Return(nil) | |
| 166 | - | ||
| 167 | - | // Check for orphaned containers | |
| 168 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 169 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"nginx:latest"}, nil) | |
| 170 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "nginx:latest").Return([]int{80}, nil) | |
| 171 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "nginx:latest").Return([]string{}, nil) | |
| 172 | - | ||
| 173 | - | newContainer := &runtime.Container{ | |
| 174 | - | ID: "new-container", | |
| 175 | - | Name: "gordon-test.example.com", | |
| 176 | - | Image: "nginx:latest", | |
| 177 | - | Status: "running", | |
| 178 | - | Ports: []int{80}, | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil) | |
| 182 | - | mockRuntime.EXPECT().StartContainer(ctx, "new-container").Return(nil) | |
| 183 | - | mockRuntime.EXPECT().InspectContainer(ctx, "new-container").Return(newContainer, nil) | |
| 184 | - | ||
| 185 | - | container, err := manager.DeployContainer(ctx, route) | |
| 186 | - | ||
| 187 | - | assert.NoError(t, err) | |
| 188 | - | assert.Equal(t, newContainer, container) | |
| 189 | - | assert.Equal(t, newContainer, manager.containers["test.example.com"]) | |
| 190 | - | } | |
| 191 | - | ||
| 192 | - | func TestManager_DeployContainer_Errors(t *testing.T) { | |
| 193 | - | tests := []struct { | |
| 194 | - | name string | |
| 195 | - | setupMocks func(*mocks.MockRuntime) | |
| 196 | - | expectedError string | |
| 197 | - | }{ | |
| 198 | - | { | |
| 199 | - | name: "pull image error", | |
| 200 | - | setupMocks: func(m *mocks.MockRuntime) { | |
| 201 | - | m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil) | |
| 202 | - | m.EXPECT().ListImages(gomock.Any()).Return([]string{}, nil) | |
| 203 | - | m.EXPECT().PullImage(gomock.Any(), "nginx:latest").Return(errors.New("pull failed")) | |
| 204 | - | }, | |
| 205 | - | expectedError: "failed to pull image", | |
| 206 | - | }, | |
| 207 | - | { | |
| 208 | - | name: "create container error", | |
| 209 | - | setupMocks: func(m *mocks.MockRuntime) { | |
| 210 | - | m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil) | |
| 211 | - | m.EXPECT().ListImages(gomock.Any()).Return([]string{"nginx:latest"}, nil) | |
| 212 | - | m.EXPECT().GetImageExposedPorts(gomock.Any(), "nginx:latest").Return([]int{80}, nil) | |
| 213 | - | m.EXPECT().InspectImageEnv(gomock.Any(), "nginx:latest").Return([]string{}, nil) | |
| 214 | - | m.EXPECT().CreateContainer(gomock.Any(), gomock.Any()).Return(nil, errors.New("create failed")) | |
| 215 | - | }, | |
| 216 | - | expectedError: "failed to create container", | |
| 217 | - | }, | |
| 218 | - | { | |
| 219 | - | name: "start container error", | |
| 220 | - | setupMocks: func(m *mocks.MockRuntime) { | |
| 221 | - | m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil) | |
| 222 | - | m.EXPECT().ListImages(gomock.Any()).Return([]string{"nginx:latest"}, nil) | |
| 223 | - | m.EXPECT().GetImageExposedPorts(gomock.Any(), "nginx:latest").Return([]int{80}, nil) | |
| 224 | - | m.EXPECT().InspectImageEnv(gomock.Any(), "nginx:latest").Return([]string{}, nil) | |
| 225 | - | container := &runtime.Container{ID: "test123"} | |
| 226 | - | m.EXPECT().CreateContainer(gomock.Any(), gomock.Any()).Return(container, nil) | |
| 227 | - | m.EXPECT().StartContainer(gomock.Any(), "test123").Return(errors.New("start failed")) | |
| 228 | - | m.EXPECT().RemoveContainer(gomock.Any(), "test123", true).Return(nil) | |
| 229 | - | }, | |
| 230 | - | expectedError: "failed to start container", | |
| 231 | - | }, | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | for _, tt := range tests { | |
| 235 | - | t.Run(tt.name, func(t *testing.T) { | |
| 236 | - | ctrl := gomock.NewController(t) | |
| 237 | - | defer ctrl.Finish() | |
| 238 | - | ||
| 239 | - | ctx := testutils.TestContext(t) | |
| 240 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 241 | - | ||
| 242 | - | cfg := &config.Config{ | |
| 243 | - | Server: config.ServerConfig{ | |
| 244 | - | Runtime: "docker", | |
| 245 | - | }, | |
| 246 | - | Volumes: config.VolumeConfig{ | |
| 247 | - | AutoCreate: false, | |
| 248 | - | }, | |
| 249 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 250 | - | Enabled: false, | |
| 251 | - | }, | |
| 252 | - | } | |
| 253 | - | ||
| 254 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 255 | - | require.NoError(t, err) | |
| 256 | - | ||
| 257 | - | route := config.Route{ | |
| 258 | - | Domain: "test.example.com", | |
| 259 | - | Image: "nginx:latest", | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | tt.setupMocks(mockRuntime) | |
| 263 | - | ||
| 264 | - | _, err = manager.DeployContainer(ctx, route) | |
| 265 | - | assert.Error(t, err) | |
| 266 | - | assert.Contains(t, err.Error(), tt.expectedError) | |
| 267 | - | }) | |
| 268 | - | } | |
| 269 | - | } | |
| 270 | - | ||
| 271 | - | func TestManager_DeployContainer_WithNetworkAttachments(t *testing.T) { | |
| 272 | - | ctrl := gomock.NewController(t) | |
| 273 | - | defer ctrl.Finish() | |
| 274 | - | ||
| 275 | - | ctx := testutils.TestContext(t) | |
| 276 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 277 | - | ||
| 278 | - | cfg := &config.Config{ | |
| 279 | - | Server: config.ServerConfig{ | |
| 280 | - | Runtime: "docker", | |
| 281 | - | }, | |
| 282 | - | Volumes: config.VolumeConfig{ | |
| 283 | - | AutoCreate: false, | |
| 284 | - | }, | |
| 285 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 286 | - | Enabled: true, | |
| 287 | - | NetworkPrefix: "gordon", | |
| 288 | - | }, | |
| 289 | - | NetworkGroups: map[string][]string{ | |
| 290 | - | "backend": {"api.example.com", "worker.example.com"}, | |
| 291 | - | }, | |
| 292 | - | Attachments: map[string][]string{ | |
| 293 | - | "api.example.com": {"redis:latest"}, | |
| 294 | - | "backend": {"postgres:latest"}, | |
| 295 | - | }, | |
| 296 | - | } | |
| 297 | - | ||
| 298 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 299 | - | require.NoError(t, err) | |
| 300 | - | ||
| 301 | - | route := config.Route{ | |
| 302 | - | Domain: "api.example.com", | |
| 303 | - | Image: "myapi:latest", | |
| 304 | - | } | |
| 305 | - | ||
| 306 | - | // Main container deployment expectations | |
| 307 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 308 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"myapi:latest"}, nil) | |
| 309 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "myapi:latest").Return([]int{8080}, nil) | |
| 310 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "myapi:latest").Return([]string{}, nil) | |
| 311 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-backend").Return(true, nil) | |
| 312 | - | ||
| 313 | - | expectedContainer := &runtime.Container{ | |
| 314 | - | ID: "main-container", | |
| 315 | - | Name: "gordon-api.example.com", | |
| 316 | - | Image: "myapi:latest", | |
| 317 | - | Status: "running", | |
| 318 | - | Ports: []int{8080}, | |
| 319 | - | } | |
| 320 | - | ||
| 321 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 322 | - | mockRuntime.EXPECT().StartContainer(ctx, "main-container").Return(nil) | |
| 323 | - | mockRuntime.EXPECT().InspectContainer(ctx, "main-container").Return(expectedContainer, nil) | |
| 324 | - | ||
| 325 | - | // Attachment deployment expectations for app-specific service | |
| 326 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 327 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 328 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil) | |
| 329 | - | ||
| 330 | - | redisContainer := &runtime.Container{ | |
| 331 | - | ID: "redis-container", | |
| 332 | - | Name: "api-example-com-redis", | |
| 333 | - | } | |
| 334 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(redisContainer, nil) | |
| 335 | - | mockRuntime.EXPECT().StartContainer(ctx, "redis-container").Return(nil) | |
| 336 | - | ||
| 337 | - | // Attachment deployment expectations for network group service | |
| 338 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{redisContainer}, nil) | |
| 339 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 340 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil) | |
| 341 | - | ||
| 342 | - | pgContainer := &runtime.Container{ | |
| 343 | - | ID: "pg-container", | |
| 344 | - | Name: "gordon-shared-postgres", | |
| 345 | - | } | |
| 346 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(pgContainer, nil) | |
| 347 | - | mockRuntime.EXPECT().StartContainer(ctx, "pg-container").Return(nil) | |
| 348 | - | ||
| 349 | - | container, err := manager.DeployContainer(ctx, route) | |
| 350 | - | ||
| 351 | - | assert.NoError(t, err) | |
| 352 | - | assert.Equal(t, expectedContainer, container) | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | func TestManager_DeployAttachedService(t *testing.T) { | |
| 356 | - | ctrl := gomock.NewController(t) | |
| 357 | - | defer ctrl.Finish() | |
| 358 | - | ||
| 359 | - | ctx := testutils.TestContext(t) | |
| 360 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 361 | - | ||
| 362 | - | cfg := &config.Config{ | |
| 363 | - | Volumes: config.VolumeConfig{ | |
| 364 | - | AutoCreate: true, | |
| 365 | - | Prefix: "gordon", | |
| 366 | - | }, | |
| 367 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 368 | - | NetworkPrefix: "gordon", | |
| 369 | - | }, | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 373 | - | require.NoError(t, err) | |
| 374 | - | ||
| 375 | - | // Test deploying redis service for an app | |
| 376 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 377 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 378 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{"REDIS_VERSION=7.0"}, nil) | |
| 379 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "redis:latest").Return([]string{"/data"}, nil) | |
| 380 | - | mockRuntime.EXPECT().VolumeExists(ctx, "gordon-test-example-com-data").Return(false, nil) | |
| 381 | - | mockRuntime.EXPECT().CreateVolume(ctx, "gordon-test-example-com-data").Return(nil) | |
| 382 | - | ||
| 383 | - | expectedContainer := &runtime.Container{ | |
| 384 | - | ID: "redis-service", | |
| 385 | - | Name: "test-example-com-redis", | |
| 386 | - | } | |
| 387 | - | ||
| 388 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 389 | - | mockRuntime.EXPECT().StartContainer(ctx, "redis-service").Return(nil) | |
| 390 | - | ||
| 391 | - | err = manager.DeployAttachedService(ctx, "test.example.com", "redis:latest") | |
| 392 | - | ||
| 393 | - | assert.NoError(t, err) | |
| 394 | - | }⏎̸ | |
D internal/container/manager_network_test.go +0 -421
| @@ -1,421 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "testing" | |
| 6 | - | ||
| 7 | - | "github.com/stretchr/testify/assert" | |
| 8 | - | "github.com/stretchr/testify/require" | |
| 9 | - | "go.uber.org/mock/gomock" | |
| 10 | - | ||
| 11 | - | "gordon/internal/config" | |
| 12 | - | "gordon/internal/testutils" | |
| 13 | - | "gordon/internal/testutils/mocks" | |
| 14 | - | "gordon/pkg/runtime" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | func TestManager_GetNetworkForApp_WithGroups(t *testing.T) { | |
| 18 | - | manager := &Manager{ | |
| 19 | - | config: &config.Config{ | |
| 20 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 21 | - | Enabled: true, | |
| 22 | - | NetworkPrefix: "gordon", | |
| 23 | - | }, | |
| 24 | - | NetworkGroups: map[string][]string{ | |
| 25 | - | "backend": {"api.example.com", "worker.example.com"}, | |
| 26 | - | "frontend": {"web.example.com", "cdn.example.com"}, | |
| 27 | - | }, | |
| 28 | - | }, | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | // Test app in backend group | |
| 32 | - | network := manager.GetNetworkForApp("api.example.com") | |
| 33 | - | assert.Equal(t, "gordon-backend", network) | |
| 34 | - | ||
| 35 | - | // Test app in frontend group | |
| 36 | - | network = manager.GetNetworkForApp("web.example.com") | |
| 37 | - | assert.Equal(t, "gordon-frontend", network) | |
| 38 | - | ||
| 39 | - | // Test standalone app | |
| 40 | - | network = manager.GetNetworkForApp("standalone.example.com") | |
| 41 | - | assert.Equal(t, "gordon-standalone-example-com", network) | |
| 42 | - | ||
| 43 | - | // Test disabled network isolation | |
| 44 | - | manager.config.NetworkIsolation.Enabled = false | |
| 45 | - | network = manager.GetNetworkForApp("test.example.com") | |
| 46 | - | assert.Equal(t, "bridge", network) | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | func TestManager_generateNetworkName(t *testing.T) { | |
| 50 | - | manager := &Manager{ | |
| 51 | - | config: &config.Config{ | |
| 52 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 53 | - | NetworkPrefix: "gordon", | |
| 54 | - | }, | |
| 55 | - | }, | |
| 56 | - | } | |
| 57 | - | ||
| 58 | - | networkName := manager.generateNetworkName("test.example.com") | |
| 59 | - | assert.Equal(t, "gordon-test-example-com", networkName) | |
| 60 | - | ||
| 61 | - | networkName = manager.generateNetworkName("backend") | |
| 62 | - | assert.Equal(t, "gordon-backend", networkName) | |
| 63 | - | ||
| 64 | - | networkName = manager.generateNetworkName("complex-app.subdomain.example.com") | |
| 65 | - | assert.Equal(t, "gordon-complex-app-subdomain-example-com", networkName) | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | func TestManager_GetAppsForNetwork(t *testing.T) { | |
| 69 | - | manager := &Manager{ | |
| 70 | - | config: &config.Config{ | |
| 71 | - | NetworkGroups: map[string][]string{ | |
| 72 | - | "backend": {"api.example.com", "worker.example.com"}, | |
| 73 | - | "frontend": {"web.example.com"}, | |
| 74 | - | }, | |
| 75 | - | }, | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | // Test network group | |
| 79 | - | apps := manager.GetAppsForNetwork("backend") | |
| 80 | - | assert.Equal(t, []string{"api.example.com", "worker.example.com"}, apps) | |
| 81 | - | ||
| 82 | - | // Test single app group | |
| 83 | - | apps = manager.GetAppsForNetwork("frontend") | |
| 84 | - | assert.Equal(t, []string{"web.example.com"}, apps) | |
| 85 | - | ||
| 86 | - | // Test single app (not in any group) | |
| 87 | - | apps = manager.GetAppsForNetwork("standalone.example.com") | |
| 88 | - | assert.Equal(t, []string{"standalone.example.com"}, apps) | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | func TestManager_CreateNetworkIfNeeded(t *testing.T) { | |
| 92 | - | ctrl := gomock.NewController(t) | |
| 93 | - | defer ctrl.Finish() | |
| 94 | - | ||
| 95 | - | ctx := testutils.TestContext(t) | |
| 96 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 97 | - | ||
| 98 | - | cfg := &config.Config{} | |
| 99 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 100 | - | require.NoError(t, err) | |
| 101 | - | ||
| 102 | - | // Test bridge network (should not try to create) | |
| 103 | - | err = manager.CreateNetworkIfNeeded(ctx, "bridge") | |
| 104 | - | assert.NoError(t, err) | |
| 105 | - | ||
| 106 | - | // Test default network (should not try to create) | |
| 107 | - | err = manager.CreateNetworkIfNeeded(ctx, "default") | |
| 108 | - | assert.NoError(t, err) | |
| 109 | - | ||
| 110 | - | // Test custom network that doesn't exist | |
| 111 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, nil) | |
| 112 | - | mockRuntime.EXPECT().CreateNetwork(ctx, "gordon-test", map[string]string{"driver": "bridge"}).Return(nil) | |
| 113 | - | ||
| 114 | - | err = manager.CreateNetworkIfNeeded(ctx, "gordon-test") | |
| 115 | - | assert.NoError(t, err) | |
| 116 | - | ||
| 117 | - | // Test custom network that already exists | |
| 118 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-existing").Return(true, nil) | |
| 119 | - | ||
| 120 | - | err = manager.CreateNetworkIfNeeded(ctx, "gordon-existing") | |
| 121 | - | assert.NoError(t, err) | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | func TestManager_CreateNetworkIfNeeded_Errors(t *testing.T) { | |
| 125 | - | ctrl := gomock.NewController(t) | |
| 126 | - | defer ctrl.Finish() | |
| 127 | - | ||
| 128 | - | ctx := testutils.TestContext(t) | |
| 129 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 130 | - | ||
| 131 | - | cfg := &config.Config{} | |
| 132 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 133 | - | require.NoError(t, err) | |
| 134 | - | ||
| 135 | - | // Test error checking if network exists | |
| 136 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, errors.New("check failed")) | |
| 137 | - | ||
| 138 | - | err = manager.CreateNetworkIfNeeded(ctx, "gordon-test") | |
| 139 | - | assert.Error(t, err) | |
| 140 | - | assert.Contains(t, err.Error(), "failed to check if network exists") | |
| 141 | - | ||
| 142 | - | // Test error creating network | |
| 143 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, nil) | |
| 144 | - | mockRuntime.EXPECT().CreateNetwork(ctx, "gordon-test", map[string]string{"driver": "bridge"}).Return(errors.New("create failed")) | |
| 145 | - | ||
| 146 | - | err = manager.CreateNetworkIfNeeded(ctx, "gordon-test") | |
| 147 | - | assert.Error(t, err) | |
| 148 | - | assert.Contains(t, err.Error(), "failed to create network") | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | func TestManager_cleanupNetworkIfEmpty(t *testing.T) { | |
| 152 | - | ctrl := gomock.NewController(t) | |
| 153 | - | defer ctrl.Finish() | |
| 154 | - | ||
| 155 | - | ctx := testutils.TestContext(t) | |
| 156 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 157 | - | ||
| 158 | - | cfg := &config.Config{} | |
| 159 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 160 | - | require.NoError(t, err) | |
| 161 | - | ||
| 162 | - | // Test cleanup of empty network | |
| 163 | - | networks := []*runtime.NetworkInfo{ | |
| 164 | - | { | |
| 165 | - | Name: "gordon-test", | |
| 166 | - | Containers: []string{}, // Empty network | |
| 167 | - | }, | |
| 168 | - | { | |
| 169 | - | Name: "gordon-used", | |
| 170 | - | Containers: []string{"container1"}, // Has containers | |
| 171 | - | }, | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil) | |
| 175 | - | mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test").Return(nil) | |
| 176 | - | ||
| 177 | - | err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test") | |
| 178 | - | assert.NoError(t, err) | |
| 179 | - | ||
| 180 | - | // Test network with containers (should not be removed) | |
| 181 | - | mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil) | |
| 182 | - | ||
| 183 | - | err = manager.cleanupNetworkIfEmpty(ctx, "gordon-used") | |
| 184 | - | assert.NoError(t, err) | |
| 185 | - | ||
| 186 | - | // Test bridge network (should not try to remove) | |
| 187 | - | err = manager.cleanupNetworkIfEmpty(ctx, "bridge") | |
| 188 | - | assert.NoError(t, err) | |
| 189 | - | ||
| 190 | - | // Test default network (should not try to remove) | |
| 191 | - | err = manager.cleanupNetworkIfEmpty(ctx, "default") | |
| 192 | - | assert.NoError(t, err) | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | func TestManager_cleanupNetworkIfEmpty_Errors(t *testing.T) { | |
| 196 | - | ctrl := gomock.NewController(t) | |
| 197 | - | defer ctrl.Finish() | |
| 198 | - | ||
| 199 | - | ctx := testutils.TestContext(t) | |
| 200 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 201 | - | ||
| 202 | - | cfg := &config.Config{} | |
| 203 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 204 | - | require.NoError(t, err) | |
| 205 | - | ||
| 206 | - | // Test error listing networks | |
| 207 | - | mockRuntime.EXPECT().ListNetworks(ctx).Return(nil, errors.New("list failed")) | |
| 208 | - | ||
| 209 | - | err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test") | |
| 210 | - | assert.Error(t, err) | |
| 211 | - | assert.Contains(t, err.Error(), "failed to list networks") | |
| 212 | - | ||
| 213 | - | // Test error removing network | |
| 214 | - | networks := []*runtime.NetworkInfo{ | |
| 215 | - | { | |
| 216 | - | Name: "gordon-test", | |
| 217 | - | Containers: []string{}, | |
| 218 | - | }, | |
| 219 | - | } | |
| 220 | - | ||
| 221 | - | mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil) | |
| 222 | - | mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test").Return(errors.New("remove failed")) | |
| 223 | - | ||
| 224 | - | err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test") | |
| 225 | - | assert.Error(t, err) | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | func TestManager_DeployAttachedService_SharedService(t *testing.T) { | |
| 229 | - | ctrl := gomock.NewController(t) | |
| 230 | - | defer ctrl.Finish() | |
| 231 | - | ||
| 232 | - | ctx := testutils.TestContext(t) | |
| 233 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 234 | - | ||
| 235 | - | cfg := &config.Config{ | |
| 236 | - | Volumes: config.VolumeConfig{ | |
| 237 | - | AutoCreate: false, | |
| 238 | - | }, | |
| 239 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 240 | - | NetworkPrefix: "gordon", | |
| 241 | - | }, | |
| 242 | - | NetworkGroups: map[string][]string{ | |
| 243 | - | "backend": {"api.example.com", "worker.example.com"}, | |
| 244 | - | }, | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 248 | - | require.NoError(t, err) | |
| 249 | - | ||
| 250 | - | // Test deploying shared service for network group | |
| 251 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 252 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 253 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{"POSTGRES_VERSION=15"}, nil) | |
| 254 | - | ||
| 255 | - | expectedContainer := &runtime.Container{ | |
| 256 | - | ID: "postgres-service", | |
| 257 | - | Name: "gordon-shared-postgres", | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 261 | - | mockRuntime.EXPECT().StartContainer(ctx, "postgres-service").Return(nil) | |
| 262 | - | ||
| 263 | - | err = manager.DeployAttachedService(ctx, "backend", "postgres:latest") | |
| 264 | - | ||
| 265 | - | assert.NoError(t, err) | |
| 266 | - | } | |
| 267 | - | ||
| 268 | - | func TestManager_DeployAttachedService_AppSpecific(t *testing.T) { | |
| 269 | - | ctrl := gomock.NewController(t) | |
| 270 | - | defer ctrl.Finish() | |
| 271 | - | ||
| 272 | - | ctx := testutils.TestContext(t) | |
| 273 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 274 | - | ||
| 275 | - | cfg := &config.Config{ | |
| 276 | - | Volumes: config.VolumeConfig{ | |
| 277 | - | AutoCreate: false, | |
| 278 | - | }, | |
| 279 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 280 | - | NetworkPrefix: "gordon", | |
| 281 | - | }, | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 285 | - | require.NoError(t, err) | |
| 286 | - | ||
| 287 | - | // Test deploying app-specific service | |
| 288 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 289 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 290 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil) | |
| 291 | - | ||
| 292 | - | expectedContainer := &runtime.Container{ | |
| 293 | - | ID: "redis-service", | |
| 294 | - | Name: "api-example-com-redis", | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 298 | - | mockRuntime.EXPECT().StartContainer(ctx, "redis-service").Return(nil) | |
| 299 | - | ||
| 300 | - | err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest") | |
| 301 | - | ||
| 302 | - | assert.NoError(t, err) | |
| 303 | - | } | |
| 304 | - | ||
| 305 | - | func TestManager_DeployAttachedService_AlreadyRunning(t *testing.T) { | |
| 306 | - | ctrl := gomock.NewController(t) | |
| 307 | - | defer ctrl.Finish() | |
| 308 | - | ||
| 309 | - | ctx := testutils.TestContext(t) | |
| 310 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 311 | - | ||
| 312 | - | cfg := &config.Config{ | |
| 313 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 314 | - | NetworkPrefix: "gordon", | |
| 315 | - | }, | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 319 | - | require.NoError(t, err) | |
| 320 | - | ||
| 321 | - | // Mock existing container with same name | |
| 322 | - | existingContainers := []*runtime.Container{ | |
| 323 | - | { | |
| 324 | - | ID: "existing-redis", | |
| 325 | - | Name: "api-example-com-redis", | |
| 326 | - | }, | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return(existingContainers, nil) | |
| 330 | - | ||
| 331 | - | err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest") | |
| 332 | - | ||
| 333 | - | assert.NoError(t, err) | |
| 334 | - | } | |
| 335 | - | ||
| 336 | - | func TestManager_DeployAttachedService_WithVolumes(t *testing.T) { | |
| 337 | - | ctrl := gomock.NewController(t) | |
| 338 | - | defer ctrl.Finish() | |
| 339 | - | ||
| 340 | - | ctx := testutils.TestContext(t) | |
| 341 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 342 | - | ||
| 343 | - | cfg := &config.Config{ | |
| 344 | - | Volumes: config.VolumeConfig{ | |
| 345 | - | AutoCreate: true, | |
| 346 | - | Prefix: "gordon", | |
| 347 | - | }, | |
| 348 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 349 | - | NetworkPrefix: "gordon", | |
| 350 | - | }, | |
| 351 | - | } | |
| 352 | - | ||
| 353 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 354 | - | require.NoError(t, err) | |
| 355 | - | ||
| 356 | - | // Test service with volumes | |
| 357 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 358 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 359 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil) | |
| 360 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data"}, nil) | |
| 361 | - | ||
| 362 | - | // Volume creation expectations | |
| 363 | - | mockRuntime.EXPECT().VolumeExists(ctx, "gordon-api-example-com-var-lib-postgresql-data").Return(false, nil) | |
| 364 | - | mockRuntime.EXPECT().CreateVolume(ctx, "gordon-api-example-com-var-lib-postgresql-data").Return(nil) | |
| 365 | - | ||
| 366 | - | expectedContainer := &runtime.Container{ | |
| 367 | - | ID: "postgres-service", | |
| 368 | - | Name: "api-example-com-postgres", | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 372 | - | mockRuntime.EXPECT().StartContainer(ctx, "postgres-service").Return(nil) | |
| 373 | - | ||
| 374 | - | err = manager.DeployAttachedService(ctx, "api.example.com", "postgres:latest") | |
| 375 | - | ||
| 376 | - | assert.NoError(t, err) | |
| 377 | - | } | |
| 378 | - | ||
| 379 | - | func TestManager_DeployAttachedService_Errors(t *testing.T) { | |
| 380 | - | ctrl := gomock.NewController(t) | |
| 381 | - | defer ctrl.Finish() | |
| 382 | - | ||
| 383 | - | ctx := testutils.TestContext(t) | |
| 384 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 385 | - | ||
| 386 | - | cfg := &config.Config{ | |
| 387 | - | Volumes: config.VolumeConfig{ | |
| 388 | - | AutoCreate: false, | |
| 389 | - | }, | |
| 390 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 391 | - | NetworkPrefix: "gordon", | |
| 392 | - | }, | |
| 393 | - | } | |
| 394 | - | ||
| 395 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 396 | - | require.NoError(t, err) | |
| 397 | - | ||
| 398 | - | // Test create container error | |
| 399 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 400 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 401 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil) | |
| 402 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(nil, errors.New("create failed")) | |
| 403 | - | ||
| 404 | - | err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest") | |
| 405 | - | assert.Error(t, err) | |
| 406 | - | assert.Contains(t, err.Error(), "failed to create service container") | |
| 407 | - | ||
| 408 | - | // Test start container error | |
| 409 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil) | |
| 410 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil) | |
| 411 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil) | |
| 412 | - | ||
| 413 | - | container := &runtime.Container{ID: "service123", Name: "api-example-com-redis"} | |
| 414 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(container, nil) | |
| 415 | - | mockRuntime.EXPECT().StartContainer(ctx, "service123").Return(errors.New("start failed")) | |
| 416 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "service123", true).Return(nil) | |
| 417 | - | ||
| 418 | - | err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest") | |
| 419 | - | assert.Error(t, err) | |
| 420 | - | assert.Contains(t, err.Error(), "failed to start service container") | |
| 421 | - | }⏎̸ | |
D internal/container/manager_operations_test.go +0 -517
| @@ -1,517 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "os" | |
| 6 | - | "testing" | |
| 7 | - | ||
| 8 | - | "github.com/stretchr/testify/assert" | |
| 9 | - | "github.com/stretchr/testify/require" | |
| 10 | - | "go.uber.org/mock/gomock" | |
| 11 | - | ||
| 12 | - | "gordon/internal/config" | |
| 13 | - | "gordon/internal/testutils" | |
| 14 | - | "gordon/internal/testutils/mocks" | |
| 15 | - | "gordon/pkg/runtime" | |
| 16 | - | ) | |
| 17 | - | ||
| 18 | - | func TestManager_SyncContainers(t *testing.T) { | |
| 19 | - | ctrl := gomock.NewController(t) | |
| 20 | - | defer ctrl.Finish() | |
| 21 | - | ||
| 22 | - | ctx := testutils.TestContext(t) | |
| 23 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 24 | - | ||
| 25 | - | cfg := &config.Config{} | |
| 26 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 27 | - | require.NoError(t, err) | |
| 28 | - | ||
| 29 | - | // Add some containers to internal state that might be stale | |
| 30 | - | manager.containers["old.example.com"] = &runtime.Container{ID: "old-container"} | |
| 31 | - | ||
| 32 | - | // Mock runtime returning current containers | |
| 33 | - | currentContainers := []*runtime.Container{ | |
| 34 | - | { | |
| 35 | - | ID: "active-container-1", | |
| 36 | - | Name: "gordon-app1.example.com", | |
| 37 | - | Labels: map[string]string{ | |
| 38 | - | "gordon.managed": "true", | |
| 39 | - | "gordon.domain": "app1.example.com", | |
| 40 | - | }, | |
| 41 | - | }, | |
| 42 | - | { | |
| 43 | - | ID: "active-container-2", | |
| 44 | - | Name: "gordon-app2.example.com", | |
| 45 | - | Labels: map[string]string{ | |
| 46 | - | "gordon.managed": "true", | |
| 47 | - | "gordon.domain": "app2.example.com", | |
| 48 | - | }, | |
| 49 | - | }, | |
| 50 | - | { | |
| 51 | - | ID: "unmanaged-container", | |
| 52 | - | Name: "some-other-container", | |
| 53 | - | Labels: map[string]string{ | |
| 54 | - | "other.label": "value", | |
| 55 | - | }, | |
| 56 | - | }, | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return(currentContainers, nil) | |
| 60 | - | ||
| 61 | - | err = manager.SyncContainers(ctx) | |
| 62 | - | ||
| 63 | - | assert.NoError(t, err) | |
| 64 | - | assert.Len(t, manager.containers, 2) | |
| 65 | - | assert.Contains(t, manager.containers, "app1.example.com") | |
| 66 | - | assert.Contains(t, manager.containers, "app2.example.com") | |
| 67 | - | assert.NotContains(t, manager.containers, "old.example.com") | |
| 68 | - | assert.Equal(t, "active-container-1", manager.containers["app1.example.com"].ID) | |
| 69 | - | assert.Equal(t, "active-container-2", manager.containers["app2.example.com"].ID) | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | func TestManager_SyncContainers_Error(t *testing.T) { | |
| 73 | - | ctrl := gomock.NewController(t) | |
| 74 | - | defer ctrl.Finish() | |
| 75 | - | ||
| 76 | - | ctx := testutils.TestContext(t) | |
| 77 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 78 | - | ||
| 79 | - | cfg := &config.Config{} | |
| 80 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 81 | - | require.NoError(t, err) | |
| 82 | - | ||
| 83 | - | mockRuntime.EXPECT().ListContainers(ctx, false).Return(nil, errors.New("runtime error")) | |
| 84 | - | ||
| 85 | - | err = manager.SyncContainers(ctx) | |
| 86 | - | ||
| 87 | - | assert.Error(t, err) | |
| 88 | - | assert.Contains(t, err.Error(), "failed to list containers") | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | func TestManager_StopContainer(t *testing.T) { | |
| 92 | - | ctrl := gomock.NewController(t) | |
| 93 | - | defer ctrl.Finish() | |
| 94 | - | ||
| 95 | - | ctx := testutils.TestContext(t) | |
| 96 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 97 | - | ||
| 98 | - | cfg := &config.Config{} | |
| 99 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 100 | - | require.NoError(t, err) | |
| 101 | - | ||
| 102 | - | mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(nil) | |
| 103 | - | ||
| 104 | - | err = manager.StopContainer(ctx, "container123") | |
| 105 | - | ||
| 106 | - | assert.NoError(t, err) | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | func TestManager_StopContainer_Error(t *testing.T) { | |
| 110 | - | ctrl := gomock.NewController(t) | |
| 111 | - | defer ctrl.Finish() | |
| 112 | - | ||
| 113 | - | ctx := testutils.TestContext(t) | |
| 114 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 115 | - | ||
| 116 | - | cfg := &config.Config{} | |
| 117 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 118 | - | require.NoError(t, err) | |
| 119 | - | ||
| 120 | - | mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(errors.New("stop failed")) | |
| 121 | - | ||
| 122 | - | err = manager.StopContainer(ctx, "container123") | |
| 123 | - | ||
| 124 | - | assert.Error(t, err) | |
| 125 | - | assert.Contains(t, err.Error(), "failed to stop container") | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | func TestManager_StopContainerByDomain(t *testing.T) { | |
| 129 | - | ctrl := gomock.NewController(t) | |
| 130 | - | defer ctrl.Finish() | |
| 131 | - | ||
| 132 | - | ctx := testutils.TestContext(t) | |
| 133 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 134 | - | ||
| 135 | - | cfg := &config.Config{} | |
| 136 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 137 | - | require.NoError(t, err) | |
| 138 | - | ||
| 139 | - | container := &runtime.Container{ | |
| 140 | - | ID: "container123", | |
| 141 | - | } | |
| 142 | - | manager.containers["test.example.com"] = container | |
| 143 | - | ||
| 144 | - | mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(nil) | |
| 145 | - | ||
| 146 | - | err = manager.StopContainerByDomain(ctx, "test.example.com") | |
| 147 | - | ||
| 148 | - | assert.NoError(t, err) | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | func TestManager_StopContainerByDomain_NotFound(t *testing.T) { | |
| 152 | - | cfg := &config.Config{} | |
| 153 | - | manager := &Manager{ | |
| 154 | - | config: cfg, | |
| 155 | - | containers: make(map[string]*runtime.Container), | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | ctx := testutils.TestContext(t) | |
| 159 | - | err := manager.StopContainerByDomain(ctx, "nonexistent.example.com") | |
| 160 | - | ||
| 161 | - | assert.Error(t, err) | |
| 162 | - | assert.Contains(t, err.Error(), "no container found for domain") | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | func TestManager_RemoveContainer(t *testing.T) { | |
| 166 | - | ctrl := gomock.NewController(t) | |
| 167 | - | defer ctrl.Finish() | |
| 168 | - | ||
| 169 | - | ctx := testutils.TestContext(t) | |
| 170 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 171 | - | ||
| 172 | - | cfg := &config.Config{ | |
| 173 | - | Volumes: config.VolumeConfig{ | |
| 174 | - | Preserve: true, // Don't cleanup volumes | |
| 175 | - | }, | |
| 176 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 177 | - | Enabled: false, | |
| 178 | - | }, | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 182 | - | require.NoError(t, err) | |
| 183 | - | ||
| 184 | - | container := &runtime.Container{ | |
| 185 | - | ID: "container123", | |
| 186 | - | } | |
| 187 | - | manager.containers["test.example.com"] = container | |
| 188 | - | ||
| 189 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil) | |
| 190 | - | ||
| 191 | - | err = manager.RemoveContainer(ctx, "container123", true) | |
| 192 | - | ||
| 193 | - | assert.NoError(t, err) | |
| 194 | - | assert.NotContains(t, manager.containers, "test.example.com") | |
| 195 | - | } | |
| 196 | - | ||
| 197 | - | func TestManager_RemoveContainer_WithVolumeCleanup(t *testing.T) { | |
| 198 | - | ctrl := gomock.NewController(t) | |
| 199 | - | defer ctrl.Finish() | |
| 200 | - | ||
| 201 | - | ctx := testutils.TestContext(t) | |
| 202 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 203 | - | ||
| 204 | - | cfg := &config.Config{ | |
| 205 | - | Volumes: config.VolumeConfig{ | |
| 206 | - | Preserve: false, // Should cleanup volumes | |
| 207 | - | Prefix: "gordon", | |
| 208 | - | }, | |
| 209 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 210 | - | Enabled: false, | |
| 211 | - | }, | |
| 212 | - | Routes: map[string]string{ | |
| 213 | - | "test.example.com": "nginx:latest", | |
| 214 | - | }, | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 218 | - | require.NoError(t, err) | |
| 219 | - | ||
| 220 | - | container := &runtime.Container{ | |
| 221 | - | ID: "container123", | |
| 222 | - | } | |
| 223 | - | manager.containers["test.example.com"] = container | |
| 224 | - | ||
| 225 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil) | |
| 226 | - | ||
| 227 | - | // Expect volume cleanup | |
| 228 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil) | |
| 229 | - | varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx") | |
| 230 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil) | |
| 231 | - | mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(nil) | |
| 232 | - | ||
| 233 | - | err = manager.RemoveContainer(ctx, "container123", true) | |
| 234 | - | ||
| 235 | - | assert.NoError(t, err) | |
| 236 | - | assert.NotContains(t, manager.containers, "test.example.com") | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | func TestManager_RemoveContainer_WithNetworkCleanup(t *testing.T) { | |
| 240 | - | ctrl := gomock.NewController(t) | |
| 241 | - | defer ctrl.Finish() | |
| 242 | - | ||
| 243 | - | ctx := testutils.TestContext(t) | |
| 244 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 245 | - | ||
| 246 | - | cfg := &config.Config{ | |
| 247 | - | Volumes: config.VolumeConfig{ | |
| 248 | - | Preserve: true, | |
| 249 | - | }, | |
| 250 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 251 | - | Enabled: true, | |
| 252 | - | NetworkPrefix: "gordon", | |
| 253 | - | }, | |
| 254 | - | } | |
| 255 | - | ||
| 256 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 257 | - | require.NoError(t, err) | |
| 258 | - | ||
| 259 | - | container := &runtime.Container{ | |
| 260 | - | ID: "container123", | |
| 261 | - | } | |
| 262 | - | manager.containers["test.example.com"] = container | |
| 263 | - | ||
| 264 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil) | |
| 265 | - | ||
| 266 | - | // Expect network cleanup check | |
| 267 | - | mockRuntime.EXPECT().ListNetworks(ctx).Return([]*runtime.NetworkInfo{ | |
| 268 | - | { | |
| 269 | - | Name: "gordon-test-example-com", | |
| 270 | - | Containers: []string{}, // Empty network | |
| 271 | - | }, | |
| 272 | - | }, nil) | |
| 273 | - | mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test-example-com").Return(nil) | |
| 274 | - | ||
| 275 | - | err = manager.RemoveContainer(ctx, "container123", true) | |
| 276 | - | ||
| 277 | - | assert.NoError(t, err) | |
| 278 | - | assert.NotContains(t, manager.containers, "test.example.com") | |
| 279 | - | } | |
| 280 | - | ||
| 281 | - | func TestManager_RemoveContainerByDomain(t *testing.T) { | |
| 282 | - | ctrl := gomock.NewController(t) | |
| 283 | - | defer ctrl.Finish() | |
| 284 | - | ||
| 285 | - | ctx := testutils.TestContext(t) | |
| 286 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 287 | - | ||
| 288 | - | cfg := &config.Config{ | |
| 289 | - | Volumes: config.VolumeConfig{ | |
| 290 | - | Preserve: true, | |
| 291 | - | }, | |
| 292 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 293 | - | Enabled: false, | |
| 294 | - | }, | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 298 | - | require.NoError(t, err) | |
| 299 | - | ||
| 300 | - | container := &runtime.Container{ | |
| 301 | - | ID: "container123", | |
| 302 | - | } | |
| 303 | - | manager.containers["test.example.com"] = container | |
| 304 | - | ||
| 305 | - | mockRuntime.EXPECT().RemoveContainer(ctx, "container123", false).Return(nil) | |
| 306 | - | ||
| 307 | - | err = manager.RemoveContainerByDomain(ctx, "test.example.com", false) | |
| 308 | - | ||
| 309 | - | assert.NoError(t, err) | |
| 310 | - | assert.NotContains(t, manager.containers, "test.example.com") | |
| 311 | - | } | |
| 312 | - | ||
| 313 | - | func TestManager_RemoveContainerByDomain_NotFound(t *testing.T) { | |
| 314 | - | cfg := &config.Config{} | |
| 315 | - | manager := &Manager{ | |
| 316 | - | config: cfg, | |
| 317 | - | containers: make(map[string]*runtime.Container), | |
| 318 | - | } | |
| 319 | - | ||
| 320 | - | ctx := testutils.TestContext(t) | |
| 321 | - | err := manager.RemoveContainerByDomain(ctx, "nonexistent.example.com", false) | |
| 322 | - | ||
| 323 | - | assert.Error(t, err) | |
| 324 | - | assert.Contains(t, err.Error(), "no container found for domain") | |
| 325 | - | } | |
| 326 | - | ||
| 327 | - | func TestManager_GetContainerPort(t *testing.T) { | |
| 328 | - | ctrl := gomock.NewController(t) | |
| 329 | - | defer ctrl.Finish() | |
| 330 | - | ||
| 331 | - | ctx := testutils.TestContext(t) | |
| 332 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 333 | - | ||
| 334 | - | cfg := &config.Config{} | |
| 335 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 336 | - | require.NoError(t, err) | |
| 337 | - | ||
| 338 | - | container := &runtime.Container{ | |
| 339 | - | ID: "container123", | |
| 340 | - | } | |
| 341 | - | manager.containers["test.example.com"] = container | |
| 342 | - | ||
| 343 | - | mockRuntime.EXPECT().GetContainerPort(ctx, "container123", 80).Return(8080, nil) | |
| 344 | - | ||
| 345 | - | port, err := manager.GetContainerPort(ctx, "test.example.com", 80) | |
| 346 | - | ||
| 347 | - | assert.NoError(t, err) | |
| 348 | - | assert.Equal(t, 8080, port) | |
| 349 | - | } | |
| 350 | - | ||
| 351 | - | func TestManager_GetContainerPort_NotFound(t *testing.T) { | |
| 352 | - | cfg := &config.Config{} | |
| 353 | - | manager := &Manager{ | |
| 354 | - | config: cfg, | |
| 355 | - | containers: make(map[string]*runtime.Container), | |
| 356 | - | } | |
| 357 | - | ||
| 358 | - | ctx := testutils.TestContext(t) | |
| 359 | - | _, err := manager.GetContainerPort(ctx, "nonexistent.example.com", 80) | |
| 360 | - | ||
| 361 | - | assert.Error(t, err) | |
| 362 | - | assert.Contains(t, err.Error(), "no container found for domain") | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | func TestManager_FindContainerByDomain(t *testing.T) { | |
| 366 | - | manager := &Manager{ | |
| 367 | - | containers: map[string]*runtime.Container{ | |
| 368 | - | "test.example.com": {ID: "container123"}, | |
| 369 | - | }, | |
| 370 | - | } | |
| 371 | - | ||
| 372 | - | id, exists := manager.FindContainerByDomain("test.example.com") | |
| 373 | - | assert.True(t, exists) | |
| 374 | - | assert.Equal(t, "container123", id) | |
| 375 | - | ||
| 376 | - | id, exists = manager.FindContainerByDomain("nonexistent.example.com") | |
| 377 | - | assert.False(t, exists) | |
| 378 | - | assert.Empty(t, id) | |
| 379 | - | } | |
| 380 | - | ||
| 381 | - | func TestManager_FindDomainByContainerID(t *testing.T) { | |
| 382 | - | manager := &Manager{ | |
| 383 | - | containers: map[string]*runtime.Container{ | |
| 384 | - | "test.example.com": {ID: "container123"}, | |
| 385 | - | "app.example.com": {ID: "container456"}, | |
| 386 | - | }, | |
| 387 | - | } | |
| 388 | - | ||
| 389 | - | domain, exists := manager.FindDomainByContainerID("container123") | |
| 390 | - | assert.True(t, exists) | |
| 391 | - | assert.Equal(t, "test.example.com", domain) | |
| 392 | - | ||
| 393 | - | domain, exists = manager.FindDomainByContainerID("nonexistent") | |
| 394 | - | assert.False(t, exists) | |
| 395 | - | assert.Empty(t, domain) | |
| 396 | - | } | |
| 397 | - | ||
| 398 | - | func TestManager_StopAllManagedContainers(t *testing.T) { | |
| 399 | - | ctrl := gomock.NewController(t) | |
| 400 | - | defer ctrl.Finish() | |
| 401 | - | ||
| 402 | - | ctx := testutils.TestContext(t) | |
| 403 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 404 | - | ||
| 405 | - | cfg := &config.Config{} | |
| 406 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 407 | - | require.NoError(t, err) | |
| 408 | - | ||
| 409 | - | manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"} | |
| 410 | - | manager.containers["app2.example.com"] = &runtime.Container{ID: "container2"} | |
| 411 | - | ||
| 412 | - | mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil) | |
| 413 | - | mockRuntime.EXPECT().StopContainer(ctx, "container2").Return(nil) | |
| 414 | - | ||
| 415 | - | err = manager.StopAllManagedContainers(ctx) | |
| 416 | - | ||
| 417 | - | assert.NoError(t, err) | |
| 418 | - | assert.Empty(t, manager.containers) | |
| 419 | - | } | |
| 420 | - | ||
| 421 | - | func TestManager_StopAllManagedContainers_WithErrors(t *testing.T) { | |
| 422 | - | ctrl := gomock.NewController(t) | |
| 423 | - | defer ctrl.Finish() | |
| 424 | - | ||
| 425 | - | ctx := testutils.TestContext(t) | |
| 426 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 427 | - | ||
| 428 | - | cfg := &config.Config{} | |
| 429 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 430 | - | require.NoError(t, err) | |
| 431 | - | ||
| 432 | - | manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"} | |
| 433 | - | manager.containers["app2.example.com"] = &runtime.Container{ID: "container2"} | |
| 434 | - | ||
| 435 | - | mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil) | |
| 436 | - | mockRuntime.EXPECT().StopContainer(ctx, "container2").Return(errors.New("stop failed")) | |
| 437 | - | ||
| 438 | - | err = manager.StopAllManagedContainers(ctx) | |
| 439 | - | ||
| 440 | - | assert.Error(t, err) | |
| 441 | - | assert.Contains(t, err.Error(), "failed to stop 1 containers") | |
| 442 | - | } | |
| 443 | - | ||
| 444 | - | func TestManager_StopAllManagedContainers_Empty(t *testing.T) { | |
| 445 | - | ctrl := gomock.NewController(t) | |
| 446 | - | defer ctrl.Finish() | |
| 447 | - | ||
| 448 | - | ctx := testutils.TestContext(t) | |
| 449 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 450 | - | ||
| 451 | - | cfg := &config.Config{} | |
| 452 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 453 | - | require.NoError(t, err) | |
| 454 | - | ||
| 455 | - | err = manager.StopAllManagedContainers(ctx) | |
| 456 | - | ||
| 457 | - | assert.NoError(t, err) | |
| 458 | - | assert.Empty(t, manager.containers) | |
| 459 | - | } | |
| 460 | - | ||
| 461 | - | func TestManager_UpdateConfig(t *testing.T) { | |
| 462 | - | ctrl := gomock.NewController(t) | |
| 463 | - | defer ctrl.Finish() | |
| 464 | - | ||
| 465 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 466 | - | ||
| 467 | - | originalCfg := &config.Config{ | |
| 468 | - | Server: config.ServerConfig{ | |
| 469 | - | Runtime: "docker", | |
| 470 | - | }, | |
| 471 | - | } | |
| 472 | - | ||
| 473 | - | manager, err := NewManagerWithRuntime(originalCfg, mockRuntime) | |
| 474 | - | require.NoError(t, err) | |
| 475 | - | ||
| 476 | - | newCfg := &config.Config{ | |
| 477 | - | Server: config.ServerConfig{ | |
| 478 | - | Runtime: "podman", | |
| 479 | - | }, | |
| 480 | - | Routes: map[string]string{ | |
| 481 | - | "new-app.example.com": "nginx:latest", | |
| 482 | - | }, | |
| 483 | - | } | |
| 484 | - | ||
| 485 | - | manager.UpdateConfig(newCfg) | |
| 486 | - | ||
| 487 | - | // Cleanup the env file created by UpdateConfig | |
| 488 | - | defer func() { | |
| 489 | - | if err := os.Remove("new-app_example_com.env"); err != nil && !os.IsNotExist(err) { | |
| 490 | - | t.Logf("Failed to cleanup env file: %v", err) | |
| 491 | - | } | |
| 492 | - | }() | |
| 493 | - | ||
| 494 | - | assert.Equal(t, newCfg, manager.config) | |
| 495 | - | assert.Equal(t, "podman", manager.config.Server.Runtime) | |
| 496 | - | } | |
| 497 | - | ||
| 498 | - | func TestManager_Shutdown(t *testing.T) { | |
| 499 | - | ctrl := gomock.NewController(t) | |
| 500 | - | defer ctrl.Finish() | |
| 501 | - | ||
| 502 | - | ctx := testutils.TestContext(t) | |
| 503 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 504 | - | ||
| 505 | - | cfg := &config.Config{} | |
| 506 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 507 | - | require.NoError(t, err) | |
| 508 | - | ||
| 509 | - | manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"} | |
| 510 | - | ||
| 511 | - | mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil) | |
| 512 | - | ||
| 513 | - | err = manager.Shutdown(ctx) | |
| 514 | - | ||
| 515 | - | assert.NoError(t, err) | |
| 516 | - | assert.Empty(t, manager.containers) | |
| 517 | - | }⏎̸ | |
D internal/container/manager_test.go +0 -183
| @@ -1,183 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | ||
| 6 | - | "github.com/stretchr/testify/assert" | |
| 7 | - | "github.com/stretchr/testify/require" | |
| 8 | - | "go.uber.org/mock/gomock" | |
| 9 | - | ||
| 10 | - | "gordon/internal/config" | |
| 11 | - | "gordon/internal/testutils" | |
| 12 | - | "gordon/internal/testutils/mocks" | |
| 13 | - | "gordon/pkg/runtime" | |
| 14 | - | ) | |
| 15 | - | ||
| 16 | - | func TestManager_GetContainer(t *testing.T) { | |
| 17 | - | container := &runtime.Container{ | |
| 18 | - | ID: "test123", | |
| 19 | - | Name: "test-container", | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | manager := &Manager{ | |
| 23 | - | containers: map[string]*runtime.Container{ | |
| 24 | - | "test.example.com": container, | |
| 25 | - | }, | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // Test existing container | |
| 29 | - | found, exists := manager.GetContainer("test.example.com") | |
| 30 | - | assert.True(t, exists) | |
| 31 | - | assert.Equal(t, container, found) | |
| 32 | - | ||
| 33 | - | // Test non-existent container | |
| 34 | - | notFound, exists := manager.GetContainer("nonexistent.example.com") | |
| 35 | - | assert.False(t, exists) | |
| 36 | - | assert.Nil(t, notFound) | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | func TestManager_ListContainers(t *testing.T) { | |
| 40 | - | container1 := &runtime.Container{ID: "test1", Name: "container1"} | |
| 41 | - | container2 := &runtime.Container{ID: "test2", Name: "container2"} | |
| 42 | - | ||
| 43 | - | manager := &Manager{ | |
| 44 | - | containers: map[string]*runtime.Container{ | |
| 45 | - | "app1.example.com": container1, | |
| 46 | - | "app2.example.com": container2, | |
| 47 | - | }, | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | containers := manager.ListContainers() | |
| 51 | - | ||
| 52 | - | assert.Len(t, containers, 2) | |
| 53 | - | assert.Contains(t, containers, "app1.example.com") | |
| 54 | - | assert.Contains(t, containers, "app2.example.com") | |
| 55 | - | assert.Equal(t, container1, containers["app1.example.com"]) | |
| 56 | - | assert.Equal(t, container2, containers["app2.example.com"]) | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | func TestManager_HealthCheck(t *testing.T) { | |
| 60 | - | ctrl := gomock.NewController(t) | |
| 61 | - | defer ctrl.Finish() | |
| 62 | - | ||
| 63 | - | ctx := testutils.TestContext(t) | |
| 64 | - | ||
| 65 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 66 | - | mockRuntime.EXPECT().IsContainerRunning(ctx, "container1").Return(true, nil) | |
| 67 | - | mockRuntime.EXPECT().IsContainerRunning(ctx, "container2").Return(false, nil) | |
| 68 | - | ||
| 69 | - | manager := &Manager{ | |
| 70 | - | runtime: mockRuntime, | |
| 71 | - | containers: map[string]*runtime.Container{ | |
| 72 | - | "app1.example.com": {ID: "container1"}, | |
| 73 | - | "app2.example.com": {ID: "container2"}, | |
| 74 | - | }, | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | health := manager.HealthCheck(ctx) | |
| 78 | - | ||
| 79 | - | assert.Len(t, health, 2) | |
| 80 | - | assert.True(t, health["app1.example.com"]) | |
| 81 | - | assert.False(t, health["app2.example.com"]) | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | func TestManager_GetNetworkForApp(t *testing.T) { | |
| 85 | - | manager := &Manager{ | |
| 86 | - | config: &config.Config{ | |
| 87 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 88 | - | Enabled: true, | |
| 89 | - | NetworkPrefix: "gordon", | |
| 90 | - | }, | |
| 91 | - | NetworkGroups: map[string][]string{ | |
| 92 | - | "backend": {"api.example.com", "worker.example.com"}, | |
| 93 | - | }, | |
| 94 | - | }, | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | // Test app in network group | |
| 98 | - | network := manager.GetNetworkForApp("api.example.com") | |
| 99 | - | assert.Equal(t, "gordon-backend", network) | |
| 100 | - | ||
| 101 | - | // Test standalone app | |
| 102 | - | network = manager.GetNetworkForApp("frontend.example.com") | |
| 103 | - | assert.Equal(t, "gordon-frontend-example-com", network) | |
| 104 | - | ||
| 105 | - | // Test disabled network isolation | |
| 106 | - | manager.config.NetworkIsolation.Enabled = false | |
| 107 | - | network = manager.GetNetworkForApp("test.example.com") | |
| 108 | - | assert.Equal(t, "bridge", network) | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | func TestManager_Runtime(t *testing.T) { | |
| 112 | - | ctrl := gomock.NewController(t) | |
| 113 | - | defer ctrl.Finish() | |
| 114 | - | ||
| 115 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 116 | - | manager := &Manager{ | |
| 117 | - | runtime: mockRuntime, | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | result := manager.Runtime() | |
| 121 | - | assert.Equal(t, mockRuntime, result) | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | func TestManager_DeployContainer(t *testing.T) { | |
| 125 | - | ctrl := gomock.NewController(t) | |
| 126 | - | defer ctrl.Finish() | |
| 127 | - | ||
| 128 | - | ctx := testutils.TestContext(t) | |
| 129 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 130 | - | ||
| 131 | - | cfg := &config.Config{ | |
| 132 | - | Server: config.ServerConfig{ | |
| 133 | - | Runtime: "docker", | |
| 134 | - | }, | |
| 135 | - | Volumes: config.VolumeConfig{ | |
| 136 | - | AutoCreate: true, | |
| 137 | - | Prefix: "gordon", | |
| 138 | - | Preserve: false, | |
| 139 | - | }, | |
| 140 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 141 | - | Enabled: true, | |
| 142 | - | NetworkPrefix: "gordon", | |
| 143 | - | }, | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 147 | - | require.NoError(t, err) | |
| 148 | - | ||
| 149 | - | route := config.Route{ | |
| 150 | - | Domain: "test.example.com", | |
| 151 | - | Image: "nginx:latest", | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // Set up expectations for successful deployment | |
| 155 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 156 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"nginx:latest"}, nil) | |
| 157 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "nginx:latest").Return([]int{80}, nil) | |
| 158 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "nginx:latest").Return([]string{"PATH=/usr/local/sbin:/usr/local/bin"}, nil) | |
| 159 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil) | |
| 160 | - | varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx") | |
| 161 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(false, nil) | |
| 162 | - | mockRuntime.EXPECT().CreateVolume(ctx, varLogNginxVolume).Return(nil) | |
| 163 | - | mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test-example-com").Return(false, nil) | |
| 164 | - | mockRuntime.EXPECT().CreateNetwork(ctx, "gordon-test-example-com", gomock.Any()).Return(nil) | |
| 165 | - | ||
| 166 | - | expectedContainer := &runtime.Container{ | |
| 167 | - | ID: "container123", | |
| 168 | - | Name: "gordon-test.example.com", | |
| 169 | - | Image: "nginx:latest", | |
| 170 | - | Status: "running", | |
| 171 | - | Ports: []int{80}, | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 175 | - | mockRuntime.EXPECT().StartContainer(ctx, "container123").Return(nil) | |
| 176 | - | mockRuntime.EXPECT().InspectContainer(ctx, "container123").Return(expectedContainer, nil) | |
| 177 | - | ||
| 178 | - | container, err := manager.DeployContainer(ctx, route) | |
| 179 | - | ||
| 180 | - | assert.NoError(t, err) | |
| 181 | - | assert.Equal(t, expectedContainer, container) | |
| 182 | - | assert.Equal(t, expectedContainer, manager.containers["test.example.com"]) | |
| 183 | - | } | |
D internal/container/manager_volume_test.go +0 -388
| @@ -1,388 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "errors" | |
| 5 | - | "testing" | |
| 6 | - | ||
| 7 | - | "github.com/stretchr/testify/assert" | |
| 8 | - | "github.com/stretchr/testify/require" | |
| 9 | - | "go.uber.org/mock/gomock" | |
| 10 | - | ||
| 11 | - | "gordon/internal/config" | |
| 12 | - | "gordon/internal/testutils" | |
| 13 | - | "gordon/internal/testutils/mocks" | |
| 14 | - | "gordon/pkg/runtime" | |
| 15 | - | ) | |
| 16 | - | ||
| 17 | - | func TestManager_cleanupVolumesForDomain(t *testing.T) { | |
| 18 | - | ctrl := gomock.NewController(t) | |
| 19 | - | defer ctrl.Finish() | |
| 20 | - | ||
| 21 | - | ctx := testutils.TestContext(t) | |
| 22 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 23 | - | ||
| 24 | - | cfg := &config.Config{ | |
| 25 | - | Volumes: config.VolumeConfig{ | |
| 26 | - | Prefix: "gordon", | |
| 27 | - | }, | |
| 28 | - | Routes: map[string]string{ | |
| 29 | - | "test.example.com": "nginx:latest", | |
| 30 | - | }, | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 34 | - | require.NoError(t, err) | |
| 35 | - | ||
| 36 | - | // Mock image inspection for volume paths | |
| 37 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx", "/etc/nginx"}, nil) | |
| 38 | - | ||
| 39 | - | // Mock volume existence checks and removal | |
| 40 | - | varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx") | |
| 41 | - | etcNginxVolume := generateVolumeName("gordon", "test.example.com", "/etc/nginx") | |
| 42 | - | ||
| 43 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil) | |
| 44 | - | mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(nil) | |
| 45 | - | ||
| 46 | - | mockRuntime.EXPECT().VolumeExists(ctx, etcNginxVolume).Return(true, nil) | |
| 47 | - | mockRuntime.EXPECT().RemoveVolume(ctx, etcNginxVolume, true).Return(nil) | |
| 48 | - | ||
| 49 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 50 | - | ||
| 51 | - | assert.NoError(t, err) | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | func TestManager_cleanupVolumesForDomain_NoRoute(t *testing.T) { | |
| 55 | - | ctrl := gomock.NewController(t) | |
| 56 | - | defer ctrl.Finish() | |
| 57 | - | ||
| 58 | - | ctx := testutils.TestContext(t) | |
| 59 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 60 | - | ||
| 61 | - | cfg := &config.Config{ | |
| 62 | - | Volumes: config.VolumeConfig{ | |
| 63 | - | Prefix: "gordon", | |
| 64 | - | }, | |
| 65 | - | Routes: map[string]string{}, | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 69 | - | require.NoError(t, err) | |
| 70 | - | ||
| 71 | - | // Should return nil without doing anything | |
| 72 | - | err = manager.cleanupVolumesForDomain(ctx, "nonexistent.example.com") | |
| 73 | - | ||
| 74 | - | assert.NoError(t, err) | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | func TestManager_cleanupVolumesForDomain_WithRegistryAuth(t *testing.T) { | |
| 78 | - | ctrl := gomock.NewController(t) | |
| 79 | - | defer ctrl.Finish() | |
| 80 | - | ||
| 81 | - | ctx := testutils.TestContext(t) | |
| 82 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 83 | - | ||
| 84 | - | cfg := &config.Config{ | |
| 85 | - | Server: config.ServerConfig{ | |
| 86 | - | RegistryDomain: "registry.example.com", | |
| 87 | - | }, | |
| 88 | - | RegistryAuth: config.RegistryAuthConfig{ | |
| 89 | - | Enabled: true, | |
| 90 | - | }, | |
| 91 | - | Volumes: config.VolumeConfig{ | |
| 92 | - | Prefix: "gordon", | |
| 93 | - | }, | |
| 94 | - | Routes: map[string]string{ | |
| 95 | - | "test.example.com": "myapp:latest", | |
| 96 | - | }, | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 100 | - | require.NoError(t, err) | |
| 101 | - | ||
| 102 | - | // Should use prefixed image reference | |
| 103 | - | expectedImageRef := "registry.example.com/myapp:latest" | |
| 104 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, expectedImageRef).Return([]string{"/data"}, nil) | |
| 105 | - | ||
| 106 | - | dataVolume := generateVolumeName("gordon", "test.example.com", "/data") | |
| 107 | - | mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(true, nil) | |
| 108 | - | mockRuntime.EXPECT().RemoveVolume(ctx, dataVolume, true).Return(nil) | |
| 109 | - | ||
| 110 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 111 | - | ||
| 112 | - | assert.NoError(t, err) | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | func TestManager_cleanupVolumesForDomain_NoVolumes(t *testing.T) { | |
| 116 | - | ctrl := gomock.NewController(t) | |
| 117 | - | defer ctrl.Finish() | |
| 118 | - | ||
| 119 | - | ctx := testutils.TestContext(t) | |
| 120 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 121 | - | ||
| 122 | - | cfg := &config.Config{ | |
| 123 | - | Volumes: config.VolumeConfig{ | |
| 124 | - | Prefix: "gordon", | |
| 125 | - | }, | |
| 126 | - | Routes: map[string]string{ | |
| 127 | - | "test.example.com": "alpine:latest", | |
| 128 | - | }, | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 132 | - | require.NoError(t, err) | |
| 133 | - | ||
| 134 | - | // Image has no volumes | |
| 135 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "alpine:latest").Return([]string{}, nil) | |
| 136 | - | ||
| 137 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 138 | - | ||
| 139 | - | assert.NoError(t, err) | |
| 140 | - | } | |
| 141 | - | ||
| 142 | - | func TestManager_cleanupVolumesForDomain_VolumeNotExists(t *testing.T) { | |
| 143 | - | ctrl := gomock.NewController(t) | |
| 144 | - | defer ctrl.Finish() | |
| 145 | - | ||
| 146 | - | ctx := testutils.TestContext(t) | |
| 147 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 148 | - | ||
| 149 | - | cfg := &config.Config{ | |
| 150 | - | Volumes: config.VolumeConfig{ | |
| 151 | - | Prefix: "gordon", | |
| 152 | - | }, | |
| 153 | - | Routes: map[string]string{ | |
| 154 | - | "test.example.com": "nginx:latest", | |
| 155 | - | }, | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 159 | - | require.NoError(t, err) | |
| 160 | - | ||
| 161 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil) | |
| 162 | - | ||
| 163 | - | // Volume doesn't exist, should skip removal | |
| 164 | - | varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx") | |
| 165 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(false, nil) | |
| 166 | - | ||
| 167 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 168 | - | ||
| 169 | - | assert.NoError(t, err) | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | func TestManager_cleanupVolumesForDomain_Errors(t *testing.T) { | |
| 173 | - | ctrl := gomock.NewController(t) | |
| 174 | - | defer ctrl.Finish() | |
| 175 | - | ||
| 176 | - | ctx := testutils.TestContext(t) | |
| 177 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 178 | - | ||
| 179 | - | cfg := &config.Config{ | |
| 180 | - | Volumes: config.VolumeConfig{ | |
| 181 | - | Prefix: "gordon", | |
| 182 | - | }, | |
| 183 | - | Routes: map[string]string{ | |
| 184 | - | "test.example.com": "nginx:latest", | |
| 185 | - | }, | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 189 | - | require.NoError(t, err) | |
| 190 | - | ||
| 191 | - | // Test error inspecting image volumes | |
| 192 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return(nil, errors.New("inspect failed")) | |
| 193 | - | ||
| 194 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 195 | - | assert.Error(t, err) | |
| 196 | - | ||
| 197 | - | // Test error checking volume existence | |
| 198 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil) | |
| 199 | - | varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx") | |
| 200 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(false, errors.New("check failed")) | |
| 201 | - | ||
| 202 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 203 | - | assert.NoError(t, err) // Should continue despite check error | |
| 204 | - | ||
| 205 | - | // Test error removing volume | |
| 206 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil) | |
| 207 | - | mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil) | |
| 208 | - | mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(errors.New("remove failed")) | |
| 209 | - | ||
| 210 | - | err = manager.cleanupVolumesForDomain(ctx, "test.example.com") | |
| 211 | - | assert.Error(t, err) | |
| 212 | - | assert.Contains(t, err.Error(), "failed to cleanup 1 volumes") | |
| 213 | - | } | |
| 214 | - | ||
| 215 | - | func TestManager_DeployContainer_VolumeAutoCreation(t *testing.T) { | |
| 216 | - | ctrl := gomock.NewController(t) | |
| 217 | - | defer ctrl.Finish() | |
| 218 | - | ||
| 219 | - | ctx := testutils.TestContext(t) | |
| 220 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 221 | - | ||
| 222 | - | cfg := &config.Config{ | |
| 223 | - | Server: config.ServerConfig{ | |
| 224 | - | Runtime: "docker", | |
| 225 | - | }, | |
| 226 | - | Volumes: config.VolumeConfig{ | |
| 227 | - | AutoCreate: true, | |
| 228 | - | Prefix: "gordon", | |
| 229 | - | Preserve: false, | |
| 230 | - | }, | |
| 231 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 232 | - | Enabled: false, | |
| 233 | - | }, | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 237 | - | require.NoError(t, err) | |
| 238 | - | ||
| 239 | - | route := config.Route{ | |
| 240 | - | Domain: "test.example.com", | |
| 241 | - | Image: "postgres:latest", | |
| 242 | - | } | |
| 243 | - | ||
| 244 | - | // Set up expectations for deployment with volume creation | |
| 245 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 246 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil) | |
| 247 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 248 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil) | |
| 249 | - | ||
| 250 | - | // Mock volume discovery and creation | |
| 251 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data", "/etc/postgresql"}, nil) | |
| 252 | - | ||
| 253 | - | // First volume | |
| 254 | - | dataVolume := generateVolumeName("gordon", "test.example.com", "/var/lib/postgresql/data") | |
| 255 | - | mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(false, nil) | |
| 256 | - | mockRuntime.EXPECT().CreateVolume(ctx, dataVolume).Return(nil) | |
| 257 | - | ||
| 258 | - | // Second volume already exists | |
| 259 | - | etcVolume := generateVolumeName("gordon", "test.example.com", "/etc/postgresql") | |
| 260 | - | mockRuntime.EXPECT().VolumeExists(ctx, etcVolume).Return(true, nil) | |
| 261 | - | ||
| 262 | - | expectedContainer := &runtime.Container{ | |
| 263 | - | ID: "postgres-container", | |
| 264 | - | Name: "gordon-test.example.com", | |
| 265 | - | Image: "postgres:latest", | |
| 266 | - | Status: "running", | |
| 267 | - | Ports: []int{5432}, | |
| 268 | - | } | |
| 269 | - | ||
| 270 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 271 | - | mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil) | |
| 272 | - | mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil) | |
| 273 | - | ||
| 274 | - | container, err := manager.DeployContainer(ctx, route) | |
| 275 | - | ||
| 276 | - | assert.NoError(t, err) | |
| 277 | - | assert.Equal(t, expectedContainer, container) | |
| 278 | - | } | |
| 279 | - | ||
| 280 | - | func TestManager_DeployContainer_VolumeCreationError(t *testing.T) { | |
| 281 | - | ctrl := gomock.NewController(t) | |
| 282 | - | defer ctrl.Finish() | |
| 283 | - | ||
| 284 | - | ctx := testutils.TestContext(t) | |
| 285 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 286 | - | ||
| 287 | - | cfg := &config.Config{ | |
| 288 | - | Server: config.ServerConfig{ | |
| 289 | - | Runtime: "docker", | |
| 290 | - | }, | |
| 291 | - | Volumes: config.VolumeConfig{ | |
| 292 | - | AutoCreate: true, | |
| 293 | - | Prefix: "gordon", | |
| 294 | - | }, | |
| 295 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 296 | - | Enabled: false, | |
| 297 | - | }, | |
| 298 | - | } | |
| 299 | - | ||
| 300 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 301 | - | require.NoError(t, err) | |
| 302 | - | ||
| 303 | - | route := config.Route{ | |
| 304 | - | Domain: "test.example.com", | |
| 305 | - | Image: "postgres:latest", | |
| 306 | - | } | |
| 307 | - | ||
| 308 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 309 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil) | |
| 310 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 311 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil) | |
| 312 | - | ||
| 313 | - | // Mock volume creation failure | |
| 314 | - | mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data"}, nil) | |
| 315 | - | dataVolume := generateVolumeName("gordon", "test.example.com", "/var/lib/postgresql/data") | |
| 316 | - | mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(false, nil) | |
| 317 | - | mockRuntime.EXPECT().CreateVolume(ctx, dataVolume).Return(errors.New("create failed")) | |
| 318 | - | ||
| 319 | - | // Should still continue with container creation (volume creation is best-effort) | |
| 320 | - | expectedContainer := &runtime.Container{ | |
| 321 | - | ID: "postgres-container", | |
| 322 | - | Name: "gordon-test.example.com", | |
| 323 | - | Image: "postgres:latest", | |
| 324 | - | Status: "running", | |
| 325 | - | Ports: []int{5432}, | |
| 326 | - | } | |
| 327 | - | ||
| 328 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 329 | - | mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil) | |
| 330 | - | mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil) | |
| 331 | - | ||
| 332 | - | container, err := manager.DeployContainer(ctx, route) | |
| 333 | - | ||
| 334 | - | assert.NoError(t, err) | |
| 335 | - | assert.Equal(t, expectedContainer, container) | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | func TestManager_DeployContainer_VolumeAutoCreationDisabled(t *testing.T) { | |
| 339 | - | ctrl := gomock.NewController(t) | |
| 340 | - | defer ctrl.Finish() | |
| 341 | - | ||
| 342 | - | ctx := testutils.TestContext(t) | |
| 343 | - | mockRuntime := mocks.NewMockRuntime(ctrl) | |
| 344 | - | ||
| 345 | - | cfg := &config.Config{ | |
| 346 | - | Server: config.ServerConfig{ | |
| 347 | - | Runtime: "docker", | |
| 348 | - | }, | |
| 349 | - | Volumes: config.VolumeConfig{ | |
| 350 | - | AutoCreate: false, // Disabled | |
| 351 | - | }, | |
| 352 | - | NetworkIsolation: config.NetworkIsolationConfig{ | |
| 353 | - | Enabled: false, | |
| 354 | - | }, | |
| 355 | - | } | |
| 356 | - | ||
| 357 | - | manager, err := NewManagerWithRuntime(cfg, mockRuntime) | |
| 358 | - | require.NoError(t, err) | |
| 359 | - | ||
| 360 | - | route := config.Route{ | |
| 361 | - | Domain: "test.example.com", | |
| 362 | - | Image: "postgres:latest", | |
| 363 | - | } | |
| 364 | - | ||
| 365 | - | mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil) | |
| 366 | - | mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil) | |
| 367 | - | mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil) | |
| 368 | - | mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil) | |
| 369 | - | ||
| 370 | - | // Should NOT inspect image volumes when auto-creation is disabled | |
| 371 | - | ||
| 372 | - | expectedContainer := &runtime.Container{ | |
| 373 | - | ID: "postgres-container", | |
| 374 | - | Name: "gordon-test.example.com", | |
| 375 | - | Image: "postgres:latest", | |
| 376 | - | Status: "running", | |
| 377 | - | Ports: []int{5432}, | |
| 378 | - | } | |
| 379 | - | ||
| 380 | - | mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil) | |
| 381 | - | mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil) | |
| 382 | - | mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil) | |
| 383 | - | ||
| 384 | - | container, err := manager.DeployContainer(ctx, route) | |
| 385 | - | ||
| 386 | - | assert.NoError(t, err) | |
| 387 | - | assert.Equal(t, expectedContainer, container) | |
| 388 | - | }⏎̸ | |
D internal/container/test_manager.go +0 -35
| @@ -1,35 +0,0 @@ | |||
| 1 | - | package container | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "gordon/internal/config" | |
| 5 | - | "gordon/internal/env" | |
| 6 | - | "gordon/internal/logging" | |
| 7 | - | "gordon/pkg/runtime" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // NewManagerWithRuntime creates a manager with an injected runtime for testing | |
| 11 | - | func NewManagerWithRuntime(cfg *config.Config, rt runtime.Runtime) (*Manager, error) { | |
| 12 | - | // Create environment loader | |
| 13 | - | envLoader := env.NewLoader(cfg) | |
| 14 | - | ||
| 15 | - | // Register secret providers based on config | |
| 16 | - | for _, providerName := range cfg.Env.Providers { | |
| 17 | - | switch providerName { | |
| 18 | - | case "pass": | |
| 19 | - | envLoader.RegisterSecretProvider("pass", env.NewPassProvider()) | |
| 20 | - | case "sops": | |
| 21 | - | envLoader.RegisterSecretProvider("sops", env.NewSopsProvider()) | |
| 22 | - | } | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | // Create log manager | |
| 26 | - | logManager := logging.NewLogManager(cfg, rt) | |
| 27 | - | ||
| 28 | - | return &Manager{ | |
| 29 | - | runtime: rt, | |
| 30 | - | config: cfg, | |
| 31 | - | containers: make(map[string]*runtime.Container), | |
| 32 | - | envLoader: envLoader, | |
| 33 | - | logManager: logManager, | |
| 34 | - | }, nil | |
| 35 | - | }⏎̸ | |
A internal/domain/container.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | // Package domain contains pure business types without external dependencies. | |
| 2 | + | // These types are used throughout the application and have no tags or framework dependencies. | |
| 3 | + | package domain | |
| 4 | + | ||
| 5 | + | // Container represents a running container in the system. | |
| 6 | + | type Container struct { | |
| 7 | + | ID string | |
| 8 | + | Image string | |
| 9 | + | Name string | |
| 10 | + | Status string | |
| 11 | + | Ports []int | |
| 12 | + | Labels map[string]string | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // NetworkInfo represents network configuration and state. | |
| 16 | + | type NetworkInfo struct { | |
| 17 | + | ID string | |
| 18 | + | Name string | |
| 19 | + | Driver string | |
| 20 | + | Containers []string | |
| 21 | + | Labels map[string]string | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // ContainerConfig holds configuration for creating a container. | |
| 25 | + | type ContainerConfig struct { | |
| 26 | + | Image string | |
| 27 | + | Name string | |
| 28 | + | Env []string | |
| 29 | + | Ports []int | |
| 30 | + | Labels map[string]string | |
| 31 | + | WorkingDir string | |
| 32 | + | Cmd []string | |
| 33 | + | AutoRemove bool | |
| 34 | + | Volumes map[string]string // map[containerPath]volumeName | |
| 35 | + | NetworkMode string // Network to join | |
| 36 | + | Hostname string // Container hostname for DNS | |
| 37 | + | Aliases []string // Additional network aliases | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // ContainerStatus represents the current state of a container. | |
| 41 | + | type ContainerStatus string | |
| 42 | + | ||
| 43 | + | const ( | |
| 44 | + | ContainerStatusRunning ContainerStatus = "running" | |
| 45 | + | ContainerStatusStopped ContainerStatus = "stopped" | |
| 46 | + | ContainerStatusCreated ContainerStatus = "created" | |
| 47 | + | ContainerStatusExited ContainerStatus = "exited" | |
| 48 | + | ContainerStatusPaused ContainerStatus = "paused" | |
| 49 | + | ContainerStatusUnknown ContainerStatus = "unknown" | |
| 50 | + | ) | |
A internal/domain/errors.go +50 -0
| @@ -0,0 +1,50 @@ | |||
| 1 | + | package domain | |
| 2 | + | ||
| 3 | + | import "errors" | |
| 4 | + | ||
| 5 | + | // Domain errors represent business-level errors that can occur in the system. | |
| 6 | + | // These errors are used across layers to communicate specific failure conditions. | |
| 7 | + | var ( | |
| 8 | + | // Container errors | |
| 9 | + | ErrContainerNotFound = errors.New("container not found") | |
| 10 | + | ErrContainerExists = errors.New("container already exists") | |
| 11 | + | ErrContainerNotRunning = errors.New("container is not running") | |
| 12 | + | ErrContainerRunning = errors.New("container is already running") | |
| 13 | + | ||
| 14 | + | // Image errors | |
| 15 | + | ErrImageNotFound = errors.New("image not found") | |
| 16 | + | ErrImagePullFailed = errors.New("failed to pull image") | |
| 17 | + | ErrInvalidImageFormat = errors.New("invalid image format") | |
| 18 | + | ||
| 19 | + | // Route errors | |
| 20 | + | ErrRouteNotFound = errors.New("route not found") | |
| 21 | + | ErrRouteExists = errors.New("route already exists") | |
| 22 | + | ErrInvalidRoute = errors.New("invalid route configuration") | |
| 23 | + | ErrNoTargetAvailable = errors.New("no target available for route") | |
| 24 | + | ||
| 25 | + | // Registry errors | |
| 26 | + | ErrManifestNotFound = errors.New("manifest not found") | |
| 27 | + | ErrBlobNotFound = errors.New("blob not found") | |
| 28 | + | ErrUploadNotFound = errors.New("upload not found") | |
| 29 | + | ErrInvalidDigest = errors.New("invalid digest") | |
| 30 | + | ErrDigestMismatch = errors.New("digest mismatch") | |
| 31 | + | ErrUnauthorized = errors.New("unauthorized") | |
| 32 | + | ||
| 33 | + | // Network errors | |
| 34 | + | ErrNetworkNotFound = errors.New("network not found") | |
| 35 | + | ErrNetworkExists = errors.New("network already exists") | |
| 36 | + | ||
| 37 | + | // Volume errors | |
| 38 | + | ErrVolumeNotFound = errors.New("volume not found") | |
| 39 | + | ErrVolumeExists = errors.New("volume already exists") | |
| 40 | + | ||
| 41 | + | // Config errors | |
| 42 | + | ErrConfigNotFound = errors.New("configuration not found") | |
| 43 | + | ErrInvalidConfig = errors.New("invalid configuration") | |
| 44 | + | ErrConfigLoadFailed = errors.New("failed to load configuration") | |
| 45 | + | ||
| 46 | + | // Environment errors | |
| 47 | + | ErrEnvFileNotFound = errors.New("environment file not found") | |
| 48 | + | ErrSecretNotFound = errors.New("secret not found") | |
| 49 | + | ErrProviderNotFound = errors.New("secret provider not found") | |
| 50 | + | ) | |
A internal/domain/event.go +52 -0
| @@ -0,0 +1,52 @@ | |||
| 1 | + | package domain | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // EventType defines the type of event that occurred. | |
| 6 | + | type EventType string | |
| 7 | + | ||
| 8 | + | const ( | |
| 9 | + | EventImagePushed EventType = "image.pushed" | |
| 10 | + | EventImageDeleted EventType = "image.deleted" | |
| 11 | + | EventConfigReload EventType = "config.reload" | |
| 12 | + | EventManualReload EventType = "manual.reload" | |
| 13 | + | EventContainerStop EventType = "container.stop" | |
| 14 | + | EventContainerStart EventType = "container.start" | |
| 15 | + | EventContainerHealthCheck EventType = "container.health_check" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // Event represents a domain event that occurred in the system. | |
| 19 | + | type Event struct { | |
| 20 | + | ID string | |
| 21 | + | Type EventType | |
| 22 | + | Timestamp time.Time | |
| 23 | + | ImageName string | |
| 24 | + | Tag string | |
| 25 | + | Route string | |
| 26 | + | ContainerID string | |
| 27 | + | Data any | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // ImagePushedPayload contains data for image.pushed events. | |
| 31 | + | type ImagePushedPayload struct { | |
| 32 | + | Name string | |
| 33 | + | Reference string | |
| 34 | + | Manifest []byte | |
| 35 | + | Annotations map[string]string | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // ContainerEventPayload contains data for container events. | |
| 39 | + | type ContainerEventPayload struct { | |
| 40 | + | ContainerID string | |
| 41 | + | Domain string | |
| 42 | + | Image string | |
| 43 | + | Action string | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // ConfigReloadPayload contains data for config.reload events. | |
| 47 | + | type ConfigReloadPayload struct { | |
| 48 | + | Source string // "file" or "manual" | |
| 49 | + | AddedRoutes []string | |
| 50 | + | RemovedRoutes []string | |
| 51 | + | UpdatedRoutes []string | |
| 52 | + | } | |
A internal/domain/registry.go +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | package domain | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // Manifest represents an OCI image manifest. | |
| 6 | + | type Manifest struct { | |
| 7 | + | Name string | |
| 8 | + | Reference string | |
| 9 | + | ContentType string | |
| 10 | + | Data []byte | |
| 11 | + | Digest string | |
| 12 | + | Size int64 | |
| 13 | + | Annotations map[string]string | |
| 14 | + | CreatedAt time.Time | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // Blob represents a binary large object (layer or config) in the registry. | |
| 18 | + | type Blob struct { | |
| 19 | + | Digest string | |
| 20 | + | Size int64 | |
| 21 | + | CreatedAt time.Time | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Repository represents a container image repository. | |
| 25 | + | type Repository struct { | |
| 26 | + | Name string | |
| 27 | + | Tags []string | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Upload represents an in-progress blob upload. | |
| 31 | + | type Upload struct { | |
| 32 | + | UUID string | |
| 33 | + | Name string | |
| 34 | + | Size int64 | |
| 35 | + | StartedAt time.Time | |
| 36 | + | } | |
A internal/domain/route.go +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | package domain | |
| 2 | + | ||
| 3 | + | // Route represents a mapping from a domain to a container image. | |
| 4 | + | type Route struct { | |
| 5 | + | Domain string | |
| 6 | + | Image string | |
| 7 | + | HTTPS bool | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | // ProxyTarget represents the destination for proxying requests. | |
| 11 | + | type ProxyTarget struct { | |
| 12 | + | Host string | |
| 13 | + | Port int | |
| 14 | + | ContainerID string | |
| 15 | + | Scheme string // "http" or "https" | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // RouteMatch represents the result of matching a request to a route. | |
| 19 | + | type RouteMatch struct { | |
| 20 | + | Route Route | |
| 21 | + | Container *Container | |
| 22 | + | Target *ProxyTarget | |
| 23 | + | } | |
D internal/env/loader.go +0 -294
| @@ -1,294 +0,0 @@ | |||
| 1 | - | package env | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bufio" | |
| 5 | - | "fmt" | |
| 6 | - | "os" | |
| 7 | - | "path/filepath" | |
| 8 | - | "strings" | |
| 9 | - | ||
| 10 | - | "gordon/internal/config" | |
| 11 | - | ||
| 12 | - | "github.com/rs/zerolog/log" | |
| 13 | - | ) | |
| 14 | - | ||
| 15 | - | type Loader struct { | |
| 16 | - | cfg *config.Config | |
| 17 | - | secretProviders map[string]SecretProvider | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | func NewLoader(cfg *config.Config) *Loader { | |
| 21 | - | return &Loader{ | |
| 22 | - | cfg: cfg, | |
| 23 | - | secretProviders: make(map[string]SecretProvider), | |
| 24 | - | } | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | func (l *Loader) RegisterSecretProvider(name string, provider SecretProvider) { | |
| 28 | - | l.secretProviders[name] = provider | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | func (l *Loader) LoadEnvForRoute(domain string) ([]string, error) { | |
| 32 | - | envVars := []string{} | |
| 33 | - | ||
| 34 | - | // Create domain-safe filename (replace dots and other chars with underscores) | |
| 35 | - | safeDomain := strings.ReplaceAll(domain, ".", "_") | |
| 36 | - | safeDomain = strings.ReplaceAll(safeDomain, ":", "_") | |
| 37 | - | safeDomain = strings.ReplaceAll(safeDomain, "/", "_") | |
| 38 | - | ||
| 39 | - | envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env") | |
| 40 | - | ||
| 41 | - | // Check if env file exists | |
| 42 | - | if _, err := os.Stat(envFile); os.IsNotExist(err) { | |
| 43 | - | log.Debug(). | |
| 44 | - | Str("domain", domain). | |
| 45 | - | Str("env_file", envFile). | |
| 46 | - | Msg("No env file found for route") | |
| 47 | - | return envVars, nil | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | log.Debug(). | |
| 51 | - | Str("domain", domain). | |
| 52 | - | Str("env_file", envFile). | |
| 53 | - | Msg("Loading env file for route") | |
| 54 | - | ||
| 55 | - | file, err := os.Open(envFile) | |
| 56 | - | if err != nil { | |
| 57 | - | return nil, fmt.Errorf("failed to open env file %s: %w", envFile, err) | |
| 58 | - | } | |
| 59 | - | defer file.Close() | |
| 60 | - | ||
| 61 | - | scanner := bufio.NewScanner(file) | |
| 62 | - | lineNum := 0 | |
| 63 | - | ||
| 64 | - | for scanner.Scan() { | |
| 65 | - | lineNum++ | |
| 66 | - | line := strings.TrimSpace(scanner.Text()) | |
| 67 | - | ||
| 68 | - | // Skip empty lines and comments | |
| 69 | - | if line == "" || strings.HasPrefix(line, "#") { | |
| 70 | - | continue | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | // Parse KEY=VALUE format | |
| 74 | - | parts := strings.SplitN(line, "=", 2) | |
| 75 | - | if len(parts) != 2 { | |
| 76 | - | log.Warn(). | |
| 77 | - | Str("file", envFile). | |
| 78 | - | Int("line", lineNum). | |
| 79 | - | Str("content", line). | |
| 80 | - | Msg("Invalid env file line format, skipping") | |
| 81 | - | continue | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | key := strings.TrimSpace(parts[0]) | |
| 85 | - | value := strings.TrimSpace(parts[1]) | |
| 86 | - | ||
| 87 | - | // Remove quotes if present | |
| 88 | - | if len(value) >= 2 { | |
| 89 | - | if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || | |
| 90 | - | (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) { | |
| 91 | - | value = value[1 : len(value)-1] | |
| 92 | - | } | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | // Resolve secrets if value contains secret syntax | |
| 96 | - | resolvedValue, err := l.resolveSecrets(value) | |
| 97 | - | if err != nil { | |
| 98 | - | log.Error(). | |
| 99 | - | Err(err). | |
| 100 | - | Str("key", key). | |
| 101 | - | Str("file", envFile). | |
| 102 | - | Int("line", lineNum). | |
| 103 | - | Msg("Failed to resolve secret") | |
| 104 | - | return nil, fmt.Errorf("failed to resolve secret for %s in %s line %d: %w", key, envFile, lineNum, err) | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | envVars = append(envVars, fmt.Sprintf("%s=%s", key, resolvedValue)) | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | if err := scanner.Err(); err != nil { | |
| 111 | - | return nil, fmt.Errorf("error reading env file %s: %w", envFile, err) | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | log.Info(). | |
| 115 | - | Str("domain", domain). | |
| 116 | - | Int("count", len(envVars)). | |
| 117 | - | Msg("Loaded environment variables for route") | |
| 118 | - | ||
| 119 | - | return envVars, nil | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | func (l *Loader) resolveSecrets(value string) (string, error) { | |
| 123 | - | // Check if value contains secret syntax: ${provider:path} | |
| 124 | - | if !strings.Contains(value, "${") { | |
| 125 | - | return value, nil | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | result := value | |
| 129 | - | for { | |
| 130 | - | start := strings.Index(result, "${") | |
| 131 | - | if start == -1 { | |
| 132 | - | break | |
| 133 | - | } | |
| 134 | - | ||
| 135 | - | end := strings.Index(result[start:], "}") | |
| 136 | - | if end == -1 { | |
| 137 | - | return "", fmt.Errorf("unclosed secret syntax in value: %s", value) | |
| 138 | - | } | |
| 139 | - | end += start | |
| 140 | - | ||
| 141 | - | secretRef := result[start+2 : end] | |
| 142 | - | parts := strings.SplitN(secretRef, ":", 2) | |
| 143 | - | if len(parts) != 2 { | |
| 144 | - | return "", fmt.Errorf("invalid secret syntax: expected 'provider:path', got '%s'", secretRef) | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | providerName := parts[0] | |
| 148 | - | secretPath := parts[1] | |
| 149 | - | ||
| 150 | - | provider, exists := l.secretProviders[providerName] | |
| 151 | - | if !exists { | |
| 152 | - | return "", fmt.Errorf("unknown secret provider: %s", providerName) | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | secretValue, err := provider.GetSecret(secretPath) | |
| 156 | - | if err != nil { | |
| 157 | - | return "", fmt.Errorf("failed to get secret from provider %s: %w", providerName, err) | |
| 158 | - | } | |
| 159 | - | ||
| 160 | - | // Replace the secret reference with the actual value | |
| 161 | - | result = result[:start] + secretValue + result[end+1:] | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | return result, nil | |
| 165 | - | } | |
| 166 | - | ||
| 167 | - | func (l *Loader) EnsureEnvDir() error { | |
| 168 | - | if err := os.MkdirAll(l.cfg.Env.Dir, 0700); err != nil { | |
| 169 | - | return fmt.Errorf("failed to create env directory %s: %w", l.cfg.Env.Dir, err) | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | log.Debug(). | |
| 173 | - | Str("env_dir", l.cfg.Env.Dir). | |
| 174 | - | Msg("Ensured env directory exists") | |
| 175 | - | ||
| 176 | - | return nil | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | func (l *Loader) CreateEnvFilesForRoutes() error { | |
| 180 | - | routes := l.cfg.GetRoutes() | |
| 181 | - | if len(routes) == 0 { | |
| 182 | - | log.Debug().Msg("No routes configured, skipping env file creation") | |
| 183 | - | return nil | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | createdCount := 0 | |
| 187 | - | for _, route := range routes { | |
| 188 | - | // Create domain-safe filename (same logic as LoadEnvForRoute) | |
| 189 | - | safeDomain := strings.ReplaceAll(route.Domain, ".", "_") | |
| 190 | - | safeDomain = strings.ReplaceAll(safeDomain, ":", "_") | |
| 191 | - | safeDomain = strings.ReplaceAll(safeDomain, "/", "_") | |
| 192 | - | ||
| 193 | - | envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env") | |
| 194 | - | ||
| 195 | - | // Check if file already exists | |
| 196 | - | if _, err := os.Stat(envFile); err == nil { | |
| 197 | - | log.Debug(). | |
| 198 | - | Str("domain", route.Domain). | |
| 199 | - | Str("env_file", envFile). | |
| 200 | - | Msg("Env file already exists, skipping creation") | |
| 201 | - | continue | |
| 202 | - | } else if !os.IsNotExist(err) { | |
| 203 | - | log.Warn(). | |
| 204 | - | Err(err). | |
| 205 | - | Str("env_file", envFile). | |
| 206 | - | Msg("Error checking env file, skipping creation") | |
| 207 | - | continue | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | // Create empty env file with helpful comments | |
| 211 | - | content := fmt.Sprintf(`# Environment variables for route: %s | |
| 212 | - | # Image: %s | |
| 213 | - | ||
| 214 | - | `, route.Domain, route.Image) | |
| 215 | - | ||
| 216 | - | if err := os.WriteFile(envFile, []byte(content), 0600); err != nil { | |
| 217 | - | log.Error(). | |
| 218 | - | Err(err). | |
| 219 | - | Str("domain", route.Domain). | |
| 220 | - | Str("env_file", envFile). | |
| 221 | - | Msg("Failed to create env file") | |
| 222 | - | continue | |
| 223 | - | } | |
| 224 | - | ||
| 225 | - | createdCount++ | |
| 226 | - | log.Info(). | |
| 227 | - | Str("domain", route.Domain). | |
| 228 | - | Str("env_file", envFile). | |
| 229 | - | Msg("Created empty env file for route") | |
| 230 | - | } | |
| 231 | - | ||
| 232 | - | if createdCount > 0 { | |
| 233 | - | log.Info(). | |
| 234 | - | Int("created", createdCount). | |
| 235 | - | Int("total_routes", len(routes)). | |
| 236 | - | Str("env_dir", l.cfg.Env.Dir). | |
| 237 | - | Msg("Created env files for routes") | |
| 238 | - | } | |
| 239 | - | ||
| 240 | - | return nil | |
| 241 | - | } | |
| 242 | - | ||
| 243 | - | func (l *Loader) CreateEnvFileForRoute(domain, image string) error { | |
| 244 | - | // Create domain-safe filename (same logic as LoadEnvForRoute) | |
| 245 | - | safeDomain := strings.ReplaceAll(domain, ".", "_") | |
| 246 | - | safeDomain = strings.ReplaceAll(safeDomain, ":", "_") | |
| 247 | - | safeDomain = strings.ReplaceAll(safeDomain, "/", "_") | |
| 248 | - | ||
| 249 | - | envFile := filepath.Join(l.cfg.Env.Dir, safeDomain+".env") | |
| 250 | - | ||
| 251 | - | // Check if file already exists | |
| 252 | - | if _, err := os.Stat(envFile); err == nil { | |
| 253 | - | log.Debug(). | |
| 254 | - | Str("domain", domain). | |
| 255 | - | Str("env_file", envFile). | |
| 256 | - | Msg("Env file already exists, skipping creation") | |
| 257 | - | return nil | |
| 258 | - | } else if !os.IsNotExist(err) { | |
| 259 | - | return fmt.Errorf("error checking env file %s: %w", envFile, err) | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | // Ensure directory exists | |
| 263 | - | if err := l.EnsureEnvDir(); err != nil { | |
| 264 | - | return err | |
| 265 | - | } | |
| 266 | - | ||
| 267 | - | // Create empty env file with helpful comments | |
| 268 | - | content := fmt.Sprintf(`# Environment variables for route: %s | |
| 269 | - | # Image: %s | |
| 270 | - | ||
| 271 | - | `, domain, image) | |
| 272 | - | ||
| 273 | - | if err := os.WriteFile(envFile, []byte(content), 0600); err != nil { | |
| 274 | - | return fmt.Errorf("failed to create env file %s: %w", envFile, err) | |
| 275 | - | } | |
| 276 | - | ||
| 277 | - | log.Info(). | |
| 278 | - | Str("domain", domain). | |
| 279 | - | Str("env_file", envFile). | |
| 280 | - | Msg("Created empty env file for new route") | |
| 281 | - | ||
| 282 | - | return nil | |
| 283 | - | } | |
| 284 | - | ||
| 285 | - | func (l *Loader) UpdateConfig(cfg *config.Config) { | |
| 286 | - | l.cfg = cfg | |
| 287 | - | } | |
| 288 | - | ||
| 289 | - | func (l *Loader) GetEnvFilePath(domain string) string { | |
| 290 | - | safeDomain := strings.ReplaceAll(domain, ".", "_") | |
| 291 | - | safeDomain = strings.ReplaceAll(safeDomain, ":", "_") | |
| 292 | - | safeDomain = strings.ReplaceAll(safeDomain, "/", "_") | |
| 293 | - | return filepath.Join(l.cfg.Env.Dir, safeDomain+".env") | |
| 294 | - | }⏎̸ | |
D internal/env/loader_test.go +0 -488
| @@ -1,488 +0,0 @@ | |||
| 1 | - | package env | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "os" | |
| 5 | - | "path/filepath" | |
| 6 | - | "testing" | |
| 7 | - | ||
| 8 | - | "github.com/stretchr/testify/assert" | |
| 9 | - | "github.com/stretchr/testify/require" | |
| 10 | - | ||
| 11 | - | "gordon/internal/config" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | func TestNewLoader(t *testing.T) { | |
| 15 | - | cfg := &config.Config{ | |
| 16 | - | Env: config.EnvConfig{ | |
| 17 | - | Dir: "/tmp/env", | |
| 18 | - | }, | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | loader := NewLoader(cfg) | |
| 22 | - | ||
| 23 | - | assert.NotNil(t, loader) | |
| 24 | - | assert.Equal(t, cfg, loader.cfg) | |
| 25 | - | assert.NotNil(t, loader.secretProviders) | |
| 26 | - | assert.Empty(t, loader.secretProviders) | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | func TestLoader_RegisterSecretProvider(t *testing.T) { | |
| 30 | - | cfg := &config.Config{} | |
| 31 | - | loader := NewLoader(cfg) | |
| 32 | - | ||
| 33 | - | mockProvider := &MockSecretProvider{name: "test"} | |
| 34 | - | loader.RegisterSecretProvider("test", mockProvider) | |
| 35 | - | ||
| 36 | - | assert.Contains(t, loader.secretProviders, "test") | |
| 37 | - | assert.Equal(t, mockProvider, loader.secretProviders["test"]) | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | func TestLoader_LoadEnvForRoute(t *testing.T) { | |
| 41 | - | tests := []struct { | |
| 42 | - | name string | |
| 43 | - | domain string | |
| 44 | - | fileContent string | |
| 45 | - | expectedVars []string | |
| 46 | - | expectedError bool | |
| 47 | - | setupProvider bool | |
| 48 | - | providerSecret string | |
| 49 | - | }{ | |
| 50 | - | { | |
| 51 | - | name: "no env file exists", | |
| 52 | - | domain: "app.example.com", | |
| 53 | - | expectedVars: []string{}, | |
| 54 | - | }, | |
| 55 | - | { | |
| 56 | - | name: "basic env file", | |
| 57 | - | domain: "app.example.com", | |
| 58 | - | fileContent: "KEY1=value1\nKEY2=value2\n", | |
| 59 | - | expectedVars: []string{ | |
| 60 | - | "KEY1=value1", | |
| 61 | - | "KEY2=value2", | |
| 62 | - | }, | |
| 63 | - | }, | |
| 64 | - | { | |
| 65 | - | name: "env file with comments and empty lines", | |
| 66 | - | domain: "app.example.com", | |
| 67 | - | fileContent: `# This is a comment | |
| 68 | - | KEY1=value1 | |
| 69 | - | ||
| 70 | - | # Another comment | |
| 71 | - | KEY2=value2 | |
| 72 | - | `, | |
| 73 | - | expectedVars: []string{ | |
| 74 | - | "KEY1=value1", | |
| 75 | - | "KEY2=value2", | |
| 76 | - | }, | |
| 77 | - | }, | |
| 78 | - | { | |
| 79 | - | name: "env file with quoted values", | |
| 80 | - | domain: "app.example.com", | |
| 81 | - | fileContent: `KEY1="quoted value"` + "\n" + `KEY2='single quoted'` + "\n", | |
| 82 | - | expectedVars: []string{ | |
| 83 | - | "KEY1=quoted value", | |
| 84 | - | "KEY2=single quoted", | |
| 85 | - | }, | |
| 86 | - | }, | |
| 87 | - | { | |
| 88 | - | name: "env file with secret resolution", | |
| 89 | - | domain: "app.example.com", | |
| 90 | - | fileContent: "SECRET_KEY=${test:secret/path}\nNORMAL_KEY=value\n", | |
| 91 | - | setupProvider: true, | |
| 92 | - | providerSecret: "secret-value", | |
| 93 | - | expectedVars: []string{ | |
| 94 | - | "SECRET_KEY=secret-value", | |
| 95 | - | "NORMAL_KEY=value", | |
| 96 | - | }, | |
| 97 | - | }, | |
| 98 | - | { | |
| 99 | - | name: "env file with invalid format", | |
| 100 | - | domain: "app.example.com", | |
| 101 | - | fileContent: "INVALID_LINE_NO_EQUALS\nVALID_KEY=value\n", | |
| 102 | - | expectedVars: []string{ | |
| 103 | - | "VALID_KEY=value", | |
| 104 | - | }, | |
| 105 | - | }, | |
| 106 | - | { | |
| 107 | - | name: "env file with unknown secret provider", | |
| 108 | - | domain: "app.example.com", | |
| 109 | - | fileContent: "SECRET_KEY=${unknown:secret/path}\n", | |
| 110 | - | expectedError: true, | |
| 111 | - | }, | |
| 112 | - | { | |
| 113 | - | name: "env file with malformed secret syntax", | |
| 114 | - | domain: "app.example.com", | |
| 115 | - | fileContent: "SECRET_KEY=${malformed\n", | |
| 116 | - | expectedError: true, | |
| 117 | - | }, | |
| 118 | - | { | |
| 119 | - | name: "env file with invalid secret format", | |
| 120 | - | domain: "app.example.com", | |
| 121 | - | fileContent: "SECRET_KEY=${invalid-format}\n", | |
| 122 | - | expectedError: true, | |
| 123 | - | }, | |
| 124 | - | { | |
| 125 | - | name: "domain with special characters", | |
| 126 | - | domain: "sub.app.example.com:8080", | |
| 127 | - | fileContent: "KEY1=value1\n", | |
| 128 | - | expectedVars: []string{ | |
| 129 | - | "KEY1=value1", | |
| 130 | - | }, | |
| 131 | - | }, | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | for _, tt := range tests { | |
| 135 | - | t.Run(tt.name, func(t *testing.T) { | |
| 136 | - | envDir := "/tmp/env" | |
| 137 | - | cfg := &config.Config{ | |
| 138 | - | Env: config.EnvConfig{ | |
| 139 | - | Dir: envDir, | |
| 140 | - | }, | |
| 141 | - | } | |
| 142 | - | ||
| 143 | - | loader := NewLoader(cfg) | |
| 144 | - | ||
| 145 | - | // Setup secret provider if needed | |
| 146 | - | if tt.setupProvider { | |
| 147 | - | mockProvider := &MockSecretProvider{ | |
| 148 | - | name: "test", | |
| 149 | - | secret: tt.providerSecret, | |
| 150 | - | } | |
| 151 | - | loader.RegisterSecretProvider("test", mockProvider) | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // Create env file if content is provided | |
| 155 | - | if tt.fileContent != "" { | |
| 156 | - | // Create safe domain name for file | |
| 157 | - | safeDomain := "app_example_com" | |
| 158 | - | if tt.domain == "sub.app.example.com:8080" { | |
| 159 | - | safeDomain = "sub_app_example_com_8080" | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | // envFile would be: filepath.Join(envDir, safeDomain+".env") | |
| 163 | - | ||
| 164 | - | // Create directory and file using real filesystem for this test | |
| 165 | - | realEnvDir := filepath.Join(os.TempDir(), "gordon-test-env") | |
| 166 | - | defer os.RemoveAll(realEnvDir) | |
| 167 | - | ||
| 168 | - | err := os.MkdirAll(realEnvDir, 0755) | |
| 169 | - | require.NoError(t, err) | |
| 170 | - | ||
| 171 | - | realEnvFile := filepath.Join(realEnvDir, safeDomain+".env") | |
| 172 | - | err = os.WriteFile(realEnvFile, []byte(tt.fileContent), 0644) | |
| 173 | - | require.NoError(t, err) | |
| 174 | - | ||
| 175 | - | // Update config to use real directory | |
| 176 | - | cfg.Env.Dir = realEnvDir | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | // Execute test | |
| 180 | - | vars, err := loader.LoadEnvForRoute(tt.domain) | |
| 181 | - | ||
| 182 | - | // Verify results | |
| 183 | - | if tt.expectedError { | |
| 184 | - | assert.Error(t, err) | |
| 185 | - | } else { | |
| 186 | - | assert.NoError(t, err) | |
| 187 | - | assert.Equal(t, tt.expectedVars, vars) | |
| 188 | - | } | |
| 189 | - | }) | |
| 190 | - | } | |
| 191 | - | } | |
| 192 | - | ||
| 193 | - | func TestLoader_ResolveSecrets(t *testing.T) { | |
| 194 | - | cfg := &config.Config{} | |
| 195 | - | loader := NewLoader(cfg) | |
| 196 | - | ||
| 197 | - | // Register mock secret provider | |
| 198 | - | mockProvider := &MockSecretProvider{ | |
| 199 | - | name: "test", | |
| 200 | - | secret: "secret-value", | |
| 201 | - | } | |
| 202 | - | loader.RegisterSecretProvider("test", mockProvider) | |
| 203 | - | ||
| 204 | - | tests := []struct { | |
| 205 | - | name string | |
| 206 | - | input string | |
| 207 | - | expected string | |
| 208 | - | expectedError bool | |
| 209 | - | }{ | |
| 210 | - | { | |
| 211 | - | name: "no secret syntax", | |
| 212 | - | input: "plain-value", | |
| 213 | - | expected: "plain-value", | |
| 214 | - | }, | |
| 215 | - | { | |
| 216 | - | name: "single secret replacement", | |
| 217 | - | input: "${test:secret/path}", | |
| 218 | - | expected: "secret-value", | |
| 219 | - | }, | |
| 220 | - | { | |
| 221 | - | name: "secret in middle of string", | |
| 222 | - | input: "prefix-${test:secret/path}-suffix", | |
| 223 | - | expected: "prefix-secret-value-suffix", | |
| 224 | - | }, | |
| 225 | - | { | |
| 226 | - | name: "multiple secrets", | |
| 227 | - | input: "${test:secret1}-${test:secret2}", | |
| 228 | - | expected: "secret-value-secret-value", | |
| 229 | - | }, | |
| 230 | - | { | |
| 231 | - | name: "unclosed secret syntax", | |
| 232 | - | input: "${test:secret", | |
| 233 | - | expectedError: true, | |
| 234 | - | }, | |
| 235 | - | { | |
| 236 | - | name: "invalid secret format", | |
| 237 | - | input: "${invalid-format}", | |
| 238 | - | expectedError: true, | |
| 239 | - | }, | |
| 240 | - | { | |
| 241 | - | name: "unknown provider", | |
| 242 | - | input: "${unknown:secret/path}", | |
| 243 | - | expectedError: true, | |
| 244 | - | }, | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | for _, tt := range tests { | |
| 248 | - | t.Run(tt.name, func(t *testing.T) { | |
| 249 | - | result, err := loader.resolveSecrets(tt.input) | |
| 250 | - | ||
| 251 | - | if tt.expectedError { | |
| 252 | - | assert.Error(t, err) | |
| 253 | - | } else { | |
| 254 | - | assert.NoError(t, err) | |
| 255 | - | assert.Equal(t, tt.expected, result) | |
| 256 | - | } | |
| 257 | - | }) | |
| 258 | - | } | |
| 259 | - | } | |
| 260 | - | ||
| 261 | - | func TestLoader_EnsureEnvDir(t *testing.T) { | |
| 262 | - | tempDir := os.TempDir() | |
| 263 | - | envDir := filepath.Join(tempDir, "gordon-test-env-ensure") | |
| 264 | - | defer os.RemoveAll(envDir) | |
| 265 | - | ||
| 266 | - | cfg := &config.Config{ | |
| 267 | - | Env: config.EnvConfig{ | |
| 268 | - | Dir: envDir, | |
| 269 | - | }, | |
| 270 | - | } | |
| 271 | - | ||
| 272 | - | loader := NewLoader(cfg) | |
| 273 | - | ||
| 274 | - | // Directory should not exist initially | |
| 275 | - | _, err := os.Stat(envDir) | |
| 276 | - | assert.True(t, os.IsNotExist(err)) | |
| 277 | - | ||
| 278 | - | // Ensure directory | |
| 279 | - | err = loader.EnsureEnvDir() | |
| 280 | - | assert.NoError(t, err) | |
| 281 | - | ||
| 282 | - | // Directory should now exist | |
| 283 | - | info, err := os.Stat(envDir) | |
| 284 | - | assert.NoError(t, err) | |
| 285 | - | assert.True(t, info.IsDir()) | |
| 286 | - | assert.Equal(t, os.FileMode(0700), info.Mode().Perm()) | |
| 287 | - | } | |
| 288 | - | ||
| 289 | - | func TestLoader_CreateEnvFilesForRoutes(t *testing.T) { | |
| 290 | - | tempDir := os.TempDir() | |
| 291 | - | envDir := filepath.Join(tempDir, "gordon-test-env-create") | |
| 292 | - | defer os.RemoveAll(envDir) | |
| 293 | - | ||
| 294 | - | cfg := &config.Config{ | |
| 295 | - | Env: config.EnvConfig{ | |
| 296 | - | Dir: envDir, | |
| 297 | - | }, | |
| 298 | - | Routes: map[string]string{ | |
| 299 | - | "app1.example.com": "nginx:latest", | |
| 300 | - | "app2.example.com": "redis:alpine", | |
| 301 | - | "api.example.com:8080": "myapi:v1", | |
| 302 | - | }, | |
| 303 | - | } | |
| 304 | - | ||
| 305 | - | loader := NewLoader(cfg) | |
| 306 | - | ||
| 307 | - | // Ensure the env directory exists | |
| 308 | - | err := os.MkdirAll(envDir, 0755) | |
| 309 | - | require.NoError(t, err) | |
| 310 | - | ||
| 311 | - | err = loader.CreateEnvFilesForRoutes() | |
| 312 | - | assert.NoError(t, err) | |
| 313 | - | ||
| 314 | - | // Check that files were created | |
| 315 | - | expectedFiles := map[string]struct{domain, image string}{ | |
| 316 | - | "app1_example_com.env": {domain: "app1.example.com", image: "nginx:latest"}, | |
| 317 | - | "app2_example_com.env": {domain: "app2.example.com", image: "redis:alpine"}, | |
| 318 | - | "api_example_com_8080.env": {domain: "api.example.com:8080", image: "myapi:v1"}, | |
| 319 | - | } | |
| 320 | - | ||
| 321 | - | for fileName, expected := range expectedFiles { | |
| 322 | - | filePath := filepath.Join(envDir, fileName) | |
| 323 | - | assert.FileExists(t, filePath) | |
| 324 | - | ||
| 325 | - | // Check file content | |
| 326 | - | content, err := os.ReadFile(filePath) | |
| 327 | - | assert.NoError(t, err) | |
| 328 | - | ||
| 329 | - | expectedContent := "# Environment variables for route: " + expected.domain + "\n# Image: " + expected.image + "\n\n" | |
| 330 | - | assert.Contains(t, string(content), expectedContent) | |
| 331 | - | ||
| 332 | - | // Check file permissions | |
| 333 | - | info, err := os.Stat(filePath) | |
| 334 | - | assert.NoError(t, err) | |
| 335 | - | assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) | |
| 336 | - | } | |
| 337 | - | } | |
| 338 | - | ||
| 339 | - | func TestLoader_CreateEnvFilesForRoutes_NoRoutes(t *testing.T) { | |
| 340 | - | cfg := &config.Config{ | |
| 341 | - | Env: config.EnvConfig{ | |
| 342 | - | Dir: "/tmp/env", | |
| 343 | - | }, | |
| 344 | - | Routes: map[string]string{}, | |
| 345 | - | } | |
| 346 | - | ||
| 347 | - | loader := NewLoader(cfg) | |
| 348 | - | ||
| 349 | - | err := loader.CreateEnvFilesForRoutes() | |
| 350 | - | assert.NoError(t, err) | |
| 351 | - | } | |
| 352 | - | ||
| 353 | - | func TestLoader_CreateEnvFilesForRoutes_ExistingFile(t *testing.T) { | |
| 354 | - | tempDir := os.TempDir() | |
| 355 | - | envDir := filepath.Join(tempDir, "gordon-test-env-existing") | |
| 356 | - | defer os.RemoveAll(envDir) | |
| 357 | - | ||
| 358 | - | err := os.MkdirAll(envDir, 0755) | |
| 359 | - | require.NoError(t, err) | |
| 360 | - | ||
| 361 | - | // Create existing file | |
| 362 | - | existingFile := filepath.Join(envDir, "app_example_com.env") | |
| 363 | - | err = os.WriteFile(existingFile, []byte("EXISTING=value\n"), 0644) | |
| 364 | - | require.NoError(t, err) | |
| 365 | - | ||
| 366 | - | cfg := &config.Config{ | |
| 367 | - | Env: config.EnvConfig{ | |
| 368 | - | Dir: envDir, | |
| 369 | - | }, | |
| 370 | - | Routes: map[string]string{ | |
| 371 | - | "app.example.com": "nginx:latest", | |
| 372 | - | }, | |
| 373 | - | } | |
| 374 | - | ||
| 375 | - | loader := NewLoader(cfg) | |
| 376 | - | ||
| 377 | - | err = loader.CreateEnvFilesForRoutes() | |
| 378 | - | assert.NoError(t, err) | |
| 379 | - | ||
| 380 | - | // File should still have original content | |
| 381 | - | content, err := os.ReadFile(existingFile) | |
| 382 | - | assert.NoError(t, err) | |
| 383 | - | assert.Equal(t, "EXISTING=value\n", string(content)) | |
| 384 | - | } | |
| 385 | - | ||
| 386 | - | func TestLoader_CreateEnvFileForRoute(t *testing.T) { | |
| 387 | - | tempDir := os.TempDir() | |
| 388 | - | envDir := filepath.Join(tempDir, "gordon-test-env-single") | |
| 389 | - | defer os.RemoveAll(envDir) | |
| 390 | - | ||
| 391 | - | cfg := &config.Config{ | |
| 392 | - | Env: config.EnvConfig{ | |
| 393 | - | Dir: envDir, | |
| 394 | - | }, | |
| 395 | - | } | |
| 396 | - | ||
| 397 | - | loader := NewLoader(cfg) | |
| 398 | - | ||
| 399 | - | domain := "test.example.com" | |
| 400 | - | image := "test:latest" | |
| 401 | - | ||
| 402 | - | err := loader.CreateEnvFileForRoute(domain, image) | |
| 403 | - | assert.NoError(t, err) | |
| 404 | - | ||
| 405 | - | // Check file was created | |
| 406 | - | expectedFile := filepath.Join(envDir, "test_example_com.env") | |
| 407 | - | assert.FileExists(t, expectedFile) | |
| 408 | - | ||
| 409 | - | // Check content | |
| 410 | - | content, err := os.ReadFile(expectedFile) | |
| 411 | - | assert.NoError(t, err) | |
| 412 | - | ||
| 413 | - | expectedContent := "# Environment variables for route: test.example.com\n# Image: test:latest\n\n" | |
| 414 | - | assert.Equal(t, expectedContent, string(content)) | |
| 415 | - | ||
| 416 | - | // Test creating the same file again (should skip) | |
| 417 | - | err = loader.CreateEnvFileForRoute(domain, image) | |
| 418 | - | assert.NoError(t, err) | |
| 419 | - | ||
| 420 | - | // Content should be unchanged | |
| 421 | - | content2, err := os.ReadFile(expectedFile) | |
| 422 | - | assert.NoError(t, err) | |
| 423 | - | assert.Equal(t, string(content), string(content2)) | |
| 424 | - | } | |
| 425 | - | ||
| 426 | - | func TestLoader_UpdateConfig(t *testing.T) { | |
| 427 | - | cfg1 := &config.Config{Env: config.EnvConfig{Dir: "/tmp/env1"}} | |
| 428 | - | cfg2 := &config.Config{Env: config.EnvConfig{Dir: "/tmp/env2"}} | |
| 429 | - | ||
| 430 | - | loader := NewLoader(cfg1) | |
| 431 | - | assert.Equal(t, cfg1, loader.cfg) | |
| 432 | - | ||
| 433 | - | loader.UpdateConfig(cfg2) | |
| 434 | - | assert.Equal(t, cfg2, loader.cfg) | |
| 435 | - | } | |
| 436 | - | ||
| 437 | - | func TestLoader_GetEnvFilePath(t *testing.T) { | |
| 438 | - | cfg := &config.Config{ | |
| 439 | - | Env: config.EnvConfig{ | |
| 440 | - | Dir: "/tmp/env", | |
| 441 | - | }, | |
| 442 | - | } | |
| 443 | - | ||
| 444 | - | loader := NewLoader(cfg) | |
| 445 | - | ||
| 446 | - | tests := []struct { | |
| 447 | - | domain string | |
| 448 | - | expected string | |
| 449 | - | }{ | |
| 450 | - | { | |
| 451 | - | domain: "app.example.com", | |
| 452 | - | expected: "/tmp/env/app_example_com.env", | |
| 453 | - | }, | |
| 454 | - | { | |
| 455 | - | domain: "api.example.com:8080", | |
| 456 | - | expected: "/tmp/env/api_example_com_8080.env", | |
| 457 | - | }, | |
| 458 | - | { | |
| 459 | - | domain: "sub.domain.example.com/path", | |
| 460 | - | expected: "/tmp/env/sub_domain_example_com_path.env", | |
| 461 | - | }, | |
| 462 | - | } | |
| 463 | - | ||
| 464 | - | for _, tt := range tests { | |
| 465 | - | t.Run(tt.domain, func(t *testing.T) { | |
| 466 | - | result := loader.GetEnvFilePath(tt.domain) | |
| 467 | - | assert.Equal(t, tt.expected, result) | |
| 468 | - | }) | |
| 469 | - | } | |
| 470 | - | } | |
| 471 | - | ||
| 472 | - | // MockSecretProvider is a mock implementation of SecretProvider for testing | |
| 473 | - | type MockSecretProvider struct { | |
| 474 | - | name string | |
| 475 | - | secret string | |
| 476 | - | err error | |
| 477 | - | } | |
| 478 | - | ||
| 479 | - | func (m *MockSecretProvider) Name() string { | |
| 480 | - | return m.name | |
| 481 | - | } | |
| 482 | - | ||
| 483 | - | func (m *MockSecretProvider) GetSecret(path string) (string, error) { | |
| 484 | - | if m.err != nil { | |
| 485 | - | return "", m.err | |
| 486 | - | } | |
| 487 | - | return m.secret, nil | |
| 488 | - | }⏎̸ | |
D internal/env/providers.go +0 -104
| @@ -1,104 +0,0 @@ | |||
| 1 | - | package env | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "os/exec" | |
| 7 | - | "strings" | |
| 8 | - | "time" | |
| 9 | - | ||
| 10 | - | "github.com/rs/zerolog/log" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | type SecretProvider interface { | |
| 14 | - | GetSecret(path string) (string, error) | |
| 15 | - | Name() string | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | type PassProvider struct { | |
| 19 | - | timeout time.Duration | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | func NewPassProvider() *PassProvider { | |
| 23 | - | return &PassProvider{ | |
| 24 | - | timeout: 10 * time.Second, | |
| 25 | - | } | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | func (p *PassProvider) Name() string { | |
| 29 | - | return "pass" | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | func (p *PassProvider) GetSecret(path string) (string, error) { | |
| 33 | - | ctx, cancel := context.WithTimeout(context.Background(), p.timeout) | |
| 34 | - | defer cancel() | |
| 35 | - | ||
| 36 | - | cmd := exec.CommandContext(ctx, "pass", "show", path) | |
| 37 | - | output, err := cmd.Output() | |
| 38 | - | if err != nil { | |
| 39 | - | if exitError, ok := err.(*exec.ExitError); ok { | |
| 40 | - | return "", fmt.Errorf("pass command failed: %s", string(exitError.Stderr)) | |
| 41 | - | } | |
| 42 | - | return "", fmt.Errorf("failed to execute pass command: %w", err) | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | secret := strings.TrimSpace(string(output)) | |
| 46 | - | if secret == "" { | |
| 47 | - | return "", fmt.Errorf("empty secret returned from pass for path: %s", path) | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | log.Debug(). | |
| 51 | - | Str("path", path). | |
| 52 | - | Msg("Successfully retrieved secret from pass") | |
| 53 | - | ||
| 54 | - | return secret, nil | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | type SopsProvider struct { | |
| 58 | - | timeout time.Duration | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | func NewSopsProvider() *SopsProvider { | |
| 62 | - | return &SopsProvider{ | |
| 63 | - | timeout: 10 * time.Second, | |
| 64 | - | } | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | func (s *SopsProvider) Name() string { | |
| 68 | - | return "sops" | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | func (s *SopsProvider) GetSecret(path string) (string, error) { | |
| 72 | - | ctx, cancel := context.WithTimeout(context.Background(), s.timeout) | |
| 73 | - | defer cancel() | |
| 74 | - | ||
| 75 | - | // Extract key from path format: file.yaml:key.nested.path | |
| 76 | - | parts := strings.SplitN(path, ":", 2) | |
| 77 | - | if len(parts) != 2 { | |
| 78 | - | return "", fmt.Errorf("sops path must be in format 'file:key', got: %s", path) | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | filePath := parts[0] | |
| 82 | - | keyPath := parts[1] | |
| 83 | - | ||
| 84 | - | // Use sops -d --extract to get specific key | |
| 85 | - | cmd := exec.CommandContext(ctx, "sops", "-d", "--extract", fmt.Sprintf("[\"%s\"]", keyPath), filePath) | |
| 86 | - | output, err := cmd.Output() | |
| 87 | - | if err != nil { | |
| 88 | - | if exitError, ok := err.(*exec.ExitError); ok { | |
| 89 | - | return "", fmt.Errorf("sops command failed: %s", string(exitError.Stderr)) | |
| 90 | - | } | |
| 91 | - | return "", fmt.Errorf("failed to execute sops command: %w", err) | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | secret := strings.TrimSpace(string(output)) | |
| 95 | - | if secret == "" { | |
| 96 | - | return "", fmt.Errorf("empty secret returned from sops for path: %s", path) | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | log.Debug(). | |
| 100 | - | Str("path", path). | |
| 101 | - | Msg("Successfully retrieved secret from sops") | |
| 102 | - | ||
| 103 | - | return secret, nil | |
| 104 | - | }⏎̸ | |
D internal/env/providers_test.go +0 -248
| @@ -1,248 +0,0 @@ | |||
| 1 | - | package env | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "os/exec" | |
| 6 | - | "testing" | |
| 7 | - | "time" | |
| 8 | - | ||
| 9 | - | "github.com/stretchr/testify/assert" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | func TestNewPassProvider(t *testing.T) { | |
| 13 | - | provider := NewPassProvider() | |
| 14 | - | assert.NotNil(t, provider) | |
| 15 | - | assert.Equal(t, "pass", provider.Name()) | |
| 16 | - | assert.Equal(t, 10*time.Second, provider.timeout) | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | func TestPassProvider_Name(t *testing.T) { | |
| 20 | - | provider := NewPassProvider() | |
| 21 | - | assert.Equal(t, "pass", provider.Name()) | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | func TestPassProvider_GetSecret(t *testing.T) { | |
| 25 | - | provider := NewPassProvider() | |
| 26 | - | ||
| 27 | - | tests := []struct { | |
| 28 | - | name string | |
| 29 | - | path string | |
| 30 | - | mockCommand bool | |
| 31 | - | mockOutput string | |
| 32 | - | mockError bool | |
| 33 | - | expectedSecret string | |
| 34 | - | expectedError bool | |
| 35 | - | }{ | |
| 36 | - | { | |
| 37 | - | name: "successful secret retrieval", | |
| 38 | - | path: "test/secret", | |
| 39 | - | mockCommand: true, | |
| 40 | - | mockOutput: "secret-value\n", | |
| 41 | - | expectedSecret: "secret-value", | |
| 42 | - | }, | |
| 43 | - | { | |
| 44 | - | name: "command execution fails", | |
| 45 | - | path: "test/nonexistent", | |
| 46 | - | mockCommand: true, | |
| 47 | - | mockError: true, | |
| 48 | - | expectedError: true, | |
| 49 | - | }, | |
| 50 | - | { | |
| 51 | - | name: "empty secret returned", | |
| 52 | - | path: "test/empty", | |
| 53 | - | mockCommand: true, | |
| 54 | - | mockOutput: "\n", | |
| 55 | - | expectedError: true, | |
| 56 | - | }, | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | for _, tt := range tests { | |
| 60 | - | t.Run(tt.name, func(t *testing.T) { | |
| 61 | - | // Check if pass command is available | |
| 62 | - | _, err := exec.LookPath("pass") | |
| 63 | - | if err != nil { | |
| 64 | - | t.Skip("pass command not available, skipping test") | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | // Since we can't easily mock exec.Command in this context, | |
| 68 | - | // we'll test the interface and structure, but skip actual execution | |
| 69 | - | // unless we have a real pass installation | |
| 70 | - | ||
| 71 | - | // Test the provider structure | |
| 72 | - | assert.Equal(t, "pass", provider.Name()) | |
| 73 | - | assert.NotZero(t, provider.timeout) | |
| 74 | - | ||
| 75 | - | // For actual testing, we would need a more sophisticated mocking approach | |
| 76 | - | // or integration tests with a real pass setup | |
| 77 | - | if !tt.mockCommand { | |
| 78 | - | secret, err := provider.GetSecret(tt.path) | |
| 79 | - | if tt.expectedError { | |
| 80 | - | assert.Error(t, err) | |
| 81 | - | } else { | |
| 82 | - | assert.NoError(t, err) | |
| 83 | - | assert.Equal(t, tt.expectedSecret, secret) | |
| 84 | - | } | |
| 85 | - | } | |
| 86 | - | }) | |
| 87 | - | } | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | func TestNewSopsProvider(t *testing.T) { | |
| 91 | - | provider := NewSopsProvider() | |
| 92 | - | assert.NotNil(t, provider) | |
| 93 | - | assert.Equal(t, "sops", provider.Name()) | |
| 94 | - | assert.Equal(t, 10*time.Second, provider.timeout) | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | func TestSopsProvider_Name(t *testing.T) { | |
| 98 | - | provider := NewSopsProvider() | |
| 99 | - | assert.Equal(t, "sops", provider.Name()) | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | func TestSopsProvider_GetSecret(t *testing.T) { | |
| 103 | - | provider := NewSopsProvider() | |
| 104 | - | ||
| 105 | - | tests := []struct { | |
| 106 | - | name string | |
| 107 | - | path string | |
| 108 | - | expectedSecret string | |
| 109 | - | expectedError bool | |
| 110 | - | }{ | |
| 111 | - | { | |
| 112 | - | name: "invalid path format - no colon", | |
| 113 | - | path: "invalid-path", | |
| 114 | - | expectedError: true, | |
| 115 | - | }, | |
| 116 | - | { | |
| 117 | - | name: "invalid path format - empty key", | |
| 118 | - | path: "file.yaml:", | |
| 119 | - | expectedError: true, | |
| 120 | - | }, | |
| 121 | - | { | |
| 122 | - | name: "invalid path format - empty file", | |
| 123 | - | path: ":key.path", | |
| 124 | - | expectedError: true, | |
| 125 | - | }, | |
| 126 | - | } | |
| 127 | - | ||
| 128 | - | for _, tt := range tests { | |
| 129 | - | t.Run(tt.name, func(t *testing.T) { | |
| 130 | - | // Check if sops command is available | |
| 131 | - | _, err := exec.LookPath("sops") | |
| 132 | - | if err != nil { | |
| 133 | - | t.Skip("sops command not available, skipping test") | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | // Test the provider structure | |
| 137 | - | assert.Equal(t, "sops", provider.Name()) | |
| 138 | - | assert.NotZero(t, provider.timeout) | |
| 139 | - | ||
| 140 | - | // Test path validation | |
| 141 | - | secret, err := provider.GetSecret(tt.path) | |
| 142 | - | if tt.expectedError { | |
| 143 | - | assert.Error(t, err) | |
| 144 | - | assert.Empty(t, secret) | |
| 145 | - | } else { | |
| 146 | - | // For successful cases, we'd need actual sops files | |
| 147 | - | // which is complex to set up in unit tests | |
| 148 | - | // This would be better tested in integration tests | |
| 149 | - | } | |
| 150 | - | }) | |
| 151 | - | } | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | func TestSopsProvider_GetSecret_ValidFormat(t *testing.T) { | |
| 155 | - | provider := NewSopsProvider() | |
| 156 | - | ||
| 157 | - | // Check if sops command is available | |
| 158 | - | _, err := exec.LookPath("sops") | |
| 159 | - | if err != nil { | |
| 160 | - | t.Skip("sops command not available, skipping test") | |
| 161 | - | } | |
| 162 | - | ||
| 163 | - | // Test with valid format but non-existent file (should fail gracefully) | |
| 164 | - | _, err = provider.GetSecret("nonexistent.yaml:some.key") | |
| 165 | - | assert.Error(t, err) | |
| 166 | - | assert.Contains(t, err.Error(), "sops command failed") | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | func TestSecretProvider_Interface(t *testing.T) { | |
| 170 | - | // Test that our providers implement the SecretProvider interface | |
| 171 | - | var _ SecretProvider = NewPassProvider() | |
| 172 | - | var _ SecretProvider = NewSopsProvider() | |
| 173 | - | } | |
| 174 | - | ||
| 175 | - | func TestProviders_Timeout(t *testing.T) { | |
| 176 | - | // Test that providers have reasonable timeouts | |
| 177 | - | passProvider := NewPassProvider() | |
| 178 | - | sopsProvider := NewSopsProvider() | |
| 179 | - | ||
| 180 | - | assert.Equal(t, 10*time.Second, passProvider.timeout) | |
| 181 | - | assert.Equal(t, 10*time.Second, sopsProvider.timeout) | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | func TestProviders_ContextCancellation(t *testing.T) { | |
| 185 | - | // Test that providers respect context cancellation | |
| 186 | - | passProvider := NewPassProvider() | |
| 187 | - | sopsProvider := NewSopsProvider() | |
| 188 | - | ||
| 189 | - | // Create a context that's already cancelled | |
| 190 | - | _, cancel := context.WithCancel(context.Background()) | |
| 191 | - | cancel() | |
| 192 | - | ||
| 193 | - | // Providers should handle cancelled contexts gracefully | |
| 194 | - | // (This is more of a behavioral test - the actual implementation | |
| 195 | - | // uses exec.CommandContext which should respect cancellation) | |
| 196 | - | assert.NotNil(t, passProvider) | |
| 197 | - | assert.NotNil(t, sopsProvider) | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | // Mock implementations for more controlled testing | |
| 201 | - | type MockPassProvider struct { | |
| 202 | - | output string | |
| 203 | - | err error | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | func (m *MockPassProvider) Name() string { | |
| 207 | - | return "pass" | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | func (m *MockPassProvider) GetSecret(path string) (string, error) { | |
| 211 | - | if m.err != nil { | |
| 212 | - | return "", m.err | |
| 213 | - | } | |
| 214 | - | return m.output, nil | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | type MockSopsProvider struct { | |
| 218 | - | output string | |
| 219 | - | err error | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | func (m *MockSopsProvider) Name() string { | |
| 223 | - | return "sops" | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | func (m *MockSopsProvider) GetSecret(path string) (string, error) { | |
| 227 | - | if m.err != nil { | |
| 228 | - | return "", m.err | |
| 229 | - | } | |
| 230 | - | return m.output, nil | |
| 231 | - | } | |
| 232 | - | ||
| 233 | - | func TestMockProviders(t *testing.T) { | |
| 234 | - | // Test our mock implementations | |
| 235 | - | mockPass := &MockPassProvider{output: "test-secret", err: nil} | |
| 236 | - | mockSops := &MockSopsProvider{output: "sops-secret", err: nil} | |
| 237 | - | ||
| 238 | - | assert.Equal(t, "pass", mockPass.Name()) | |
| 239 | - | assert.Equal(t, "sops", mockSops.Name()) | |
| 240 | - | ||
| 241 | - | secret, err := mockPass.GetSecret("test/path") | |
| 242 | - | assert.NoError(t, err) | |
| 243 | - | assert.Equal(t, "test-secret", secret) | |
| 244 | - | ||
| 245 | - | secret, err = mockSops.GetSecret("file.yaml:key") | |
| 246 | - | assert.NoError(t, err) | |
| 247 | - | assert.Equal(t, "sops-secret", secret) | |
| 248 | - | }⏎̸ | |
D internal/events/bus.go +0 -169
| @@ -1,169 +0,0 @@ | |||
| 1 | - | package events | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "sync" | |
| 7 | - | "time" | |
| 8 | - | ||
| 9 | - | "github.com/google/uuid" | |
| 10 | - | "github.com/rs/zerolog/log" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | type InMemoryEventBus struct { | |
| 14 | - | handlers []EventHandler | |
| 15 | - | eventChan chan Event | |
| 16 | - | done chan struct{} | |
| 17 | - | mu sync.RWMutex | |
| 18 | - | ctx context.Context | |
| 19 | - | cancel context.CancelFunc | |
| 20 | - | bufferSize int | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | func NewInMemoryEventBus(bufferSize int) *InMemoryEventBus { | |
| 24 | - | if bufferSize <= 0 { | |
| 25 | - | bufferSize = 100 | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 29 | - | ||
| 30 | - | return &InMemoryEventBus{ | |
| 31 | - | handlers: make([]EventHandler, 0), | |
| 32 | - | eventChan: make(chan Event, bufferSize), | |
| 33 | - | done: make(chan struct{}), | |
| 34 | - | ctx: ctx, | |
| 35 | - | cancel: cancel, | |
| 36 | - | bufferSize: bufferSize, | |
| 37 | - | } | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | func (bus *InMemoryEventBus) Publish(eventType EventType, payload interface{}) error { | |
| 41 | - | event := Event{ | |
| 42 | - | ID: uuid.New().String(), | |
| 43 | - | Type: eventType, | |
| 44 | - | Timestamp: time.Now(), | |
| 45 | - | Data: payload, | |
| 46 | - | } | |
| 47 | - | ||
| 48 | - | // Add specific fields for known event types | |
| 49 | - | switch p := payload.(type) { | |
| 50 | - | case ImagePushedPayload: | |
| 51 | - | event.ImageName = p.Name | |
| 52 | - | event.Tag = p.Reference | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | select { | |
| 56 | - | case bus.eventChan <- event: | |
| 57 | - | log.Debug(). | |
| 58 | - | Str("event_id", event.ID). | |
| 59 | - | Str("event_type", string(event.Type)). | |
| 60 | - | Str("image_name", event.ImageName). | |
| 61 | - | Str("tag", event.Tag). | |
| 62 | - | Msg("Event published") | |
| 63 | - | return nil | |
| 64 | - | case <-bus.ctx.Done(): | |
| 65 | - | return fmt.Errorf("event bus is stopped") | |
| 66 | - | default: | |
| 67 | - | return fmt.Errorf("event channel is full, dropping event %s", event.ID) | |
| 68 | - | } | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | func (bus *InMemoryEventBus) Subscribe(handler EventHandler) error { | |
| 72 | - | bus.mu.Lock() | |
| 73 | - | defer bus.mu.Unlock() | |
| 74 | - | ||
| 75 | - | bus.handlers = append(bus.handlers, handler) | |
| 76 | - | log.Debug(). | |
| 77 | - | Str("handler_type", fmt.Sprintf("%T", handler)). | |
| 78 | - | Int("total_handlers", len(bus.handlers)). | |
| 79 | - | Msg("Event handler subscribed") | |
| 80 | - | ||
| 81 | - | return nil | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | func (bus *InMemoryEventBus) Unsubscribe(handler EventHandler) error { | |
| 85 | - | bus.mu.Lock() | |
| 86 | - | defer bus.mu.Unlock() | |
| 87 | - | ||
| 88 | - | for i, h := range bus.handlers { | |
| 89 | - | if h == handler { | |
| 90 | - | bus.handlers = append(bus.handlers[:i], bus.handlers[i+1:]...) | |
| 91 | - | log.Debug(). | |
| 92 | - | Str("handler_type", fmt.Sprintf("%T", handler)). | |
| 93 | - | Int("total_handlers", len(bus.handlers)). | |
| 94 | - | Msg("Event handler unsubscribed") | |
| 95 | - | return nil | |
| 96 | - | } | |
| 97 | - | } | |
| 98 | - | ||
| 99 | - | return fmt.Errorf("handler not found") | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | func (bus *InMemoryEventBus) Start() error { | |
| 103 | - | log.Info(). | |
| 104 | - | Int("buffer_size", bus.bufferSize). | |
| 105 | - | Msg("Starting event bus") | |
| 106 | - | ||
| 107 | - | go bus.processEvents() | |
| 108 | - | return nil | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | func (bus *InMemoryEventBus) Stop() error { | |
| 112 | - | log.Info().Msg("Stopping event bus") | |
| 113 | - | ||
| 114 | - | bus.cancel() | |
| 115 | - | ||
| 116 | - | select { | |
| 117 | - | case <-bus.done: | |
| 118 | - | log.Info().Msg("Event bus stopped") | |
| 119 | - | return nil | |
| 120 | - | case <-time.After(5 * time.Second): | |
| 121 | - | log.Warn().Msg("Event bus stop timeout") | |
| 122 | - | return fmt.Errorf("timeout waiting for event bus to stop") | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | func (bus *InMemoryEventBus) processEvents() { | |
| 127 | - | defer close(bus.done) | |
| 128 | - | ||
| 129 | - | for { | |
| 130 | - | select { | |
| 131 | - | case event := <-bus.eventChan: | |
| 132 | - | bus.handleEvent(event) | |
| 133 | - | case <-bus.ctx.Done(): | |
| 134 | - | log.Debug().Msg("Event bus processing stopped") | |
| 135 | - | return | |
| 136 | - | } | |
| 137 | - | } | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | func (bus *InMemoryEventBus) handleEvent(event Event) { | |
| 141 | - | bus.mu.RLock() | |
| 142 | - | handlers := make([]EventHandler, len(bus.handlers)) | |
| 143 | - | copy(handlers, bus.handlers) | |
| 144 | - | bus.mu.RUnlock() | |
| 145 | - | ||
| 146 | - | for _, handler := range handlers { | |
| 147 | - | if handler.CanHandle(event.Type) { | |
| 148 | - | go func(h EventHandler) { | |
| 149 | - | start := time.Now() | |
| 150 | - | ||
| 151 | - | if err := h.Handle(event); err != nil { | |
| 152 | - | log.Error(). | |
| 153 | - | Err(err). | |
| 154 | - | Str("event_id", event.ID). | |
| 155 | - | Str("event_type", string(event.Type)). | |
| 156 | - | Str("handler_type", fmt.Sprintf("%T", h)). | |
| 157 | - | Msg("Error handling event") | |
| 158 | - | } else { | |
| 159 | - | log.Debug(). | |
| 160 | - | Str("event_id", event.ID). | |
| 161 | - | Str("event_type", string(event.Type)). | |
| 162 | - | Str("handler_type", fmt.Sprintf("%T", h)). | |
| 163 | - | Dur("duration", time.Since(start)). | |
| 164 | - | Msg("Event handled successfully") | |
| 165 | - | } | |
| 166 | - | }(handler) | |
| 167 | - | } | |
| 168 | - | } | |
| 169 | - | } | |
D internal/events/bus_test.go +0 -337
| @@ -1,337 +0,0 @@ | |||
| 1 | - | package events | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "sync" | |
| 5 | - | "testing" | |
| 6 | - | "time" | |
| 7 | - | ||
| 8 | - | "github.com/stretchr/testify/assert" | |
| 9 | - | "github.com/stretchr/testify/mock" | |
| 10 | - | "github.com/stretchr/testify/require" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | // MockEventHandler is a mock implementation of EventHandler | |
| 14 | - | type MockEventHandler struct { | |
| 15 | - | mock.Mock | |
| 16 | - | mu sync.Mutex | |
| 17 | - | events []Event | |
| 18 | - | canHandle bool | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | func (m *MockEventHandler) Handle(event Event) error { | |
| 22 | - | m.mu.Lock() | |
| 23 | - | defer m.mu.Unlock() | |
| 24 | - | m.events = append(m.events, event) | |
| 25 | - | args := m.Called(event) | |
| 26 | - | return args.Error(0) | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | func (m *MockEventHandler) CanHandle(eventType EventType) bool { | |
| 30 | - | args := m.Called(eventType) | |
| 31 | - | return args.Bool(0) | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | func (m *MockEventHandler) GetReceivedEvents() []Event { | |
| 35 | - | m.mu.Lock() | |
| 36 | - | defer m.mu.Unlock() | |
| 37 | - | return append([]Event{}, m.events...) | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | func TestInMemoryEventBus_NewInMemoryEventBus(t *testing.T) { | |
| 41 | - | bus := NewInMemoryEventBus(50) | |
| 42 | - | ||
| 43 | - | assert.NotNil(t, bus) | |
| 44 | - | assert.Equal(t, 50, cap(bus.eventChan)) | |
| 45 | - | assert.Empty(t, bus.handlers) | |
| 46 | - | assert.Equal(t, 50, bus.bufferSize) | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | func TestInMemoryEventBus_Subscribe(t *testing.T) { | |
| 50 | - | bus := NewInMemoryEventBus(10) | |
| 51 | - | handler := &MockEventHandler{} | |
| 52 | - | ||
| 53 | - | bus.Subscribe(handler) | |
| 54 | - | ||
| 55 | - | assert.Len(t, bus.handlers, 1) | |
| 56 | - | assert.Contains(t, bus.handlers, handler) | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | func TestInMemoryEventBus_Subscribe_DuplicateHandler(t *testing.T) { | |
| 60 | - | bus := NewInMemoryEventBus(10) | |
| 61 | - | handler := &MockEventHandler{} | |
| 62 | - | ||
| 63 | - | bus.Subscribe(handler) | |
| 64 | - | bus.Subscribe(handler) // Subscribe same handler twice | |
| 65 | - | ||
| 66 | - | // Implementation allows duplicates (handlers are appended to slice) | |
| 67 | - | assert.Len(t, bus.handlers, 2) | |
| 68 | - | } | |
| 69 | - | ||
| 70 | - | func TestInMemoryEventBus_Unsubscribe(t *testing.T) { | |
| 71 | - | bus := NewInMemoryEventBus(10) | |
| 72 | - | handler1 := &MockEventHandler{} | |
| 73 | - | handler2 := &MockEventHandler{} | |
| 74 | - | ||
| 75 | - | bus.Subscribe(handler1) | |
| 76 | - | bus.Subscribe(handler2) | |
| 77 | - | assert.Len(t, bus.handlers, 2) | |
| 78 | - | ||
| 79 | - | err := bus.Unsubscribe(handler1) | |
| 80 | - | ||
| 81 | - | assert.NoError(t, err) | |
| 82 | - | assert.Len(t, bus.handlers, 1) | |
| 83 | - | // Check that handler2 is still in the list (compare by position) | |
| 84 | - | assert.Equal(t, handler2, bus.handlers[0]) | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | func TestInMemoryEventBus_Unsubscribe_NonExistentHandler(t *testing.T) { | |
| 88 | - | bus := NewInMemoryEventBus(10) | |
| 89 | - | handler1 := &MockEventHandler{} | |
| 90 | - | handler2 := &MockEventHandler{} | |
| 91 | - | ||
| 92 | - | bus.Subscribe(handler1) | |
| 93 | - | err := bus.Unsubscribe(handler2) // Try to unsubscribe handler that wasn't subscribed | |
| 94 | - | ||
| 95 | - | assert.Error(t, err) | |
| 96 | - | assert.Contains(t, err.Error(), "handler not found") | |
| 97 | - | assert.Len(t, bus.handlers, 1) | |
| 98 | - | assert.Equal(t, handler1, bus.handlers[0]) | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | func TestInMemoryEventBus_StartStop(t *testing.T) { | |
| 102 | - | bus := NewInMemoryEventBus(10) | |
| 103 | - | ||
| 104 | - | // Start the bus | |
| 105 | - | err := bus.Start() | |
| 106 | - | assert.NoError(t, err) | |
| 107 | - | ||
| 108 | - | // Stop the bus | |
| 109 | - | err = bus.Stop() | |
| 110 | - | assert.NoError(t, err) | |
| 111 | - | } | |
| 112 | - | ||
| 113 | - | func TestInMemoryEventBus_Publish_Running(t *testing.T) { | |
| 114 | - | bus := NewInMemoryEventBus(10) | |
| 115 | - | ||
| 116 | - | handler := &MockEventHandler{} | |
| 117 | - | handler.On("CanHandle", ImagePushed).Return(true) | |
| 118 | - | handler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 119 | - | ||
| 120 | - | bus.Subscribe(handler) | |
| 121 | - | bus.Start() | |
| 122 | - | defer bus.Stop() | |
| 123 | - | ||
| 124 | - | payload := ImagePushedPayload{ | |
| 125 | - | Name: "nginx:latest", | |
| 126 | - | Reference: "latest", | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | bus.Publish(ImagePushed, payload) | |
| 130 | - | ||
| 131 | - | // Give some time for async processing | |
| 132 | - | time.Sleep(50 * time.Millisecond) | |
| 133 | - | ||
| 134 | - | handler.AssertExpectations(t) | |
| 135 | - | events := handler.GetReceivedEvents() | |
| 136 | - | require.Len(t, events, 1) | |
| 137 | - | assert.Equal(t, ImagePushed, events[0].Type) | |
| 138 | - | assert.Equal(t, "nginx:latest", events[0].ImageName) | |
| 139 | - | assert.Equal(t, "latest", events[0].Tag) | |
| 140 | - | } | |
| 141 | - | ||
| 142 | - | func TestInMemoryEventBus_Publish_NotRunning(t *testing.T) { | |
| 143 | - | bus := NewInMemoryEventBus(10) | |
| 144 | - | ||
| 145 | - | handler := &MockEventHandler{} | |
| 146 | - | bus.Subscribe(handler) | |
| 147 | - | ||
| 148 | - | payload := ImagePushedPayload{ | |
| 149 | - | Name: "nginx:latest", | |
| 150 | - | Reference: "latest", | |
| 151 | - | } | |
| 152 | - | ||
| 153 | - | // Publish when bus is not running | |
| 154 | - | bus.Publish(ImagePushed, payload) | |
| 155 | - | ||
| 156 | - | // Give some time in case it processes (it shouldn't) | |
| 157 | - | time.Sleep(10 * time.Millisecond) | |
| 158 | - | ||
| 159 | - | // Handler should not have received any events | |
| 160 | - | events := handler.GetReceivedEvents() | |
| 161 | - | assert.Empty(t, events) | |
| 162 | - | } | |
| 163 | - | ||
| 164 | - | func TestInMemoryEventBus_HandlerFiltering(t *testing.T) { | |
| 165 | - | bus := NewInMemoryEventBus(10) | |
| 166 | - | ||
| 167 | - | // Handler that only handles ImagePushed events | |
| 168 | - | imagePushedHandler := &MockEventHandler{} | |
| 169 | - | imagePushedHandler.On("CanHandle", ImagePushed).Return(true) | |
| 170 | - | imagePushedHandler.On("CanHandle", ConfigReload).Return(false) | |
| 171 | - | imagePushedHandler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 172 | - | ||
| 173 | - | // Handler that only handles ConfigReload events | |
| 174 | - | configReloadHandler := &MockEventHandler{} | |
| 175 | - | configReloadHandler.On("CanHandle", ImagePushed).Return(false) | |
| 176 | - | configReloadHandler.On("CanHandle", ConfigReload).Return(true) | |
| 177 | - | configReloadHandler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 178 | - | ||
| 179 | - | bus.Subscribe(imagePushedHandler) | |
| 180 | - | bus.Subscribe(configReloadHandler) | |
| 181 | - | bus.Start() | |
| 182 | - | defer bus.Stop() | |
| 183 | - | ||
| 184 | - | // Publish ImagePushed event | |
| 185 | - | bus.Publish(ImagePushed, ImagePushedPayload{ | |
| 186 | - | Name: "nginx:latest", | |
| 187 | - | Reference: "latest", | |
| 188 | - | }) | |
| 189 | - | ||
| 190 | - | // Publish ConfigReload event | |
| 191 | - | bus.Publish(ConfigReload, nil) | |
| 192 | - | ||
| 193 | - | // Give time for processing | |
| 194 | - | time.Sleep(20 * time.Millisecond) | |
| 195 | - | ||
| 196 | - | // Check that each handler only received appropriate events | |
| 197 | - | imagePushedEvents := imagePushedHandler.GetReceivedEvents() | |
| 198 | - | configReloadEvents := configReloadHandler.GetReceivedEvents() | |
| 199 | - | ||
| 200 | - | require.Len(t, imagePushedEvents, 1) | |
| 201 | - | assert.Equal(t, ImagePushed, imagePushedEvents[0].Type) | |
| 202 | - | ||
| 203 | - | require.Len(t, configReloadEvents, 1) | |
| 204 | - | assert.Equal(t, ConfigReload, configReloadEvents[0].Type) | |
| 205 | - | ||
| 206 | - | imagePushedHandler.AssertExpectations(t) | |
| 207 | - | configReloadHandler.AssertExpectations(t) | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | func TestInMemoryEventBus_HandlerError(t *testing.T) { | |
| 211 | - | bus := NewInMemoryEventBus(10) | |
| 212 | - | ||
| 213 | - | // Handler that returns an error | |
| 214 | - | errorHandler := &MockEventHandler{} | |
| 215 | - | errorHandler.On("CanHandle", ImagePushed).Return(true) | |
| 216 | - | errorHandler.On("Handle", mock.AnythingOfType("Event")).Return(assert.AnError) | |
| 217 | - | ||
| 218 | - | // Handler that succeeds | |
| 219 | - | successHandler := &MockEventHandler{} | |
| 220 | - | successHandler.On("CanHandle", ImagePushed).Return(true) | |
| 221 | - | successHandler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 222 | - | ||
| 223 | - | bus.Subscribe(errorHandler) | |
| 224 | - | bus.Subscribe(successHandler) | |
| 225 | - | bus.Start() | |
| 226 | - | defer bus.Stop() | |
| 227 | - | ||
| 228 | - | payload := ImagePushedPayload{ | |
| 229 | - | Name: "nginx:latest", | |
| 230 | - | Reference: "latest", | |
| 231 | - | } | |
| 232 | - | ||
| 233 | - | bus.Publish(ImagePushed, payload) | |
| 234 | - | ||
| 235 | - | // Give time for processing | |
| 236 | - | time.Sleep(20 * time.Millisecond) | |
| 237 | - | ||
| 238 | - | // Both handlers should have been called despite error | |
| 239 | - | errorHandler.AssertExpectations(t) | |
| 240 | - | successHandler.AssertExpectations(t) | |
| 241 | - | ||
| 242 | - | // Both should have received the event | |
| 243 | - | errorEvents := errorHandler.GetReceivedEvents() | |
| 244 | - | successEvents := successHandler.GetReceivedEvents() | |
| 245 | - | ||
| 246 | - | assert.Len(t, errorEvents, 1) | |
| 247 | - | assert.Len(t, successEvents, 1) | |
| 248 | - | } | |
| 249 | - | ||
| 250 | - | func TestInMemoryEventBus_ContextCancellation(t *testing.T) { | |
| 251 | - | bus := NewInMemoryEventBus(10) | |
| 252 | - | ||
| 253 | - | handler := &MockEventHandler{} | |
| 254 | - | handler.On("CanHandle", ImagePushed).Return(true) | |
| 255 | - | handler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 256 | - | ||
| 257 | - | bus.Subscribe(handler) | |
| 258 | - | bus.Start() | |
| 259 | - | ||
| 260 | - | // Stop the bus to trigger shutdown | |
| 261 | - | err := bus.Stop() | |
| 262 | - | assert.NoError(t, err) | |
| 263 | - | } | |
| 264 | - | ||
| 265 | - | func TestInMemoryEventBus_FullChannel(t *testing.T) { | |
| 266 | - | // Create bus with very small buffer | |
| 267 | - | bus := NewInMemoryEventBus(1) | |
| 268 | - | ||
| 269 | - | handler := &MockEventHandler{} | |
| 270 | - | handler.On("CanHandle", ImagePushed).Return(true) | |
| 271 | - | // Make handler slow to process events | |
| 272 | - | handler.On("Handle", mock.AnythingOfType("Event")).Return(nil).Run(func(args mock.Arguments) { | |
| 273 | - | time.Sleep(50 * time.Millisecond) | |
| 274 | - | }) | |
| 275 | - | ||
| 276 | - | bus.Subscribe(handler) | |
| 277 | - | bus.Start() | |
| 278 | - | defer bus.Stop() | |
| 279 | - | ||
| 280 | - | payload := ImagePushedPayload{ | |
| 281 | - | Name: "nginx:latest", | |
| 282 | - | Reference: "latest", | |
| 283 | - | } | |
| 284 | - | ||
| 285 | - | // Publish multiple events quickly to potentially fill the channel | |
| 286 | - | for i := 0; i < 5; i++ { | |
| 287 | - | bus.Publish(ImagePushed, payload) | |
| 288 | - | } | |
| 289 | - | ||
| 290 | - | // Give time for processing | |
| 291 | - | time.Sleep(200 * time.Millisecond) | |
| 292 | - | ||
| 293 | - | // Should still work, events might be dropped but no crash | |
| 294 | - | events := handler.GetReceivedEvents() | |
| 295 | - | assert.True(t, len(events) >= 1) // At least some events should be processed | |
| 296 | - | } | |
| 297 | - | ||
| 298 | - | func TestInMemoryEventBus_ConcurrentPublish(t *testing.T) { | |
| 299 | - | bus := NewInMemoryEventBus(100) | |
| 300 | - | ||
| 301 | - | handler := &MockEventHandler{} | |
| 302 | - | handler.On("CanHandle", ImagePushed).Return(true) | |
| 303 | - | handler.On("Handle", mock.AnythingOfType("Event")).Return(nil) | |
| 304 | - | ||
| 305 | - | bus.Subscribe(handler) | |
| 306 | - | bus.Start() | |
| 307 | - | defer bus.Stop() | |
| 308 | - | ||
| 309 | - | payload := ImagePushedPayload{ | |
| 310 | - | Name: "nginx:latest", | |
| 311 | - | Reference: "latest", | |
| 312 | - | } | |
| 313 | - | ||
| 314 | - | // Publish events concurrently from multiple goroutines | |
| 315 | - | numGoroutines := 10 | |
| 316 | - | eventsPerGoroutine := 5 | |
| 317 | - | var wg sync.WaitGroup | |
| 318 | - | ||
| 319 | - | for i := 0; i < numGoroutines; i++ { | |
| 320 | - | wg.Add(1) | |
| 321 | - | go func() { | |
| 322 | - | defer wg.Done() | |
| 323 | - | for j := 0; j < eventsPerGoroutine; j++ { | |
| 324 | - | bus.Publish(ImagePushed, payload) | |
| 325 | - | } | |
| 326 | - | }() | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | wg.Wait() | |
| 330 | - | ||
| 331 | - | // Give time for processing | |
| 332 | - | time.Sleep(100 * time.Millisecond) | |
| 333 | - | ||
| 334 | - | events := handler.GetReceivedEvents() | |
| 335 | - | expectedEvents := numGoroutines * eventsPerGoroutine | |
| 336 | - | assert.Equal(t, expectedEvents, len(events)) | |
| 337 | - | }⏎̸ | |
D internal/events/handlers.go +0 -393
| @@ -1,393 +0,0 @@ | |||
| 1 | - | package events | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "fmt" | |
| 6 | - | "strings" | |
| 7 | - | ||
| 8 | - | "github.com/rs/zerolog/log" | |
| 9 | - | "gordon/internal/config" | |
| 10 | - | "gordon/internal/container" | |
| 11 | - | "gordon/pkg/runtime" | |
| 12 | - | ) | |
| 13 | - | ||
| 14 | - | type ContainerEventHandler struct { | |
| 15 | - | manager container.ManagerInterface | |
| 16 | - | config *config.Config | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | func NewContainerEventHandler(manager container.ManagerInterface, config *config.Config) *ContainerEventHandler { | |
| 20 | - | return &ContainerEventHandler{ | |
| 21 | - | manager: manager, | |
| 22 | - | config: config, | |
| 23 | - | } | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | func (h *ContainerEventHandler) CanHandle(eventType EventType) bool { | |
| 27 | - | switch eventType { | |
| 28 | - | case ImagePushed, ConfigReload, ManualReload, ContainerStop, ContainerStart: | |
| 29 | - | return true | |
| 30 | - | default: | |
| 31 | - | return false | |
| 32 | - | } | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | func (h *ContainerEventHandler) Handle(event Event) error { | |
| 36 | - | switch event.Type { | |
| 37 | - | case ImagePushed: | |
| 38 | - | return h.handleImagePushed(event) | |
| 39 | - | case ConfigReload: | |
| 40 | - | return h.handleConfigReload(event) | |
| 41 | - | case ManualReload: | |
| 42 | - | return h.handleManualReload(event) | |
| 43 | - | case ContainerStop: | |
| 44 | - | return h.handleContainerStop(event) | |
| 45 | - | case ContainerStart: | |
| 46 | - | return h.handleContainerStart(event) | |
| 47 | - | default: | |
| 48 | - | return fmt.Errorf("unsupported event type: %s", event.Type) | |
| 49 | - | } | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | func (h *ContainerEventHandler) handleImagePushed(event Event) error { | |
| 53 | - | imageName := event.ImageName | |
| 54 | - | tag := event.Tag | |
| 55 | - | ||
| 56 | - | if imageName == "" { | |
| 57 | - | return fmt.Errorf("image name is required for ImagePushed event") | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | if tag == "" { | |
| 61 | - | tag = "latest" | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | fullImageName := fmt.Sprintf("%s:%s", imageName, tag) | |
| 65 | - | ||
| 66 | - | log.Info(). | |
| 67 | - | Str("image", fullImageName). | |
| 68 | - | Str("event_id", event.ID). | |
| 69 | - | Msg("Processing image push event") | |
| 70 | - | ||
| 71 | - | routes := h.findRoutesForImage(fullImageName) | |
| 72 | - | if len(routes) == 0 { | |
| 73 | - | log.Debug(). | |
| 74 | - | Str("image", fullImageName). | |
| 75 | - | Msg("No routes configured for pushed image") | |
| 76 | - | return nil | |
| 77 | - | } | |
| 78 | - | ||
| 79 | - | for _, route := range routes { | |
| 80 | - | if err := h.deployContainerForRoute(route, fullImageName); err != nil { | |
| 81 | - | log.Error(). | |
| 82 | - | Err(err). | |
| 83 | - | Str("route", route). | |
| 84 | - | Str("image", fullImageName). | |
| 85 | - | Msg("Failed to deploy container for route") | |
| 86 | - | } | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | return nil | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | func (h *ContainerEventHandler) handleConfigReload(event Event) error { | |
| 93 | - | log.Info(). | |
| 94 | - | Str("event_id", event.ID). | |
| 95 | - | Msg("Processing configuration reload event") | |
| 96 | - | ||
| 97 | - | ctx := context.Background() | |
| 98 | - | ||
| 99 | - | // Get current containers managed by Gordon | |
| 100 | - | currentContainers := h.manager.ListContainers() | |
| 101 | - | ||
| 102 | - | // Track which routes are currently active | |
| 103 | - | activeRoutes := make(map[string]*runtime.Container) | |
| 104 | - | for _, container := range currentContainers { | |
| 105 | - | if route, exists := container.Labels["gordon.route"]; exists { | |
| 106 | - | activeRoutes[route] = container | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // Process each configured route | |
| 111 | - | for route, imageName := range h.config.Routes { | |
| 112 | - | if container, exists := activeRoutes[route]; exists { | |
| 113 | - | // Route exists - check if image has changed | |
| 114 | - | currentImage := container.Labels["gordon.image"] | |
| 115 | - | if currentImage != imageName { | |
| 116 | - | log.Info(). | |
| 117 | - | Str("route", route). | |
| 118 | - | Str("old_image", currentImage). | |
| 119 | - | Str("new_image", imageName). | |
| 120 | - | Msg("Image changed for route, redeploying") | |
| 121 | - | ||
| 122 | - | if err := h.deployContainerForRoute(route, imageName); err != nil { | |
| 123 | - | log.Error(). | |
| 124 | - | Err(err). | |
| 125 | - | Str("route", route). | |
| 126 | - | Msg("Failed to redeploy container for route") | |
| 127 | - | } | |
| 128 | - | } | |
| 129 | - | // Remove from active routes so we know it's been processed | |
| 130 | - | delete(activeRoutes, route) | |
| 131 | - | } else { | |
| 132 | - | // New route - deploy container | |
| 133 | - | log.Info(). | |
| 134 | - | Str("route", route). | |
| 135 | - | Str("image", imageName). | |
| 136 | - | Msg("New route detected, deploying container") | |
| 137 | - | ||
| 138 | - | if err := h.deployContainerForRoute(route, imageName); err != nil { | |
| 139 | - | log.Error(). | |
| 140 | - | Err(err). | |
| 141 | - | Str("route", route). | |
| 142 | - | Msg("Failed to deploy container for new route") | |
| 143 | - | } | |
| 144 | - | } | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | // Stop containers for routes that are no longer configured | |
| 148 | - | for route, container := range activeRoutes { | |
| 149 | - | log.Info(). | |
| 150 | - | Str("route", route). | |
| 151 | - | Str("container_id", container.ID). | |
| 152 | - | Msg("Route no longer configured, stopping container") | |
| 153 | - | ||
| 154 | - | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |
| 155 | - | log.Error(). | |
| 156 | - | Err(err). | |
| 157 | - | Str("container_id", container.ID). | |
| 158 | - | Msg("Failed to stop container for removed route") | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 162 | - | log.Error(). | |
| 163 | - | Err(err). | |
| 164 | - | Str("container_id", container.ID). | |
| 165 | - | Msg("Failed to remove container for removed route") | |
| 166 | - | } | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | return nil | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | func (h *ContainerEventHandler) handleManualReload(event Event) error { | |
| 173 | - | log.Info(). | |
| 174 | - | Str("event_id", event.ID). | |
| 175 | - | Msg("Processing manual reload event - redeploying all containers with updated environment variables") | |
| 176 | - | ||
| 177 | - | ctx := context.Background() | |
| 178 | - | routes := h.config.GetRoutes() | |
| 179 | - | redeployedCount := 0 | |
| 180 | - | var errors []error | |
| 181 | - | ||
| 182 | - | // Redeploy each container to pick up new environment variables | |
| 183 | - | for _, route := range routes { | |
| 184 | - | // Check if container exists for this route | |
| 185 | - | if _, exists := h.manager.GetContainer(route.Domain); !exists { | |
| 186 | - | log.Debug().Str("domain", route.Domain).Msg("No container found for route, skipping") | |
| 187 | - | continue | |
| 188 | - | } | |
| 189 | - | ||
| 190 | - | log.Info().Str("domain", route.Domain).Str("image", route.Image).Msg("Redeploying container with updated environment variables") | |
| 191 | - | ||
| 192 | - | // Deploy container (this will stop the old one and start a new one with updated env vars) | |
| 193 | - | _, err := h.manager.DeployContainer(ctx, route) | |
| 194 | - | if err != nil { | |
| 195 | - | log.Error().Str("domain", route.Domain).Err(err).Msg("Failed to redeploy container during manual reload") | |
| 196 | - | errors = append(errors, fmt.Errorf("failed to redeploy container for %s: %w", route.Domain, err)) | |
| 197 | - | continue | |
| 198 | - | } | |
| 199 | - | ||
| 200 | - | redeployedCount++ | |
| 201 | - | log.Info().Str("domain", route.Domain).Msg("Container redeployed successfully with updated environment variables") | |
| 202 | - | } | |
| 203 | - | ||
| 204 | - | if len(errors) > 0 { | |
| 205 | - | log.Warn().Int("redeployed", redeployedCount).Int("errors", len(errors)).Msg("Manual reload completed with some errors") | |
| 206 | - | return fmt.Errorf("manual reload completed with %d errors: %v", len(errors), errors) | |
| 207 | - | } | |
| 208 | - | ||
| 209 | - | log.Info().Int("redeployed", redeployedCount).Int("total", len(routes)).Msg("Manual reload completed successfully") | |
| 210 | - | return nil | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | func (h *ContainerEventHandler) handleContainerStop(event Event) error { | |
| 214 | - | containerID := event.ContainerID | |
| 215 | - | if containerID == "" { | |
| 216 | - | return fmt.Errorf("container ID is required for ContainerStop event") | |
| 217 | - | } | |
| 218 | - | ||
| 219 | - | log.Info(). | |
| 220 | - | Str("container_id", containerID). | |
| 221 | - | Str("event_id", event.ID). | |
| 222 | - | Msg("Processing container stop event") | |
| 223 | - | ||
| 224 | - | ctx := context.Background() | |
| 225 | - | if err := h.manager.StopContainer(ctx, containerID); err != nil { | |
| 226 | - | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 227 | - | } | |
| 228 | - | ||
| 229 | - | return nil | |
| 230 | - | } | |
| 231 | - | ||
| 232 | - | func (h *ContainerEventHandler) handleContainerStart(event Event) error { | |
| 233 | - | containerID := event.ContainerID | |
| 234 | - | if containerID == "" { | |
| 235 | - | return fmt.Errorf("container ID is required for ContainerStart event") | |
| 236 | - | } | |
| 237 | - | ||
| 238 | - | log.Info(). | |
| 239 | - | Str("container_id", containerID). | |
| 240 | - | Str("event_id", event.ID). | |
| 241 | - | Msg("Processing container start event") | |
| 242 | - | ||
| 243 | - | // StartContainer method doesn't exist on Manager - containers are started via DeployContainer | |
| 244 | - | // For manual start, we'd need to add this method or use the runtime directly | |
| 245 | - | return fmt.Errorf("container start not implemented - use automatic deployment instead") | |
| 246 | - | } | |
| 247 | - | ||
| 248 | - | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |
| 249 | - | var routes []string | |
| 250 | - | ||
| 251 | - | for route, configuredImage := range h.config.Routes { | |
| 252 | - | if strings.EqualFold(configuredImage, imageName) { | |
| 253 | - | routes = append(routes, route) | |
| 254 | - | } | |
| 255 | - | } | |
| 256 | - | ||
| 257 | - | return routes | |
| 258 | - | } | |
| 259 | - | ||
| 260 | - | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | |
| 261 | - | ctx := context.Background() | |
| 262 | - | ||
| 263 | - | containers := h.manager.ListContainers() | |
| 264 | - | ||
| 265 | - | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | |
| 266 | - | ||
| 267 | - | // Stop and remove existing container if it exists | |
| 268 | - | for _, container := range containers { | |
| 269 | - | if container.Name == containerName { | |
| 270 | - | log.Info(). | |
| 271 | - | Str("container_name", containerName). | |
| 272 | - | Str("old_container_id", container.ID). | |
| 273 | - | Msg("Stopping existing container for route") | |
| 274 | - | ||
| 275 | - | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |
| 276 | - | log.Warn(). | |
| 277 | - | Err(err). | |
| 278 | - | Str("container_id", container.ID). | |
| 279 | - | Msg("Failed to stop existing container") | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 283 | - | log.Warn(). | |
| 284 | - | Err(err). | |
| 285 | - | Str("container_id", container.ID). | |
| 286 | - | Msg("Failed to remove existing container") | |
| 287 | - | } | |
| 288 | - | } | |
| 289 | - | } | |
| 290 | - | ||
| 291 | - | // Use DeployContainer which handles creation and starting | |
| 292 | - | routeConfig := config.Route{ | |
| 293 | - | Domain: route, | |
| 294 | - | Image: imageName, | |
| 295 | - | HTTPS: true, | |
| 296 | - | } | |
| 297 | - | ||
| 298 | - | container, err := h.manager.DeployContainer(ctx, routeConfig) | |
| 299 | - | if err != nil { | |
| 300 | - | return fmt.Errorf("failed to deploy container for route %s: %w", route, err) | |
| 301 | - | } | |
| 302 | - | ||
| 303 | - | log.Info(). | |
| 304 | - | Str("route", route). | |
| 305 | - | Str("image", imageName). | |
| 306 | - | Str("container_id", container.ID). | |
| 307 | - | Str("container_name", containerName). | |
| 308 | - | Msg("Successfully deployed container for route") | |
| 309 | - | ||
| 310 | - | return nil | |
| 311 | - | } | |
| 312 | - | ||
| 313 | - | type AutoRouteHandler struct { | |
| 314 | - | config *config.Config | |
| 315 | - | manager container.ManagerInterface | |
| 316 | - | } | |
| 317 | - | ||
| 318 | - | func NewAutoRouteHandler(cfg *config.Config, manager container.ManagerInterface) *AutoRouteHandler { | |
| 319 | - | return &AutoRouteHandler{ | |
| 320 | - | config: cfg, | |
| 321 | - | manager: manager, | |
| 322 | - | } | |
| 323 | - | } | |
| 324 | - | ||
| 325 | - | func (h *AutoRouteHandler) CanHandle(eventType EventType) bool { | |
| 326 | - | return eventType == ImagePushed | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | func (h *AutoRouteHandler) Handle(event Event) error { | |
| 330 | - | if !h.config.AutoRoute.Enabled { | |
| 331 | - | return nil | |
| 332 | - | } | |
| 333 | - | ||
| 334 | - | if event.Type != ImagePushed { | |
| 335 | - | return nil | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | payload, ok := event.Data.(ImagePushedPayload) | |
| 339 | - | if !ok { | |
| 340 | - | return fmt.Errorf("invalid payload type for ImagePushed event") | |
| 341 | - | } | |
| 342 | - | ||
| 343 | - | // Log annotations if present | |
| 344 | - | if len(payload.Annotations) > 0 { | |
| 345 | - | log.Debug(). | |
| 346 | - | Str("image", payload.Name). | |
| 347 | - | Str("reference", payload.Reference). | |
| 348 | - | Interface("annotations", payload.Annotations). | |
| 349 | - | Msg("Image pushed with annotations - auto-route processing") | |
| 350 | - | } | |
| 351 | - | ||
| 352 | - | imageName := payload.Name | |
| 353 | - | reference := payload.Reference | |
| 354 | - | fullImageName := fmt.Sprintf("%s:%s", imageName, reference) | |
| 355 | - | ||
| 356 | - | // Build the full registry image path for deployment | |
| 357 | - | registryImageName := fullImageName | |
| 358 | - | if h.config.Server.RegistryDomain != "" { | |
| 359 | - | registryImageName = fmt.Sprintf("%s/%s", h.config.Server.RegistryDomain, fullImageName) | |
| 360 | - | } | |
| 361 | - | ||
| 362 | - | // Try to extract domain from the image name part (before the colon) | |
| 363 | - | domain, isDomain := config.ExtractDomainFromImageName(imageName) | |
| 364 | - | if !isDomain { | |
| 365 | - | log.Debug(). | |
| 366 | - | Str("image", imageName). | |
| 367 | - | Msg("Image name does not contain a valid domain, skipping auto-route creation") | |
| 368 | - | return nil | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | // Check if route already exists | |
| 372 | - | if _, exists := h.config.Routes[domain]; exists { | |
| 373 | - | log.Debug(). | |
| 374 | - | Str("domain", domain). | |
| 375 | - | Str("existing_image", h.config.Routes[domain]). | |
| 376 | - | Str("new_image", fullImageName). | |
| 377 | - | Msg("Route already exists for domain, skipping auto-route creation") | |
| 378 | - | return nil | |
| 379 | - | } | |
| 380 | - | ||
| 381 | - | // Add the route to config with the full registry image path | |
| 382 | - | if err := h.config.AddRoute(domain, registryImageName); err != nil { | |
| 383 | - | return fmt.Errorf("failed to add auto-route for domain %s: %w", domain, err) | |
| 384 | - | } | |
| 385 | - | ||
| 386 | - | log.Info(). | |
| 387 | - | Str("domain", domain). | |
| 388 | - | Str("image", registryImageName). | |
| 389 | - | Msg("Successfully auto-created route - deployment will be handled by config reload event") | |
| 390 | - | ||
| 391 | - | return nil | |
| 392 | - | } | |
| 393 | - | ||
D internal/events/handlers_test.go +0 -852 collapsed to keep the page light
D internal/events/rollback_handler.go +0 -188 collapsed to keep the page light
D internal/events/types.go +0 -48
| @@ -1,48 +0,0 @@ | |||
| 1 | - | package events | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "time" | |
| 5 | - | ) | |
| 6 | - | ||
| 7 | - | type EventType string | |
| 8 | - | ||
| 9 | - | const ( | |
| 10 | - | ImagePushed EventType = "image.pushed" | |
| 11 | - | ImageDeleted EventType = "image.deleted" | |
| 12 | - | ConfigReload EventType = "config.reload" | |
| 13 | - | ManualReload EventType = "manual.reload" | |
| 14 | - | ContainerStop EventType = "container.stop" | |
| 15 | - | ContainerStart EventType = "container.start" | |
| 16 | - | ContainerHealthCheck EventType = "container.health_check" | |
| 17 | - | ) | |
| 18 | - | ||
| 19 | - | type Event struct { | |
| 20 | - | ID string `json:"id"` | |
| 21 | - | Type EventType `json:"type"` | |
| 22 | - | Timestamp time.Time `json:"timestamp"` | |
| 23 | - | ImageName string `json:"image_name,omitempty"` | |
| 24 | - | Tag string `json:"tag,omitempty"` | |
| 25 | - | Route string `json:"route,omitempty"` | |
| 26 | - | ContainerID string `json:"container_id,omitempty"` | |
| 27 | - | Data interface{} `json:"data,omitempty"` | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | type ImagePushedPayload struct { | |
| 31 | - | Name string `json:"name"` | |
| 32 | - | Reference string `json:"reference"` | |
| 33 | - | Manifest []byte `json:"-"` | |
| 34 | - | Annotations map[string]string `json:"annotations,omitempty"` | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | type EventHandler interface { | |
| 38 | - | Handle(event Event) error | |
| 39 | - | CanHandle(eventType EventType) bool | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | type EventBus interface { | |
| 43 | - | Publish(eventType EventType, payload interface{}) error | |
| 44 | - | Subscribe(handler EventHandler) error | |
| 45 | - | Unsubscribe(handler EventHandler) error | |
| 46 | - | Start() error | |
| 47 | - | Stop() error | |
| 48 | - | } | |
D internal/logging/collector.go +0 -236 collapsed to keep the page light
D internal/logging/collector_test.go +0 -597
| @@ -1,597 +0,0 @@ | |||
| 1 | - | package logging | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "errors" | |
| 6 | - | "io" | |
| 7 | - | "strings" | |
| 8 | - | "sync" | |
| 9 | - | "testing" | |
| 10 | - | "time" | |
| 11 | - | ||
| 12 | - | "github.com/stretchr/testify/assert" | |
| 13 | - | "github.com/stretchr/testify/require" | |
| 14 | - | "go.uber.org/mock/gomock" | |
| 15 | - | "gordon/internal/config" | |
| 16 | - | "gordon/internal/testutils/mocks" | |
| 17 | - | ) | |
| 18 | - | ||
| 19 | - | // mockLogStream implements io.ReadCloser for testing | |
| 20 | - | type mockLogStream struct { | |
| 21 | - | data string | |
| 22 | - | closed bool | |
| 23 | - | reader *strings.Reader | |
| 24 | - | ctx context.Context | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | func newMockLogStream(data string) *mockLogStream { | |
| 28 | - | return &mockLogStream{ | |
| 29 | - | data: data, | |
| 30 | - | reader: strings.NewReader(data), | |
| 31 | - | } | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | func newContextAwareMockLogStream(ctx context.Context) *mockLogStream { | |
| 35 | - | return &mockLogStream{ | |
| 36 | - | data: "", | |
| 37 | - | reader: strings.NewReader(""), | |
| 38 | - | ctx: ctx, | |
| 39 | - | } | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | func (m *mockLogStream) Read(p []byte) (n int, err error) { | |
| 43 | - | if m.closed { | |
| 44 | - | return 0, io.EOF | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | // If we have a context, check if it's cancelled | |
| 48 | - | if m.ctx != nil { | |
| 49 | - | select { | |
| 50 | - | case <-m.ctx.Done(): | |
| 51 | - | return 0, io.EOF | |
| 52 | - | default: | |
| 53 | - | // Block until context is cancelled | |
| 54 | - | <-m.ctx.Done() | |
| 55 | - | return 0, io.EOF | |
| 56 | - | } | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | return m.reader.Read(p) | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | func (m *mockLogStream) Close() error { | |
| 63 | - | m.closed = true | |
| 64 | - | return nil | |
| 65 | - | } | |
| 66 | - | ||
| 67 | - | func TestNewLogManager(t *testing.T) { | |
| 68 | - | ctrl := gomock.NewController(t) | |
| 69 | - | defer ctrl.Finish() | |
| 70 | - | ||
| 71 | - | cfg := &config.Config{} | |
| 72 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 73 | - | ||
| 74 | - | manager := NewLogManager(cfg, runtime) | |
| 75 | - | ||
| 76 | - | assert.NotNil(t, manager) | |
| 77 | - | assert.NotNil(t, manager.collectors) | |
| 78 | - | assert.Equal(t, cfg, manager.config) | |
| 79 | - | assert.Equal(t, runtime, manager.runtime) | |
| 80 | - | assert.Empty(t, manager.collectors) | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | func TestLogManager_StartCollection_LoggingDisabled(t *testing.T) { | |
| 84 | - | ctrl := gomock.NewController(t) | |
| 85 | - | defer ctrl.Finish() | |
| 86 | - | ||
| 87 | - | cfg := &config.Config{ | |
| 88 | - | Logging: config.LoggingConfig{ | |
| 89 | - | Enabled: false, | |
| 90 | - | }, | |
| 91 | - | } | |
| 92 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 93 | - | manager := NewLogManager(cfg, runtime) | |
| 94 | - | ||
| 95 | - | err := manager.StartCollection("container123", "test-container") | |
| 96 | - | assert.NoError(t, err) | |
| 97 | - | ||
| 98 | - | // Should not create any collectors | |
| 99 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | func TestLogManager_StartCollection_Success(t *testing.T) { | |
| 103 | - | ctrl := gomock.NewController(t) | |
| 104 | - | defer ctrl.Finish() | |
| 105 | - | ||
| 106 | - | tempDir := t.TempDir() | |
| 107 | - | cfg := &config.Config{ | |
| 108 | - | Logging: config.LoggingConfig{ | |
| 109 | - | Enabled: true, | |
| 110 | - | Dir: tempDir, | |
| 111 | - | ContainerLogDir: "containers", | |
| 112 | - | MainLogFile: "gordon.log", | |
| 113 | - | ProxyLogFile: "proxy.log", | |
| 114 | - | Level: "info", | |
| 115 | - | MaxSize: 10, | |
| 116 | - | MaxBackups: 3, | |
| 117 | - | MaxAge: 7, | |
| 118 | - | Compress: false, | |
| 119 | - | }, | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 123 | - | manager := NewLogManager(cfg, runtime) | |
| 124 | - | ||
| 125 | - | containerID := "container123" | |
| 126 | - | containerName := "test-container" | |
| 127 | - | ||
| 128 | - | err := manager.StartCollection(containerID, containerName) | |
| 129 | - | require.NoError(t, err) | |
| 130 | - | ||
| 131 | - | // Should create one collector | |
| 132 | - | assert.Equal(t, 1, manager.GetActiveCollectors()) | |
| 133 | - | ||
| 134 | - | // Check collector exists | |
| 135 | - | manager.mu.RLock() | |
| 136 | - | collector, exists := manager.collectors[containerID] | |
| 137 | - | manager.mu.RUnlock() | |
| 138 | - | ||
| 139 | - | assert.True(t, exists) | |
| 140 | - | assert.Equal(t, containerID, collector.containerID) | |
| 141 | - | assert.Equal(t, containerName, collector.containerName) | |
| 142 | - | assert.NotNil(t, collector.logWriter) | |
| 143 | - | assert.NotNil(t, collector.ctx) | |
| 144 | - | assert.NotNil(t, collector.cancel) | |
| 145 | - | ||
| 146 | - | // Clean up | |
| 147 | - | manager.StopAll() | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | func TestLogManager_StartCollection_ReplaceExisting(t *testing.T) { | |
| 151 | - | ctrl := gomock.NewController(t) | |
| 152 | - | defer ctrl.Finish() | |
| 153 | - | ||
| 154 | - | tempDir := t.TempDir() | |
| 155 | - | cfg := &config.Config{ | |
| 156 | - | Logging: config.LoggingConfig{ | |
| 157 | - | Enabled: true, | |
| 158 | - | Dir: tempDir, | |
| 159 | - | ContainerLogDir: "containers", | |
| 160 | - | MainLogFile: "gordon.log", | |
| 161 | - | ProxyLogFile: "proxy.log", | |
| 162 | - | Level: "info", | |
| 163 | - | MaxSize: 10, | |
| 164 | - | MaxBackups: 3, | |
| 165 | - | MaxAge: 7, | |
| 166 | - | Compress: false, | |
| 167 | - | }, | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 171 | - | manager := NewLogManager(cfg, runtime) | |
| 172 | - | ||
| 173 | - | containerID := "container123" | |
| 174 | - | ||
| 175 | - | // Start first collector | |
| 176 | - | err := manager.StartCollection(containerID, "first-container") | |
| 177 | - | require.NoError(t, err) | |
| 178 | - | assert.Equal(t, 1, manager.GetActiveCollectors()) | |
| 179 | - | ||
| 180 | - | // Get reference to first collector | |
| 181 | - | manager.mu.RLock() | |
| 182 | - | firstCollector := manager.collectors[containerID] | |
| 183 | - | manager.mu.RUnlock() | |
| 184 | - | ||
| 185 | - | // Start second collector with same ID (should replace) | |
| 186 | - | err = manager.StartCollection(containerID, "second-container") | |
| 187 | - | require.NoError(t, err) | |
| 188 | - | assert.Equal(t, 1, manager.GetActiveCollectors()) | |
| 189 | - | ||
| 190 | - | // Get reference to second collector | |
| 191 | - | manager.mu.RLock() | |
| 192 | - | secondCollector := manager.collectors[containerID] | |
| 193 | - | manager.mu.RUnlock() | |
| 194 | - | ||
| 195 | - | // Should be different collectors | |
| 196 | - | assert.NotEqual(t, firstCollector, secondCollector) | |
| 197 | - | assert.Equal(t, "second-container", secondCollector.containerName) | |
| 198 | - | ||
| 199 | - | // Clean up | |
| 200 | - | manager.StopAll() | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | func TestLogManager_StopCollection(t *testing.T) { | |
| 204 | - | ctrl := gomock.NewController(t) | |
| 205 | - | defer ctrl.Finish() | |
| 206 | - | ||
| 207 | - | tempDir := t.TempDir() | |
| 208 | - | cfg := &config.Config{ | |
| 209 | - | Logging: config.LoggingConfig{ | |
| 210 | - | Enabled: true, | |
| 211 | - | Dir: tempDir, | |
| 212 | - | ContainerLogDir: "containers", | |
| 213 | - | MainLogFile: "gordon.log", | |
| 214 | - | ProxyLogFile: "proxy.log", | |
| 215 | - | Level: "info", | |
| 216 | - | MaxSize: 10, | |
| 217 | - | MaxBackups: 3, | |
| 218 | - | MaxAge: 7, | |
| 219 | - | Compress: false, | |
| 220 | - | }, | |
| 221 | - | } | |
| 222 | - | ||
| 223 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 224 | - | manager := NewLogManager(cfg, runtime) | |
| 225 | - | ||
| 226 | - | containerID := "container123" | |
| 227 | - | ||
| 228 | - | // Start collector | |
| 229 | - | err := manager.StartCollection(containerID, "test-container") | |
| 230 | - | require.NoError(t, err) | |
| 231 | - | assert.Equal(t, 1, manager.GetActiveCollectors()) | |
| 232 | - | ||
| 233 | - | // Stop collector | |
| 234 | - | manager.StopCollection(containerID) | |
| 235 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 236 | - | ||
| 237 | - | // Stopping non-existent collector should not panic | |
| 238 | - | manager.StopCollection("non-existent") | |
| 239 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 240 | - | } | |
| 241 | - | ||
| 242 | - | func TestLogManager_StopAll(t *testing.T) { | |
| 243 | - | ctrl := gomock.NewController(t) | |
| 244 | - | defer ctrl.Finish() | |
| 245 | - | ||
| 246 | - | tempDir := t.TempDir() | |
| 247 | - | cfg := &config.Config{ | |
| 248 | - | Logging: config.LoggingConfig{ | |
| 249 | - | Enabled: true, | |
| 250 | - | Dir: tempDir, | |
| 251 | - | ContainerLogDir: "containers", | |
| 252 | - | MainLogFile: "gordon.log", | |
| 253 | - | ProxyLogFile: "proxy.log", | |
| 254 | - | Level: "info", | |
| 255 | - | MaxSize: 10, | |
| 256 | - | MaxBackups: 3, | |
| 257 | - | MaxAge: 7, | |
| 258 | - | Compress: false, | |
| 259 | - | }, | |
| 260 | - | } | |
| 261 | - | ||
| 262 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 263 | - | ||
| 264 | - | // Mock GetContainerLogs calls for each container - return context-aware streams | |
| 265 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "container1", true).DoAndReturn( | |
| 266 | - | func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 267 | - | return newContextAwareMockLogStream(ctx), nil | |
| 268 | - | }).AnyTimes() | |
| 269 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "container2", true).DoAndReturn( | |
| 270 | - | func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 271 | - | return newContextAwareMockLogStream(ctx), nil | |
| 272 | - | }).AnyTimes() | |
| 273 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "container3", true).DoAndReturn( | |
| 274 | - | func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 275 | - | return newContextAwareMockLogStream(ctx), nil | |
| 276 | - | }).AnyTimes() | |
| 277 | - | ||
| 278 | - | manager := NewLogManager(cfg, runtime) | |
| 279 | - | ||
| 280 | - | // Start multiple collectors | |
| 281 | - | err := manager.StartCollection("container1", "test-container1") | |
| 282 | - | require.NoError(t, err) | |
| 283 | - | err = manager.StartCollection("container2", "test-container2") | |
| 284 | - | require.NoError(t, err) | |
| 285 | - | err = manager.StartCollection("container3", "test-container3") | |
| 286 | - | require.NoError(t, err) | |
| 287 | - | ||
| 288 | - | assert.Equal(t, 3, manager.GetActiveCollectors()) | |
| 289 | - | ||
| 290 | - | // Stop all collectors | |
| 291 | - | manager.StopAll() | |
| 292 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 293 | - | ||
| 294 | - | // Stopping again should be safe | |
| 295 | - | manager.StopAll() | |
| 296 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 297 | - | } | |
| 298 | - | ||
| 299 | - | func TestLogManager_GetActiveCollectors(t *testing.T) { | |
| 300 | - | ctrl := gomock.NewController(t) | |
| 301 | - | defer ctrl.Finish() | |
| 302 | - | ||
| 303 | - | cfg := &config.Config{ | |
| 304 | - | Logging: config.LoggingConfig{ | |
| 305 | - | Enabled: false, | |
| 306 | - | }, | |
| 307 | - | } | |
| 308 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 309 | - | manager := NewLogManager(cfg, runtime) | |
| 310 | - | ||
| 311 | - | // Initially no collectors | |
| 312 | - | assert.Equal(t, 0, manager.GetActiveCollectors()) | |
| 313 | - | ||
| 314 | - | // Test concurrent access | |
| 315 | - | var wg sync.WaitGroup | |
| 316 | - | for i := 0; i < 10; i++ { | |
| 317 | - | wg.Add(1) | |
| 318 | - | go func() { | |
| 319 | - | defer wg.Done() | |
| 320 | - | count := manager.GetActiveCollectors() | |
| 321 | - | assert.GreaterOrEqual(t, count, 0) | |
| 322 | - | }() | |
| 323 | - | } | |
| 324 | - | wg.Wait() | |
| 325 | - | } | |
| 326 | - | ||
| 327 | - | func TestLogCollector_Stop(t *testing.T) { | |
| 328 | - | tempDir := t.TempDir() | |
| 329 | - | cfg := &config.Config{ | |
| 330 | - | Logging: config.LoggingConfig{ | |
| 331 | - | Enabled: true, | |
| 332 | - | Dir: tempDir, | |
| 333 | - | ContainerLogDir: "containers", | |
| 334 | - | MaxSize: 10, | |
| 335 | - | MaxBackups: 3, | |
| 336 | - | MaxAge: 7, | |
| 337 | - | Compress: false, | |
| 338 | - | }, | |
| 339 | - | } | |
| 340 | - | ||
| 341 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 342 | - | require.NoError(t, err) | |
| 343 | - | ||
| 344 | - | ctrl := gomock.NewController(t) | |
| 345 | - | defer ctrl.Finish() | |
| 346 | - | ||
| 347 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 348 | - | collector := &LogCollector{ | |
| 349 | - | containerID: "test123", | |
| 350 | - | containerName: "test-container", | |
| 351 | - | runtime: mocks.NewMockRuntime(ctrl), | |
| 352 | - | logWriter: logWriter, | |
| 353 | - | ctx: ctx, | |
| 354 | - | cancel: cancel, | |
| 355 | - | } | |
| 356 | - | ||
| 357 | - | // Test that Stop doesn't panic | |
| 358 | - | assert.NotPanics(t, func() { | |
| 359 | - | collector.Stop() | |
| 360 | - | }) | |
| 361 | - | ||
| 362 | - | // Test that Stop can be called multiple times | |
| 363 | - | assert.NotPanics(t, func() { | |
| 364 | - | collector.Stop() | |
| 365 | - | }) | |
| 366 | - | } | |
| 367 | - | ||
| 368 | - | func TestLogCollector_CollectLogsOnce_Success(t *testing.T) { | |
| 369 | - | ctrl := gomock.NewController(t) | |
| 370 | - | defer ctrl.Finish() | |
| 371 | - | ||
| 372 | - | tempDir := t.TempDir() | |
| 373 | - | cfg := &config.Config{ | |
| 374 | - | Logging: config.LoggingConfig{ | |
| 375 | - | Enabled: true, | |
| 376 | - | Dir: tempDir, | |
| 377 | - | ContainerLogDir: "containers", | |
| 378 | - | MaxSize: 10, | |
| 379 | - | MaxBackups: 3, | |
| 380 | - | MaxAge: 7, | |
| 381 | - | Compress: false, | |
| 382 | - | }, | |
| 383 | - | } | |
| 384 | - | ||
| 385 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 386 | - | require.NoError(t, err) | |
| 387 | - | ||
| 388 | - | // Create mock runtime that returns log stream | |
| 389 | - | logData := "line 1\nline 2\nline 3" | |
| 390 | - | mockStream := newMockLogStream(logData) | |
| 391 | - | ||
| 392 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 393 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil) | |
| 394 | - | ||
| 395 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 396 | - | defer cancel() | |
| 397 | - | ||
| 398 | - | collector := &LogCollector{ | |
| 399 | - | containerID: "test123", | |
| 400 | - | containerName: "test-container", | |
| 401 | - | runtime: runtime, | |
| 402 | - | logWriter: logWriter, | |
| 403 | - | ctx: ctx, | |
| 404 | - | cancel: cancel, | |
| 405 | - | } | |
| 406 | - | ||
| 407 | - | err = collector.collectLogsOnce() | |
| 408 | - | assert.NoError(t, err) | |
| 409 | - | assert.True(t, mockStream.closed) | |
| 410 | - | } | |
| 411 | - | ||
| 412 | - | func TestLogCollector_CollectLogsOnce_Error(t *testing.T) { | |
| 413 | - | ctrl := gomock.NewController(t) | |
| 414 | - | defer ctrl.Finish() | |
| 415 | - | ||
| 416 | - | tempDir := t.TempDir() | |
| 417 | - | cfg := &config.Config{ | |
| 418 | - | Logging: config.LoggingConfig{ | |
| 419 | - | Enabled: true, | |
| 420 | - | Dir: tempDir, | |
| 421 | - | ContainerLogDir: "containers", | |
| 422 | - | MaxSize: 10, | |
| 423 | - | MaxBackups: 3, | |
| 424 | - | MaxAge: 7, | |
| 425 | - | Compress: false, | |
| 426 | - | }, | |
| 427 | - | } | |
| 428 | - | ||
| 429 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 430 | - | require.NoError(t, err) | |
| 431 | - | ||
| 432 | - | // Create mock runtime that returns error | |
| 433 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 434 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(nil, errors.New("failed to get logs")) | |
| 435 | - | ||
| 436 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 437 | - | defer cancel() | |
| 438 | - | ||
| 439 | - | collector := &LogCollector{ | |
| 440 | - | containerID: "test123", | |
| 441 | - | containerName: "test-container", | |
| 442 | - | runtime: runtime, | |
| 443 | - | logWriter: logWriter, | |
| 444 | - | ctx: ctx, | |
| 445 | - | cancel: cancel, | |
| 446 | - | } | |
| 447 | - | ||
| 448 | - | err = collector.collectLogsOnce() | |
| 449 | - | assert.Error(t, err) | |
| 450 | - | assert.Contains(t, err.Error(), "failed to get log stream") | |
| 451 | - | } | |
| 452 | - | ||
| 453 | - | func TestLogCollector_CollectLogsOnce_ContextCancelled(t *testing.T) { | |
| 454 | - | ctrl := gomock.NewController(t) | |
| 455 | - | defer ctrl.Finish() | |
| 456 | - | ||
| 457 | - | tempDir := t.TempDir() | |
| 458 | - | cfg := &config.Config{ | |
| 459 | - | Logging: config.LoggingConfig{ | |
| 460 | - | Enabled: true, | |
| 461 | - | Dir: tempDir, | |
| 462 | - | ContainerLogDir: "containers", | |
| 463 | - | MaxSize: 10, | |
| 464 | - | MaxBackups: 3, | |
| 465 | - | MaxAge: 7, | |
| 466 | - | Compress: false, | |
| 467 | - | }, | |
| 468 | - | } | |
| 469 | - | ||
| 470 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 471 | - | require.NoError(t, err) | |
| 472 | - | ||
| 473 | - | // Create mock stream that never ends | |
| 474 | - | mockStream := newMockLogStream("line 1\nline 2\n") | |
| 475 | - | ||
| 476 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 477 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil) | |
| 478 | - | ||
| 479 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 480 | - | ||
| 481 | - | collector := &LogCollector{ | |
| 482 | - | containerID: "test123", | |
| 483 | - | containerName: "test-container", | |
| 484 | - | runtime: runtime, | |
| 485 | - | logWriter: logWriter, | |
| 486 | - | ctx: ctx, | |
| 487 | - | cancel: cancel, | |
| 488 | - | } | |
| 489 | - | ||
| 490 | - | // Cancel context immediately | |
| 491 | - | cancel() | |
| 492 | - | ||
| 493 | - | err = collector.collectLogsOnce() | |
| 494 | - | assert.NoError(t, err) // Should return nil when context is cancelled | |
| 495 | - | } | |
| 496 | - | ||
| 497 | - | func TestLogCollector_CollectLogs_RetryLogic(t *testing.T) { | |
| 498 | - | ctrl := gomock.NewController(t) | |
| 499 | - | defer ctrl.Finish() | |
| 500 | - | ||
| 501 | - | tempDir := t.TempDir() | |
| 502 | - | cfg := &config.Config{ | |
| 503 | - | Logging: config.LoggingConfig{ | |
| 504 | - | Enabled: true, | |
| 505 | - | Dir: tempDir, | |
| 506 | - | ContainerLogDir: "containers", | |
| 507 | - | MaxSize: 10, | |
| 508 | - | MaxBackups: 3, | |
| 509 | - | MaxAge: 7, | |
| 510 | - | Compress: false, | |
| 511 | - | }, | |
| 512 | - | } | |
| 513 | - | ||
| 514 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 515 | - | require.NoError(t, err) | |
| 516 | - | ||
| 517 | - | // Create mock runtime that fails first time, succeeds second time | |
| 518 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 519 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true). | |
| 520 | - | Return(nil, errors.New("temporary failure")).Times(1) | |
| 521 | - | runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).DoAndReturn( | |
| 522 | - | func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { | |
| 523 | - | return newContextAwareMockLogStream(ctx), nil | |
| 524 | - | }).AnyTimes() | |
| 525 | - | ||
| 526 | - | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | |
| 527 | - | defer cancel() | |
| 528 | - | ||
| 529 | - | collector := &LogCollector{ | |
| 530 | - | containerID: "test123", | |
| 531 | - | containerName: "test-container", | |
| 532 | - | runtime: runtime, | |
| 533 | - | logWriter: logWriter, | |
| 534 | - | ctx: ctx, | |
| 535 | - | cancel: cancel, | |
| 536 | - | } | |
| 537 | - | ||
| 538 | - | // Start collection in goroutine | |
| 539 | - | collector.wg.Add(1) | |
| 540 | - | go collector.collectLogs() | |
| 541 | - | ||
| 542 | - | // Wait a bit for retry to happen | |
| 543 | - | time.Sleep(1500 * time.Millisecond) | |
| 544 | - | ||
| 545 | - | // Cancel and wait for completion | |
| 546 | - | cancel() | |
| 547 | - | collector.wg.Wait() | |
| 548 | - | ||
| 549 | - | // Test passes if no panic occurs and the mocks were called as expected | |
| 550 | - | } | |
| 551 | - | ||
| 552 | - | func TestLogCollector_CollectLogs_ContextCancellation(t *testing.T) { | |
| 553 | - | ctrl := gomock.NewController(t) | |
| 554 | - | defer ctrl.Finish() | |
| 555 | - | ||
| 556 | - | tempDir := t.TempDir() | |
| 557 | - | cfg := &config.Config{ | |
| 558 | - | Logging: config.LoggingConfig{ | |
| 559 | - | Enabled: true, | |
| 560 | - | Dir: tempDir, | |
| 561 | - | ContainerLogDir: "containers", | |
| 562 | - | MaxSize: 10, | |
| 563 | - | MaxBackups: 3, | |
| 564 | - | MaxAge: 7, | |
| 565 | - | Compress: false, | |
| 566 | - | }, | |
| 567 | - | } | |
| 568 | - | ||
| 569 | - | logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container") | |
| 570 | - | require.NoError(t, err) | |
| 571 | - | ||
| 572 | - | runtime := mocks.NewMockRuntime(ctrl) | |
| 573 | - | // Should not be called if context is cancelled immediately | |
| 574 | - | ||
| 575 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 576 | - | ||
| 577 | - | collector := &LogCollector{ | |
| 578 | - | containerID: "test123", | |
| 579 | - | containerName: "test-container", | |
| 580 | - | runtime: runtime, | |
| 581 | - | logWriter: logWriter, | |
| 582 | - | ctx: ctx, | |
| 583 | - | cancel: cancel, | |
| 584 | - | } | |
| 585 | - | ||
| 586 | - | // Cancel context immediately | |
| 587 | - | cancel() | |
| 588 | - | ||
| 589 | - | // Start collection | |
| 590 | - | collector.wg.Add(1) | |
| 591 | - | go collector.collectLogs() | |
| 592 | - | ||
| 593 | - | // Wait for completion | |
| 594 | - | collector.wg.Wait() | |
| 595 | - | ||
| 596 | - | // Test passes if no panic occurs and goroutine exits cleanly | |
| 597 | - | }⏎̸ | |
D internal/logging/setup.go +0 -161 collapsed to keep the page light
D internal/logging/setup_test.go +0 -381 collapsed to keep the page light
D internal/middleware/auth.go +0 -76 collapsed to keep the page light
D internal/middleware/auth_test.go +0 -301 collapsed to keep the page light
D internal/middleware/logging_test.go +0 -541 collapsed to keep the page light
D internal/proxy/server.go +0 -296 collapsed to keep the page light
D internal/proxy/server_test.go +0 -1101 collapsed to keep the page light
D internal/registry/manifest.go +0 -4
| @@ -1,4 +0,0 @@ | |||
| 1 | - | package registry | |
| 2 | - | ||
| 3 | - | // This file is deprecated - manifest processing has been moved to pkg/manifest | |
| 4 | - | // to avoid circular import issues. This file will be removed in a future version.⏎̸ | |