detached f452917

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.
12 package middleware
23
34 import (
45 "net/http"
56 "time"
67
7- "gordon/internal/logging"
8- "github.com/rs/zerolog/log"
8+ "github.com/bnema/zerowrap"
99 )
1010
11-// ResponseWriter wraps http.ResponseWriter to capture status code and bytes written
11+// ResponseWriter wraps http.ResponseWriter to capture status code and bytes written.
1212 type ResponseWriter struct {
1313 http.ResponseWriter
1414 statusCode int
1515 bytes int
1616 }
1717
18+// NewResponseWriter creates a new wrapped response writer.
1819 func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
1920 return &ResponseWriter{
2021 ResponseWriter: w,
@@ -22,60 +23,68 @@ func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
2223 }
2324 }
2425
26+// WriteHeader captures the status code.
2527 func (rw *ResponseWriter) WriteHeader(code int) {
2628 rw.statusCode = code
2729 rw.ResponseWriter.WriteHeader(code)
2830 }
2931
32+// Write captures bytes written.
3033 func (rw *ResponseWriter) Write(b []byte) (int, error) {
3134 n, err := rw.ResponseWriter.Write(b)
3235 rw.bytes += n
3336 return n, err
3437 }
3538
39+// StatusCode returns the captured status code.
3640 func (rw *ResponseWriter) StatusCode() int {
3741 return rw.statusCode
3842 }
3943
44+// BytesWritten returns the number of bytes written.
4045 func (rw *ResponseWriter) BytesWritten() int {
4146 return rw.bytes
4247 }
4348
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+ }
7685 }
7786
78-// getClientIP extracts the client IP from various headers
87+// getClientIP extracts the client IP from various headers.
7988 func getClientIP(r *http.Request) string {
8089 // Check X-Forwarded-For header (comma-separated list, first is original client)
8190 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
@@ -87,97 +96,63 @@ func getClientIP(r *http.Request) string {
8796 }
8897 return xff
8998 }
90-
99+
91100 // Check X-Real-IP header
92101 if xri := r.Header.Get("X-Real-IP"); xri != "" {
93102 return xri
94103 }
95-
104+
96105 // Check X-Forwarded header (less common)
97106 if xf := r.Header.Get("X-Forwarded"); xf != "" {
98107 return xf
99108 }
100-
109+
101110 // Fallback to RemoteAddr
102111 return r.RemoteAddr
103112 }
104113
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 {
107116 return func(next http.Handler) http.Handler {
108117 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)
139134 })
140135 }
141136 }
142137
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.
164139 func CORS(next http.Handler) http.Handler {
165140 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
166141 w.Header().Set("Access-Control-Allow-Origin", "*")
167142 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
168143 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
169-
144+
170145 // Handle preflight OPTIONS request
171146 if r.Method == "OPTIONS" {
172147 w.WriteHeader(http.StatusOK)
173148 return
174149 }
175-
150+
176151 next.ServeHTTP(w, r)
177152 })
178153 }
179154
180-// Chain combines multiple middleware functions
155+// Chain combines multiple middleware functions.
181156 func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
182157 return func(final http.Handler) http.Handler {
183158 for i := len(middlewares) - 1; i >= 0; i-- {
@@ -185,4 +160,4 @@ func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) ht
185160 }
186161 return final
187162 }
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.
12 package registry
23
34 import (
4- "context"
55 "crypto/sha256"
66 "encoding/json"
77 "fmt"
@@ -10,131 +10,139 @@ import (
1010 "strconv"
1111 "strings"
1212
13- "gordon/internal/config"
14- "gordon/internal/events"
15- "gordon/internal/middleware"
16- "gordon/pkg/manifest"
13+ "github.com/bnema/zerowrap"
1714
18- "github.com/rs/zerolog/log"
15+ "gordon/internal/boundaries/in"
16+ "gordon/internal/boundaries/out"
17+ "gordon/internal/domain"
18+ "gordon/pkg/manifest"
1919 )
2020
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
2627 }
2728
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,
3641 }
42+}
3743
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)
4647 }
4748
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)
5252 }
5353
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+
5564 path := r.URL.Path
56-
65+
5766 // Route manifest operations: /v2/{name}/manifests/{reference}
5867 if strings.Contains(path, "/manifests/") {
59- s.handleManifestRoutes(w, r)
68+ h.handleManifestRoutes(w, r)
6069 return
6170 }
62-
71+
6372 // Route blob operations: /v2/{name}/blobs/{digest}
6473 if strings.Contains(path, "/blobs/") && !strings.Contains(path, "/uploads/") {
65- s.handleBlobRoutes(w, r)
74+ h.handleBlobRoutes(w, r)
6675 return
6776 }
68-
77+
6978 // Route blob upload operations: /v2/{name}/blobs/uploads/
7079 if strings.Contains(path, "/blobs/uploads/") {
71- s.handleBlobUploadRoutes(w, r)
80+ h.handleBlobUploadRoutes(w, r)
7281 return
7382 }
74-
83+
7584 // Route tag list operations: /v2/{name}/tags/list
7685 if strings.Contains(path, "/tags/list") {
77- s.handleTagListRoutes(w, r)
86+ h.handleTagListRoutes(w, r)
7887 return
7988 }
80-
89+
8190 // Base endpoint: /v2/
8291 if path == "/v2/" {
83- s.handleBase(w, r)
92+ h.handleBase(w, r)
8493 return
8594 }
86-
95+
8796 http.NotFound(w, r)
8897 }
8998
90-func (s *Server) handleManifestRoutes(w http.ResponseWriter, r *http.Request) {
99+func (h *Handler) handleManifestRoutes(w http.ResponseWriter, r *http.Request) {
91100 // Parse path: /v2/{name}/manifests/{reference}
92101 parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v2/"), "/")
93102 if len(parts) < 3 || parts[len(parts)-2] != "manifests" {
94103 http.NotFound(w, r)
95104 return
96105 }
97-
106+
98107 reference := parts[len(parts)-1]
99108 name := strings.Join(parts[:len(parts)-2], "/")
100-
101- // Set path values for compatibility with existing handlers
109+
102110 r.SetPathValue("name", name)
103111 r.SetPathValue("reference", reference)
104-
112+
105113 switch r.Method {
106114 case "HEAD", "GET":
107- s.handleGetManifest(w, r)
115+ h.handleGetManifest(w, r)
108116 case "PUT":
109- s.handlePutManifest(w, r)
117+ h.handlePutManifest(w, r)
110118 default:
111119 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
112120 }
113121 }
114122
115-func (s *Server) handleBlobRoutes(w http.ResponseWriter, r *http.Request) {
123+func (h *Handler) handleBlobRoutes(w http.ResponseWriter, r *http.Request) {
116124 // Parse path: /v2/{name}/blobs/{digest}
117125 parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v2/"), "/")
118126 if len(parts) < 3 || parts[len(parts)-2] != "blobs" {
119127 http.NotFound(w, r)
120128 return
121129 }
122-
130+
123131 digest := parts[len(parts)-1]
124132 name := strings.Join(parts[:len(parts)-2], "/")
125-
133+
126134 r.SetPathValue("name", name)
127135 r.SetPathValue("digest", digest)
128-
136+
129137 switch r.Method {
130138 case "HEAD", "GET":
131- s.handleGetBlob(w, r)
139+ h.handleGetBlob(w, r)
132140 default:
133141 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
134142 }
135143 }
136144
137-func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) {
145+func (h *Handler) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request) {
138146 // Parse path: /v2/{name}/blobs/uploads/{uuid?}
139147 path := strings.TrimPrefix(r.URL.Path, "/v2/")
140148 uploadIndex := strings.Index(path, "/blobs/uploads/")
@@ -142,17 +150,17 @@ func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request)
142150 http.NotFound(w, r)
143151 return
144152 }
145-
153+
146154 name := path[:uploadIndex]
147155 uploadPart := path[uploadIndex+15:] // len("/blobs/uploads/") = 15
148-
156+
149157 r.SetPathValue("name", name)
150-
158+
151159 if uploadPart == "" {
152160 // POST /v2/{name}/blobs/uploads/
153161 switch r.Method {
154162 case "POST":
155- s.handleStartBlobUpload(w, r)
163+ h.handleStartBlobUpload(w, r)
156164 default:
157165 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
158166 }
@@ -161,73 +169,64 @@ func (s *Server) handleBlobUploadRoutes(w http.ResponseWriter, r *http.Request)
161169 r.SetPathValue("uuid", uploadPart)
162170 switch r.Method {
163171 case "PATCH", "PUT":
164- s.handleBlobUpload(w, r)
172+ h.handleBlobUpload(w, r)
165173 default:
166174 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
167175 }
168176 }
169177 }
170178
171-func (s *Server) handleTagListRoutes(w http.ResponseWriter, r *http.Request) {
179+func (h *Handler) handleTagListRoutes(w http.ResponseWriter, r *http.Request) {
172180 // Parse path: /v2/{name}/tags/list
173181 path := strings.TrimPrefix(r.URL.Path, "/v2/")
174182 if !strings.HasSuffix(path, "/tags/list") {
175183 http.NotFound(w, r)
176184 return
177185 }
178-
186+
179187 name := strings.TrimSuffix(path, "/tags/list")
180188 r.SetPathValue("name", name)
181-
189+
182190 switch r.Method {
183191 case "GET":
184- s.handleListTags(w, r)
192+ h.handleListTags(w, r)
185193 default:
186194 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
187195 }
188196 }
189197
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) {
201199 w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
202200 w.WriteHeader(http.StatusOK)
203201 }
204202
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)
206206 name := r.PathValue("name")
207207 reference := r.PathValue("reference")
208208
209209 log.Debug().Str("name", name).Str("reference", reference).Msg("GET manifest")
210210
211- manifest, contentType, err := s.storage.GetManifest(name, reference)
211+ manifestData, err := h.registrySvc.GetManifest(ctx, name, reference)
212212 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")
214214 http.NotFound(w, r)
215215 return
216216 }
217217
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)))
223220 w.WriteHeader(http.StatusOK)
224221
225222 if r.Method == "GET" {
226- _, _ = w.Write(manifest)
223+ _, _ = w.Write(manifestData.Data)
227224 }
228225 }
229226
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)
231230 name := r.PathValue("name")
232231 reference := r.PathValue("reference")
233232
@@ -235,7 +234,7 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) {
235234 log.Debug().Str("name", name).Str("reference", reference).Str("content_type", contentType).Msg("PUT manifest")
236235
237236 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")
239238 http.Error(w, "Content-Type header required", http.StatusBadRequest)
240239 return
241240 }
@@ -243,58 +242,51 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) {
243242 // Read manifest data
244243 data, err := io.ReadAll(r.Body)
245244 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")
247246 http.Error(w, "Bad Request", http.StatusBadRequest)
248247 return
249248 }
250249
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-
264250 // Parse manifest annotations
265251 annotations, err := manifest.ParseManifestAnnotations(data, contentType)
266252 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")
268254 annotations = map[string]string{}
269255 }
270256
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{
282259 Name: name,
283260 Reference: reference,
284- Manifest: data,
261+ ContentType: contentType,
262+ Data: data,
285263 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)
287277 }
288278
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)
290282 name := r.PathValue("name")
291283 digest := r.PathValue("digest")
292284
293285 log.Debug().Str("name", name).Str("digest", digest).Msg("GET blob")
294286
295- path, err := s.storage.GetBlobPath(digest)
287+ path, err := h.blobStorage.GetBlobPath(digest)
296288 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")
298290 http.NotFound(w, r)
299291 return
300292 }
@@ -303,26 +295,28 @@ func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) {
303295 http.ServeFile(w, r, path)
304296 }
305297
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)
307301 name := r.PathValue("name")
308302
309- log.Debug().Str("name", name).Msg("Starting blob upload")
303+ log.Debug().Str("name", name).Msg("starting blob upload")
310304
311- // Generate a unique upload ID
312- uuid, err := s.storage.StartBlobUpload(name)
305+ uuid, err := h.registrySvc.StartUpload(ctx, name)
313306 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")
315308 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
316309 return
317310 }
318311
319- // Set headers for the response
320312 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")
322314 w.WriteHeader(http.StatusAccepted)
323315 }
324316
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)
326320 name := r.PathValue("name")
327321 uuid := r.PathValue("uuid")
328322 digest := r.URL.Query().Get("digest")
@@ -331,36 +325,34 @@ func (s *Server) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
331325 Str("name", name).
332326 Str("uuid", uuid).
333327 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")
336330
337331 // Read the chunk from the request body
338332 chunk, err := io.ReadAll(r.Body)
339333 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")
341335 http.Error(w, "Bad Request", http.StatusBadRequest)
342336 return
343337 }
344338
345339 // Append the chunk to the upload
346- length, err := s.storage.AppendBlobChunk(name, uuid, chunk)
340+ length, err := h.blobStorage.AppendBlobChunk(name, uuid, chunk)
347341 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")
349343 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
350344 return
351345 }
352346
353347 // If this is the final chunk (PUT request with digest), finalize the upload
354348 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)
359352 http.Error(w, "Bad Request: digest mismatch", http.StatusBadRequest)
360353 return
361354 }
362355
363- // Respond with 201 Created
364356 w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest))
365357 w.Header().Set("Docker-Content-Digest", digest)
366358 w.WriteHeader(http.StatusCreated)
@@ -373,26 +365,27 @@ func (s *Server) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
373365 w.WriteHeader(http.StatusAccepted)
374366 }
375367
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)
377371 name := r.PathValue("name")
378372
379- log.Debug().Str("name", name).Msg("Listing tags")
373+ log.Debug().Str("name", name).Msg("listing tags")
380374
381- tags, err := s.storage.ListTags(name)
375+ tags, err := h.registrySvc.ListTags(ctx, name)
382376 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")
384378 w.WriteHeader(http.StatusNotFound)
385379 return
386380 }
387381
388- // Construct the response JSON properly
389382 tagsJSON, err := json.Marshal(tags)
390383 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")
392385 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
393386 return
394387 }
395-
388+
396389 response := fmt.Sprintf(`{"name":"%s","tags":%s}`, name, string(tagsJSON))
397390
398391 w.Header().Set("Content-Type", "application/json")
@@ -400,67 +393,3 @@ func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) {
400393 w.WriteHeader(http.StatusOK)
401394 w.Write([]byte(response))
402395 }
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 collapsed to keep the page light
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.⏎̸
D internal/registry/mocks/storage.go +0 -106 collapsed to keep the page light
D internal/registry/server_test.go +0 -614 collapsed to keep the page light
D internal/registry/storage.go +0 -503 collapsed to keep the page light
D internal/registry/storage_test.go +0 -466 collapsed to keep the page light
A internal/usecase/config/service.go +382 -0 collapsed to keep the page light
A internal/usecase/config/service_test.go +381 -0 collapsed to keep the page light
A internal/usecase/container/events.go +245 -0 collapsed to keep the page light
A internal/usecase/container/events_test.go +412 -0
@@ -0,0 +1,412 @@
1+package container
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/zerowrap"
9+ "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/mock"
11+
12+ inmocks "gordon/internal/boundaries/in/mocks"
13+ "gordon/internal/domain"
14+)
15+
16+func testCtx() context.Context {
17+ return zerowrap.WithCtx(context.Background(), zerowrap.Default())
18+}
19+
20+// ImagePushedHandler tests
21+
22+func TestImagePushedHandler_CanHandle(t *testing.T) {
23+ containerSvc := inmocks.NewMockContainerService(t)
24+ configSvc := inmocks.NewMockConfigService(t)
25+
26+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
27+
28+ assert.True(t, handler.CanHandle(domain.EventImagePushed))
29+ assert.False(t, handler.CanHandle(domain.EventConfigReload))
30+ assert.False(t, handler.CanHandle(domain.EventManualReload))
31+}
32+
33+func TestImagePushedHandler_Handle_DeploysMatchingRoutes(t *testing.T) {
34+ containerSvc := inmocks.NewMockContainerService(t)
35+ configSvc := inmocks.NewMockConfigService(t)
36+
37+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
38+
39+ // Configure routes that match the pushed image
40+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
41+ {Domain: "app1.example.com", Image: "myapp:latest"},
42+ {Domain: "app2.example.com", Image: "myapp:latest"},
43+ {Domain: "other.example.com", Image: "otherapp:latest"},
44+ })
45+
46+ // Expect Deploy to be called for both matching routes
47+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
48+ Domain: "app1.example.com",
49+ Image: "myapp:latest",
50+ }).Return(&domain.Container{ID: "container-1"}, nil)
51+
52+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
53+ Domain: "app2.example.com",
54+ Image: "myapp:latest",
55+ }).Return(&domain.Container{ID: "container-2"}, nil)
56+
57+ event := domain.Event{
58+ ID: "event-123",
59+ Type: domain.EventImagePushed,
60+ ImageName: "myapp",
61+ Tag: "latest",
62+ }
63+
64+ err := handler.Handle(event)
65+
66+ assert.NoError(t, err)
67+}
68+
69+func TestImagePushedHandler_Handle_NoMatchingRoutes(t *testing.T) {
70+ containerSvc := inmocks.NewMockContainerService(t)
71+ configSvc := inmocks.NewMockConfigService(t)
72+
73+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
74+
75+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
76+ {Domain: "other.example.com", Image: "otherapp:latest"},
77+ })
78+
79+ // No Deploy calls expected
80+
81+ event := domain.Event{
82+ ID: "event-123",
83+ Type: domain.EventImagePushed,
84+ ImageName: "myapp",
85+ Tag: "latest",
86+ }
87+
88+ err := handler.Handle(event)
89+
90+ assert.NoError(t, err)
91+}
92+
93+func TestImagePushedHandler_Handle_EmptyImageName(t *testing.T) {
94+ containerSvc := inmocks.NewMockContainerService(t)
95+ configSvc := inmocks.NewMockConfigService(t)
96+
97+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
98+
99+ event := domain.Event{
100+ ID: "event-123",
101+ Type: domain.EventImagePushed,
102+ ImageName: "",
103+ Tag: "latest",
104+ }
105+
106+ err := handler.Handle(event)
107+
108+ assert.ErrorIs(t, err, domain.ErrInvalidImageFormat)
109+}
110+
111+func TestImagePushedHandler_Handle_DefaultTag(t *testing.T) {
112+ containerSvc := inmocks.NewMockContainerService(t)
113+ configSvc := inmocks.NewMockConfigService(t)
114+
115+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
116+
117+ // Image name "myapp" with empty tag should become "myapp:latest"
118+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
119+ {Domain: "app.example.com", Image: "myapp:latest"},
120+ })
121+
122+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
123+ Domain: "app.example.com",
124+ Image: "myapp:latest",
125+ }).Return(&domain.Container{ID: "container-1"}, nil)
126+
127+ event := domain.Event{
128+ ID: "event-123",
129+ Type: domain.EventImagePushed,
130+ ImageName: "myapp",
131+ Tag: "", // Empty tag should default to "latest"
132+ }
133+
134+ err := handler.Handle(event)
135+
136+ assert.NoError(t, err)
137+}
138+
139+func TestImagePushedHandler_Handle_DeployError(t *testing.T) {
140+ containerSvc := inmocks.NewMockContainerService(t)
141+ configSvc := inmocks.NewMockConfigService(t)
142+
143+ handler := NewImagePushedHandler(testCtx(), containerSvc, configSvc)
144+
145+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
146+ {Domain: "app.example.com", Image: "myapp:latest"},
147+ })
148+
149+ containerSvc.EXPECT().Deploy(mock.Anything, mock.Anything).Return(nil, errors.New("deploy failed"))
150+
151+ event := domain.Event{
152+ ID: "event-123",
153+ Type: domain.EventImagePushed,
154+ ImageName: "myapp",
155+ Tag: "latest",
156+ }
157+
158+ // Handler logs error but doesn't fail
159+ err := handler.Handle(event)
160+
161+ assert.NoError(t, err)
162+}
163+
164+// ConfigReloadHandler tests
165+
166+func TestConfigReloadHandler_CanHandle(t *testing.T) {
167+ containerSvc := inmocks.NewMockContainerService(t)
168+ configSvc := inmocks.NewMockConfigService(t)
169+
170+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
171+
172+ assert.True(t, handler.CanHandle(domain.EventConfigReload))
173+ assert.False(t, handler.CanHandle(domain.EventImagePushed))
174+ assert.False(t, handler.CanHandle(domain.EventManualReload))
175+}
176+
177+func TestConfigReloadHandler_Handle_DeploysNewRoutes(t *testing.T) {
178+ containerSvc := inmocks.NewMockContainerService(t)
179+ configSvc := inmocks.NewMockConfigService(t)
180+
181+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
182+
183+ // No existing containers
184+ containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{})
185+
186+ // New route in config
187+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
188+ {Domain: "newapp.example.com", Image: "newapp:latest"},
189+ })
190+
191+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
192+ Domain: "newapp.example.com",
193+ Image: "newapp:latest",
194+ }).Return(&domain.Container{ID: "container-1"}, nil)
195+
196+ event := domain.Event{
197+ ID: "event-123",
198+ Type: domain.EventConfigReload,
199+ }
200+
201+ err := handler.Handle(event)
202+
203+ assert.NoError(t, err)
204+}
205+
206+func TestConfigReloadHandler_Handle_StopsRemovedRoutes(t *testing.T) {
207+ containerSvc := inmocks.NewMockContainerService(t)
208+ configSvc := inmocks.NewMockConfigService(t)
209+
210+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
211+
212+ // Existing container for a route that's no longer configured
213+ containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{
214+ "removed.example.com": {
215+ ID: "container-old",
216+ Labels: map[string]string{
217+ "gordon.route": "removed.example.com",
218+ "gordon.image": "oldapp:latest",
219+ },
220+ },
221+ })
222+
223+ // Empty routes - the route was removed
224+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
225+
226+ containerSvc.EXPECT().Stop(mock.Anything, "container-old").Return(nil)
227+ containerSvc.EXPECT().Remove(mock.Anything, "container-old", true).Return(nil)
228+
229+ event := domain.Event{
230+ ID: "event-123",
231+ Type: domain.EventConfigReload,
232+ }
233+
234+ err := handler.Handle(event)
235+
236+ assert.NoError(t, err)
237+}
238+
239+func TestConfigReloadHandler_Handle_RedeploysChangedImage(t *testing.T) {
240+ containerSvc := inmocks.NewMockContainerService(t)
241+ configSvc := inmocks.NewMockConfigService(t)
242+
243+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
244+
245+ // Existing container with old image
246+ containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{
247+ "app.example.com": {
248+ ID: "container-1",
249+ Labels: map[string]string{
250+ "gordon.route": "app.example.com",
251+ "gordon.image": "myapp:v1",
252+ },
253+ },
254+ })
255+
256+ // Config now has different image
257+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
258+ {Domain: "app.example.com", Image: "myapp:v2"},
259+ })
260+
261+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
262+ Domain: "app.example.com",
263+ Image: "myapp:v2",
264+ }).Return(&domain.Container{ID: "container-2"}, nil)
265+
266+ event := domain.Event{
267+ ID: "event-123",
268+ Type: domain.EventConfigReload,
269+ }
270+
271+ err := handler.Handle(event)
272+
273+ assert.NoError(t, err)
274+}
275+
276+func TestConfigReloadHandler_Handle_NoChanges(t *testing.T) {
277+ containerSvc := inmocks.NewMockContainerService(t)
278+ configSvc := inmocks.NewMockConfigService(t)
279+
280+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
281+
282+ // Existing container matches config
283+ containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{
284+ "app.example.com": {
285+ ID: "container-1",
286+ Labels: map[string]string{
287+ "gordon.route": "app.example.com",
288+ "gordon.image": "myapp:latest",
289+ },
290+ },
291+ })
292+
293+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
294+ {Domain: "app.example.com", Image: "myapp:latest"},
295+ })
296+
297+ // No Deploy, Stop, or Remove calls expected
298+
299+ event := domain.Event{
300+ ID: "event-123",
301+ Type: domain.EventConfigReload,
302+ }
303+
304+ err := handler.Handle(event)
305+
306+ assert.NoError(t, err)
307+}
308+
309+// ManualReloadHandler tests
310+
311+func TestManualReloadHandler_CanHandle(t *testing.T) {
312+ containerSvc := inmocks.NewMockContainerService(t)
313+ configSvc := inmocks.NewMockConfigService(t)
314+
315+ handler := NewManualReloadHandler(testCtx(), containerSvc, configSvc)
316+
317+ assert.True(t, handler.CanHandle(domain.EventManualReload))
318+ assert.False(t, handler.CanHandle(domain.EventImagePushed))
319+ assert.False(t, handler.CanHandle(domain.EventConfigReload))
320+}
321+
322+func TestManualReloadHandler_Handle_RedeploysExistingContainers(t *testing.T) {
323+ containerSvc := inmocks.NewMockContainerService(t)
324+ configSvc := inmocks.NewMockConfigService(t)
325+
326+ handler := NewManualReloadHandler(testCtx(), containerSvc, configSvc)
327+
328+ // Routes in config
329+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
330+ {Domain: "app1.example.com", Image: "app1:latest"},
331+ {Domain: "app2.example.com", Image: "app2:latest"},
332+ })
333+
334+ // Only app1 has a running container
335+ containerSvc.EXPECT().Get(mock.Anything, "app1.example.com").Return(&domain.Container{ID: "container-1"}, true)
336+ containerSvc.EXPECT().Get(mock.Anything, "app2.example.com").Return(nil, false)
337+
338+ // Only app1 should be redeployed (app2 has no container)
339+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
340+ Domain: "app1.example.com",
341+ Image: "app1:latest",
342+ }).Return(&domain.Container{ID: "container-1-new"}, nil)
343+
344+ event := domain.Event{
345+ ID: "event-123",
346+ Type: domain.EventManualReload,
347+ }
348+
349+ err := handler.Handle(event)
350+
351+ assert.NoError(t, err)
352+}
353+
354+func TestManualReloadHandler_Handle_NoContainersRunning(t *testing.T) {
355+ containerSvc := inmocks.NewMockContainerService(t)
356+ configSvc := inmocks.NewMockConfigService(t)
357+
358+ handler := NewManualReloadHandler(testCtx(), containerSvc, configSvc)
359+
360+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
361+ {Domain: "app.example.com", Image: "app:latest"},
362+ })
363+
364+ containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(nil, false)
365+
366+ // No Deploy calls - no container exists to reload
367+
368+ event := domain.Event{
369+ ID: "event-123",
370+ Type: domain.EventManualReload,
371+ }
372+
373+ err := handler.Handle(event)
374+
375+ assert.NoError(t, err)
376+}
377+
378+func TestManualReloadHandler_Handle_DeployErrors(t *testing.T) {
379+ containerSvc := inmocks.NewMockContainerService(t)
380+ configSvc := inmocks.NewMockConfigService(t)
381+
382+ handler := NewManualReloadHandler(testCtx(), containerSvc, configSvc)
383+
384+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
385+ {Domain: "app1.example.com", Image: "app1:latest"},
386+ {Domain: "app2.example.com", Image: "app2:latest"},
387+ })
388+
389+ containerSvc.EXPECT().Get(mock.Anything, "app1.example.com").Return(&domain.Container{ID: "container-1"}, true)
390+ containerSvc.EXPECT().Get(mock.Anything, "app2.example.com").Return(&domain.Container{ID: "container-2"}, true)
391+
392+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
393+ Domain: "app1.example.com",
394+ Image: "app1:latest",
395+ }).Return(nil, errors.New("deploy failed"))
396+
397+ containerSvc.EXPECT().Deploy(mock.Anything, domain.Route{
398+ Domain: "app2.example.com",
399+ Image: "app2:latest",
400+ }).Return(&domain.Container{ID: "container-2-new"}, nil)
401+
402+ event := domain.Event{
403+ ID: "event-123",
404+ Type: domain.EventManualReload,
405+ }
406+
407+ err := handler.Handle(event)
408+
409+ // Should return error indicating some failures
410+ assert.Error(t, err)
411+ assert.Contains(t, err.Error(), "1 errors")
412+}
A internal/usecase/container/service.go +657 -0 collapsed to keep the page light
A internal/usecase/container/service_test.go +573 -0 collapsed to keep the page light
A internal/usecase/proxy/service.go +290 -0 collapsed to keep the page light
A internal/usecase/proxy/service_test.go +175 -0 collapsed to keep the page light
A internal/usecase/registry/service.go +229 -0 collapsed to keep the page light
A internal/usecase/registry/service_test.go +431 -0 collapsed to keep the page light

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close