2.0.1 dcf7d6b

manager.go RawHistoryFolder

1package container
2
3import (
4	"context"
5	"fmt"
6	"sync"
7
8	"github.com/rs/zerolog/log"
9
10	"gordon/internal/config"
11	"gordon/pkg/runtime"
12)
13
14// Manager handles container lifecycle and management
15type Manager struct {
16	runtime   runtime.Runtime
17	config    *config.Config
18	containers map[string]*runtime.Container // map[domain] -> container
19	mu        sync.RWMutex
20}
21
22// NewManager creates a new container manager
23func NewManager(cfg *config.Config) (*Manager, error) {
24	// Create runtime using the factory
25	rt, err := CreateRuntime(cfg)
26	if err != nil {
27		return nil, fmt.Errorf("failed to create container runtime: %w", err)
28	}
29
30	// Test runtime connectivity
31	ctx := context.Background()
32	if err := rt.Ping(ctx); err != nil {
33		return nil, fmt.Errorf("runtime not available: %w", err)
34	}
35
36	version, err := rt.Version(ctx)
37	if err != nil {
38		log.Warn().Err(err).Msg("Could not get runtime version")
39	} else {
40		log.Info().Str("runtime", cfg.Server.Runtime).Str("version", version).Msg("Container runtime connected")
41	}
42
43	return &Manager{
44		runtime:    rt,
45		config:     cfg,
46		containers: make(map[string]*runtime.Container),
47	}, nil
48}
49
50// DeployContainer deploys a container for a specific route
51func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*runtime.Container, error) {
52	m.mu.Lock()
53	defer m.mu.Unlock()
54
55	// Check if container already exists for this domain
56	if existing, exists := m.containers[route.Domain]; exists {
57		log.Info().Str("domain", route.Domain).Str("container", existing.ID).Msg("Container already exists, restarting")
58		
59		// Stop and remove existing container
60		if err := m.runtime.StopContainer(ctx, existing.ID); err != nil {
61			log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to stop existing container")
62		}
63		
64		if err := m.runtime.RemoveContainer(ctx, existing.ID, true); err != nil {
65			log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to remove existing container")
66		}
67		
68		delete(m.containers, route.Domain)
69	}
70
71	// Pull the image first
72	log.Info().Str("image", route.Image).Msg("Pulling image")
73	if err := m.runtime.PullImage(ctx, route.Image); err != nil {
74		return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err)
75	}
76
77	// Get exposed ports from the image
78	exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, route.Image)
79	if err != nil {
80		log.Warn().Err(err).Str("image", route.Image).Msg("Failed to get exposed ports from image, using defaults")
81		exposedPorts = []int{80, 8080, 3000} // Fallback to common web server ports
82	}
83
84	// Create container configuration
85	containerConfig := &runtime.ContainerConfig{
86		Image:      route.Image,
87		Name:       fmt.Sprintf("gordon-%s", route.Domain),
88		Ports:      exposedPorts,
89		Labels: map[string]string{
90			"gordon.domain": route.Domain,
91			"gordon.image":  route.Image,
92			"gordon.managed": "true",
93			"gordon.route": route.Domain,
94		},
95		AutoRemove: false, // Keep containers for inspection
96	}
97
98	// Create the container
99	container, err := m.runtime.CreateContainer(ctx, containerConfig)
100	if err != nil {
101		return nil, fmt.Errorf("failed to create container for %s: %w", route.Domain, err)
102	}
103
104	// Start the container
105	if err := m.runtime.StartContainer(ctx, container.ID); err != nil {
106		// Clean up the created container
107		m.runtime.RemoveContainer(ctx, container.ID, true)
108		return nil, fmt.Errorf("failed to start container for %s: %w", route.Domain, err)
109	}
110
111	// Re-inspect to get updated port information
112	container, err = m.runtime.InspectContainer(ctx, container.ID)
113	if err != nil {
114		return nil, fmt.Errorf("failed to inspect started container: %w", err)
115	}
116
117	// Store the container mapping
118	m.containers[route.Domain] = container
119
120	log.Info().
121		Str("domain", route.Domain).
122		Str("image", route.Image).
123		Str("container", container.ID).
124		Ints("ports", container.Ports).
125		Msg("Container deployed successfully")
126
127	return container, nil
128}
129
130// GetContainer returns the container for a domain
131func (m *Manager) GetContainer(domain string) (*runtime.Container, bool) {
132	m.mu.RLock()
133	defer m.mu.RUnlock()
134	
135	container, exists := m.containers[domain]
136	return container, exists
137}
138
139// GetContainerPort returns the host port for a domain's container
140func (m *Manager) GetContainerPort(ctx context.Context, domain string, internalPort int) (int, error) {
141	m.mu.RLock()
142	container, exists := m.containers[domain]
143	m.mu.RUnlock()
144	
145	if !exists {
146		return 0, fmt.Errorf("no container found for domain %s", domain)
147	}
148
149	return m.runtime.GetContainerPort(ctx, container.ID, internalPort)
150}
151
152// ListContainers returns all managed containers
153func (m *Manager) ListContainers() map[string]*runtime.Container {
154	m.mu.RLock()
155	defer m.mu.RUnlock()
156	
157	// Return a copy of the map to avoid race conditions
158	result := make(map[string]*runtime.Container)
159	for domain, container := range m.containers {
160		result[domain] = container
161	}
162	return result
163}
164
165// FindContainerByDomain returns the container ID for a domain
166func (m *Manager) FindContainerByDomain(domain string) (string, bool) {
167	m.mu.RLock()
168	defer m.mu.RUnlock()
169	
170	if container, exists := m.containers[domain]; exists {
171		return container.ID, true
172	}
173	return "", false
174}
175
176// FindDomainByContainerID returns the domain for a container ID
177func (m *Manager) FindDomainByContainerID(containerID string) (string, bool) {
178	m.mu.RLock()
179	defer m.mu.RUnlock()
180	
181	for domain, container := range m.containers {
182		if container.ID == containerID {
183			return domain, true
184		}
185	}
186	return "", false
187}
188
189// StopContainer stops a container by ID
190func (m *Manager) StopContainer(ctx context.Context, containerID string) error {
191	if err := m.runtime.StopContainer(ctx, containerID); err != nil {
192		return fmt.Errorf("failed to stop container %s: %w", containerID, err)
193	}
194
195	log.Info().Str("container", containerID).Msg("Container stopped")
196	return nil
197}
198
199// StopContainerByDomain stops a container for a domain
200func (m *Manager) StopContainerByDomain(ctx context.Context, domain string) error {
201	m.mu.RLock()
202	container, exists := m.containers[domain]
203	m.mu.RUnlock()
204	
205	if !exists {
206		return fmt.Errorf("no container found for domain %s", domain)
207	}
208
209	return m.StopContainer(ctx, container.ID)
210}
211
212// RemoveContainer removes a container by ID
213func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force bool) error {
214	if err := m.runtime.RemoveContainer(ctx, containerID, force); err != nil {
215		return fmt.Errorf("failed to remove container %s: %w", containerID, err)
216	}
217
218	// Remove from our tracking map
219	m.mu.Lock()
220	defer m.mu.Unlock()
221	for domain, container := range m.containers {
222		if container.ID == containerID {
223			delete(m.containers, domain)
224			log.Info().Str("domain", domain).Str("container", containerID).Msg("Container removed")
225			break
226		}
227	}
228	
229	return nil
230}
231
232// RemoveContainerByDomain removes a container for a domain
233func (m *Manager) RemoveContainerByDomain(ctx context.Context, domain string, force bool) error {
234	m.mu.RLock()
235	container, exists := m.containers[domain]
236	m.mu.RUnlock()
237	
238	if !exists {
239		return fmt.Errorf("no container found for domain %s", domain)
240	}
241
242	return m.RemoveContainer(ctx, container.ID, force)
243}
244
245// SyncContainers synchronizes the internal state with the actual running containers
246func (m *Manager) SyncContainers(ctx context.Context) error {
247	m.mu.Lock()
248	defer m.mu.Unlock()
249
250	// Get all containers from the runtime
251	allContainers, err := m.runtime.ListContainers(ctx, false) // only running containers
252	if err != nil {
253		return fmt.Errorf("failed to list containers: %w", err)
254	}
255
256	// Find containers managed by Gordon
257	managedContainers := make(map[string]*runtime.Container)
258	for _, container := range allContainers {
259		if container.Labels != nil {
260			if domain, exists := container.Labels["gordon.domain"]; exists && container.Labels["gordon.managed"] == "true" {
261				managedContainers[domain] = container
262			}
263		}
264	}
265
266	// Update our internal state
267	m.containers = managedContainers
268
269	log.Info().Int("count", len(managedContainers)).Msg("Container state synchronized")
270	return nil
271}
272
273// HealthCheck checks the health of all managed containers
274func (m *Manager) HealthCheck(ctx context.Context) map[string]bool {
275	m.mu.RLock()
276	defer m.mu.RUnlock()
277
278	health := make(map[string]bool)
279	for domain, container := range m.containers {
280		running, err := m.runtime.IsContainerRunning(ctx, container.ID)
281		if err != nil {
282			log.Warn().Err(err).Str("domain", domain).Str("container", container.ID).Msg("Failed to check container health")
283			health[domain] = false
284		} else {
285			health[domain] = running
286		}
287	}
288
289	return health
290}
291
292// Runtime returns the underlying runtime interface
293func (m *Manager) Runtime() runtime.Runtime {
294	return m.runtime
295}

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