feat: Implemented (un)install,start/stop flow and fixed actions flow

This commit is contained in:
2026-08-01 20:39:27 +01:00
parent eb01569f7d
commit d24fe0ffbf
29 changed files with 2331 additions and 166 deletions
+120 -26
View File
@@ -5,10 +5,12 @@ import (
"fmt"
"log"
"os"
"strings"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
)
@@ -41,7 +43,7 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc
}
continue
}
_ = serviceLabel
key := normalizeName(serviceLabel)
containerID := event.Actor.ID
containerName := event.Actor.Attributes["name"]
@@ -61,52 +63,70 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc
netInfo, shouldCache, err := docker.ExtractNetworkInfo(ctx, dc, inspectData)
if err != nil {
log.Printf("Error extracting network details: %v", err)
} else if !shouldCache {
log.Printf("Bypassing special network mode: %s", inspectData.HostConfig.NetworkMode)
} else {
containerNetwork = netInfo
}
if !shouldCache {
fmt.Printf("Bypassing special network mode: %s\n", inspectData.HostConfig.NetworkMode)
cache.Unlock()
return
}
containerNetwork = netInfo
}
// Get Versions
ver, err := docker.GetContainerVersion(ctx, dc, containerID)
ver, _ := docker.GetContainerVersion(ctx, dc, containerID)
// Fetch Manifest
// Fetch Manifest (best effort; fall back to basic info if it fails)
host := containerID[:12]
port := event.Actor.Attributes["org.dcntrlized.port"]
manifest, err := docker.FetchManifestWithRetry(ctx, host, port, 20, 50*time.Millisecond)
if err != nil {
log.Printf("Error fetching manifest for container %s: %v", containerID, err)
cache.Unlock()
continue
}
// Save to Cache
log.Printf("Service started. Adding container %s - %s to cache", host, containerName)
cache.services[containerID] = types.Container{
ID: containerID,
Name: manifest.Name,
DockerName: containerName,
Image: event.Actor.Attributes["image"],
CreatedAt: fmt.Sprint(event.TimeNano),
Network: containerNetwork,
Status: "running",
Version: ver,
existing, _ := cache.services[key]
container := types.Container{
ID: containerID,
ServiceName: serviceLabel,
DockerName: containerName,
Image: event.Actor.Attributes["image"],
CreatedAt: fmt.Sprint(event.TimeNano),
Network: containerNetwork,
Status: "running",
Version: ver,
Title: existing.Title,
Vendor: existing.Vendor,
Author: existing.Author,
Description: existing.Description,
Source: existing.Source,
}
if manifest != nil {
container.Name = manifest.Name
} else {
container.Name = containerName
}
cache.services[key] = container
case "die":
if container, exists := cache.services[containerID]; exists {
log.Printf("Service stopping: %s", containerName)
container.Status = "stopping"
cache.services[containerID] = container
if container, exists := cache.services[key]; exists {
log.Printf("Service stopped: %s", containerName)
container.Status = "stopped"
cache.services[key] = container
}
case "destroy":
if container, exists := cache.services[containerID]; exists {
if container, exists := cache.services[key]; exists {
log.Printf("Service destroyed: %s", containerName)
cache.RemoveService(container)
if IsInstalled(serviceLabel) || IsInstalled(container.ServiceName) || IsInstalled(container.Name) {
container.Status = "installed"
container.ID = ""
container.DockerName = ""
container.Network = types.Network{}
cache.services[key] = container
} else {
// We already hold the lock; delete directly instead of
// calling RemoveService (which re-locks and deadlocks).
delete(cache.services, key)
}
}
}
@@ -115,3 +135,77 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc
}
}
}
// imageTag returns the tag portion of an image reference, defaulting to latest.
func imageTag(ref string) string {
base := ref
if i := strings.LastIndex(base, "/"); i >= 0 {
base = base[i+1:]
}
if i := strings.LastIndex(base, ":"); i >= 0 {
return base[i+1:]
}
return "latest"
}
// normalizeDockerState maps raw Docker container states onto the smaller status
// vocabulary used by the frontend (running / starting / stopped / installed).
func normalizeDockerState(state string) string {
switch state {
case "running":
return "running"
case "restarting", "starting":
return "starting"
case "exited", "stopped", "paused", "dead", "removing":
return "stopped"
case "created":
return "installed"
default:
return "installed"
}
}
// ReconcileContainers lists all dcntrlized-labeled containers (running or not) and
// syncs their status into the cache, so state survives a Maestro restart.
func (cache *ContainerCache) ReconcileContainers(ctx context.Context, dc *docker.DockerClient) {
filter := filters.NewArgs()
filter.Add("label", "org.dcntrlized.service")
options := container.ListOptions{
All: true,
Filters: filter,
}
existing, err := dc.Client.ContainerList(ctx, options)
if err != nil {
log.Printf("Failed to list existing containers during reconciliation: %v", err)
return
}
for _, c := range existing {
serviceLabel := c.Labels["org.dcntrlized.service"]
if serviceLabel == "" || len(c.Names) == 0 {
continue
}
key := normalizeName(serviceLabel)
cache.Lock()
entry, has := cache.services[key]
if !has {
entry = types.Container{
ServiceName: serviceLabel,
Name: strings.TrimPrefix(c.Names[0], "/"),
Image: c.Image,
Version: imageTag(c.Image),
Status: "installed",
}
}
entry.Status = normalizeDockerState(c.State)
entry.ID = c.ID
entry.DockerName = strings.TrimPrefix(c.Names[0], "/")
cache.services[key] = entry
cache.Unlock()
}
cache.TriggerUpdate()
log.Printf("Reconciled %d existing containers with Docker", len(existing))
}