real init

This commit is contained in:
2026-07-29 01:36:09 +01:00
parent bb8c94f06e
commit 0accff3e19
55 changed files with 3806 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
package actions
import (
"bytes"
"fmt"
"net/http"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/containers"
)
type ActionProxyPayload struct {
Method string
Action string
Data string `json:"data"`
}
type ActionProxyResponse struct {
StatusCode int
Body string
}
func ActionProxy(container_id, action string, payload ActionProxyPayload) (ActionProxyResponse, error) {
// Forward the action to the container with the given ID, and return the result to the frontend
containers := containers.NewContainerCache().GetServices()
for _, container := range containers {
if container.ID == container_id {
// Forward the action to this container
body := bytes.NewBuffer([]byte(payload.Data))
req, err := http.NewRequest(payload.Method, fmt.Sprintf("http://%s:%s/action/%s", container.ID[:12], container.Network.Ports[0].Host, payload.Action), body)
if err != nil {
return ActionProxyResponse{}, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return ActionProxyResponse{}, err
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
return ActionProxyResponse{
StatusCode: resp.StatusCode,
Body: buf.String(),
}, nil
}
}
return ActionProxyResponse{}, fmt.Errorf("container with ID %s not found", container_id)
}
+6
View File
@@ -0,0 +1,6 @@
package consts
const GITEA_RO_TOKEN = ""
const GITEA_RW_PACKAGE_TOKEN = ""
const GITEA_ORG = ""
const GITEA_USER = ""
+66
View File
@@ -0,0 +1,66 @@
package containers
import (
"sync"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
)
type ContainerCache struct {
sync.RWMutex
// Maps Container ID -> Container
services map[string]types.Container
UpdateCh chan struct{}
}
func NewContainerCache() *ContainerCache {
return &ContainerCache{
services: make(map[string]types.Container),
UpdateCh: make(chan struct{}, 10),
}
}
func (cache *ContainerCache) AddService(c types.Container) {
cache.Lock()
if cache.services == nil {
cache.services = make(map[string]types.Container)
}
cache.services[c.ID] = c
cache.Unlock()
// Trigger update AFTER unlocking to prevent holding locks during channel sends
cache.TriggerUpdate()
}
func (cache *ContainerCache) RemoveService(c types.Container) {
cache.Lock()
_, exists := cache.services[c.ID]
if exists {
delete(cache.services, c.ID)
}
cache.Unlock()
// Only trigger an update if an item was actually removed
if exists {
cache.TriggerUpdate()
}
}
func (cache *ContainerCache) TriggerUpdate() {
select {
case cache.UpdateCh <- struct{}{}:
default:
// Channel buffer is full; subscriber already knows an update is pending
}
}
func (cache *ContainerCache) GetServices() []types.Container {
cache.RLock()
defer cache.RUnlock()
services := make([]types.Container, 0, len(cache.services))
for _, service := range cache.services {
services = append(services, service)
}
return services
}
+1
View File
@@ -0,0 +1 @@
package containers
+117
View File
@@ -0,0 +1,117 @@
package containers
import (
"context"
"fmt"
"log"
"os"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
)
func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.DockerClient) {
filter := filters.NewArgs()
filter.Add("type", "container")
filter.Add("event", "start")
filter.Add("event", "die")
filter.Add("event", "destroy")
messages, errs := dc.Client.Events(ctx, events.ListOptions{Filters: filter})
log.Println("Subscribed to Docker container events...")
for {
select {
case <-ctx.Done():
return
case err := <-errs:
log.Printf("Docker event stream error: %v", err)
os.Exit(1)
return
case event := <-messages:
serviceLabel, ok := event.Actor.Attributes["org.dcntrlized.service"]
if !ok {
log.Default().Printf("Found unrelated service %s\n", serviceLabel)
for attribute := range event.Actor.Attributes {
log.Default().Printf("\tKey: %s\tValue: %s\n", attribute, event.Actor.Attributes[attribute])
}
continue
}
_ = serviceLabel
containerID := event.Actor.ID
containerName := event.Actor.Attributes["name"]
cache.Lock()
switch event.Action {
case "start":
// Inspect Container
inspectData, inspectErr := dc.Client.ContainerInspect(ctx, containerID)
// Get Network information
var containerNetwork types.Network
if inspectErr != nil {
log.Printf("Failed to inspect container %s: %v", containerID, inspectErr)
} else {
netInfo, shouldCache, err := docker.ExtractNetworkInfo(ctx, dc, inspectData)
if err != nil {
log.Printf("Error extracting network details: %v", err)
}
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)
// Fetch Manifest
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,
}
case "die":
if container, exists := cache.services[containerID]; exists {
log.Printf("Service stopping: %s", containerName)
container.Status = "stopping"
cache.services[containerID] = container
}
case "destroy":
if container, exists := cache.services[containerID]; exists {
log.Printf("Service destroyed: %s", containerName)
cache.RemoveService(container)
}
}
cache.Unlock()
cache.TriggerUpdate()
}
}
}
+37
View File
@@ -0,0 +1,37 @@
package containers
import (
"encoding/json"
"log"
"os"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
)
type DevService struct {
ID string `json:"id"` // Use "localhost" or IP
Port string `json:"port"`
Name string `json:"name"`
}
func (cache *ContainerCache) LoadDevServices(filePath string) {
log.Printf("Dev Mode: Loading services from %s", filePath)
data, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("Failed to read dev services file: %v", err)
}
var devServices []DevService
if err := json.Unmarshal(data, &devServices); err != nil {
log.Fatalf("Failed to parse dev services: %v", err)
}
for _, ds := range devServices {
cache.AddService(types.Container{
ID: ds.ID,
Name: ds.Name,
Status: "unknown",
})
}
}
+308
View File
@@ -0,0 +1,308 @@
package docker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
// DockerClient is a wrapper around the Docker client that provides additional functionality.
type DockerClient struct {
*client.Client
}
// ContainerMetadata holds essential high-level metadata extracted from an inspection.
type ContainerMetadata struct {
ID string
Name string
Image string
Version string
IPAddress string
Status string
Created string
Labels map[string]string
Env map[string]string
}
// NewDockerClient creates a new instance of DockerClient.
func NewDockerClient() (*DockerClient, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
return &DockerClient{cli}, nil
}
func (dc *DockerClient) Close() error {
return dc.Client.Close()
}
func ListContainers(dc *DockerClient) ([]string, error) {
// Lists containers with org.dcntrlized.service label
filter := filters.NewArgs()
filter.Add("label", "org.dcntrlized.service")
options := container.ListOptions{
All: true,
Filters: filter,
}
containers, err := dc.Client.ContainerList(context.Background(), options)
if err != nil {
return nil, err
}
var containerNames []string
for _, container := range containers {
if _, ok := container.Labels["org.dcntrlized.service"]; ok {
containerNames = append(containerNames, container.Names[0])
}
}
return containerNames, nil
}
// fetchManifestWithRetry polls the container's manifest endpoint until it succeeds or max retries are reached.
// Could probably be simplified to (ctx, container) with maxRetries and retryDelay hardcoded, low priority TODO
func FetchManifestWithRetry(ctx context.Context, host, port string, maxRetries int, retryDelay time.Duration) (*types.Manifest, error) {
url := fmt.Sprintf("http://%s:%s/_manifest", host, port)
var resp *http.Response
var err error
// Create a client that respects context deadlines/cancellation
client := &http.Client{Timeout: 2 * time.Second}
for range maxRetries {
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if reqErr == nil {
resp, err = client.Do(req)
if err == nil {
break
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(retryDelay):
}
}
if err != nil {
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var manifest types.Manifest
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
return nil, fmt.Errorf("failed to decode manifest: %w", err)
}
return &manifest, nil
}
// GetContainerVersion retrieves the container or image version from inspection data.
func GetContainerVersion(ctx context.Context, dc *DockerClient, containerID string) (string, error) {
inspectData, err := dc.Client.ContainerInspect(ctx, containerID)
if err != nil {
logger.Error("Failed to inspect container: %v", err)
return "", fmt.Errorf("failed to inspect container: %w", err)
}
// 1. Check for standard image/version labels
labels := inspectData.Config.Labels
if version, ok := labels["org.opencontainers.image.version"]; ok && version != "" {
logger.Debug("Found version from org.opencontainers.image.version label: %s", version)
return version, nil
}
if version, ok := labels["version"]; ok && version != "" {
logger.Debug("Found version from version label: %s", version)
return version, nil
}
logger.Debug("No version label found, falling back to image tag parsing for container %s", containerID)
// 2. Fall back to parsing the image tag (e.g., "redis:7.0" -> "7.0")
imageParts := strings.Split(inspectData.Config.Image, ":")
if len(imageParts) > 1 {
logger.Debug("Extracted version from image tag: %s", imageParts[len(imageParts)-1])
return imageParts[len(imageParts)-1], nil
}
logger.Debug("No version found in image tag, defaulting to 'latest' for container %s", containerID)
// 3. If no version is found, default to "latest"
return "latest", nil
}
// ExtractNetworkInfo parses the network mode, active network name, and port mappings.
func ExtractNetworkInfo(ctx context.Context, dc *DockerClient, inspectData container.InspectResponse) (types.Network, bool, error) {
var netInfo types.Network
hostConfig := inspectData.HostConfig
networkMode := string(hostConfig.NetworkMode)
// Host, none, and container-sharing modes bypass traditional network inspection
if networkMode == "host" || networkMode == "none" || strings.HasPrefix(networkMode, "container:") {
return netInfo, false, nil // Return false indicating non-standard/ignored network mode
}
networkName := ""
for nwName := range inspectData.NetworkSettings.Networks {
if nwName == "dcntrlized_net" {
netInfo.IPAddress.Address = inspectData.NetworkSettings.Networks[nwName].IPAddress
netInfo.IPAddress.Version = types.IPv4 // Currently hardcoded (I'm assuming ^^ will always be an IPv4 IP but if not we should gather version correctly low priority TODO)
}
networkName = nwName
break // First active network
}
if networkName == "" {
networkName = "bridge"
}
networkResource, err := dc.Client.NetworkInspect(ctx, networkName, network.InspectOptions{})
if err != nil {
netInfo.Mode = "bridge" // Fallback
} else {
netInfo.Mode = networkResource.Driver // e.g., "bridge", "overlay"
}
for p := range hostConfig.PortBindings {
hostPort, guestPort, err := p.Range()
if err != nil {
log.Printf("Error parsing port range for container: %v", err)
continue
}
netInfo.Ports = append(netInfo.Ports, types.PortMapping{Host: hostPort, Guest: guestPort})
}
netInfo.Name = networkName
return netInfo, true, nil
}
// GetContainerMetadata collects the primary useful metadata for a given container.
func GetContainerMetadata(ctx context.Context, dc *DockerClient, containerID string) (*ContainerMetadata, error) {
inspectData, err := dc.Client.ContainerInspect(ctx, containerID)
if err != nil {
return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
}
// Parse environment variables into a key-value map
envMap := make(map[string]string)
for _, env := range inspectData.Config.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
// Extract primary IP address from the active network settings
ipAddress := ""
for _, netSetting := range inspectData.NetworkSettings.Networks {
if netSetting.IPAddress != "" {
ipAddress = netSetting.IPAddress
break
}
}
version, _ := GetContainerVersion(ctx, dc, containerID)
return &ContainerMetadata{
ID: inspectData.ID,
Name: strings.TrimPrefix(inspectData.Name, "/"), // Clean leading slash
Image: inspectData.Config.Image,
Version: version,
IPAddress: ipAddress,
Status: inspectData.State.Status,
Created: inspectData.Created,
Labels: inspectData.Config.Labels,
Env: envMap,
}, nil
}
func GetImageTagFromContainer(ctx context.Context, cli *client.Client, containerID string) (string, error) {
// Step 1: Inspect the container
containerJSON, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return "", fmt.Errorf("failed to inspect container: %w", err)
}
// containerJSON.Config.Image holds the image name or digest used to run it
imageRef := containerJSON.Config.Image
// Step 2: Inspect the image to get its official RepoTags array
var rawBody bytes.Buffer
inspectResult, err := cli.ImageInspect(ctx, imageRef, client.ImageInspectWithRawResponse(&rawBody))
if err != nil {
return "", fmt.Errorf("failed to inspect image %s: %w", imageRef, err)
}
// If the image has repo tags, return the first match
if len(inspectResult.RepoTags) > 0 {
return inspectResult.RepoTags[0], nil
}
return imageRef, nil // Fallback to raw reference if untagged
}
/* func HasUpdateAvailable(ctx context.Context, cli *client.Client, containerID string) (bool, error) {
dc, _ := NewDockerClient()
ver, _ := GetContainerVersion(ctx, dc, containerID)
remote, _ := GetLatestRemoteVersion()
} */
/* func GetLatestRemoteVersion(registryRepo string) (string, error) {
// 1. Parse the repository reference (e.g., "git.local/dcntrlized/sampleservicedeploytest/sampleservicedeploytest")
repo, err := name.NewRepository(registryRepo)
if err != nil {
return "", fmt.Errorf("failed to parse repository: %w", err)
}
// 2. Fetch all tags from the remote registry (uses default keychain credentials like docker login)
tags, err := remote.List(repo, remote.WithAuthFromKeychain(remote.DefaultKeychain))
if err != nil {
return "", fmt.Errorf("failed to list remote tags: %w", err)
}
var latestVersion string
// 3. Filter and find the highest semantic version
for _, tag := range tags {
// Ensure it follows semantic versioning format (e.g., v1.2.3)
formattedTag := tag
if !semver.IsValid(formattedTag) {
// If it doesn't have a 'v' prefix, try prepending it to check if it's a valid semver core
if semver.IsValid("v" + tag) {
formattedTag = "v" + tag
} else {
continue // Skip non-semver tags like "latest"
}
}
if latestVersion == "" || semver.Compare(formattedTag, latestVersion) > 0 {
latestVersion = formattedTag
}
}
if latestVersion == "" {
return "", fmt.Errorf("no valid semantic version tags found in registry")
}
return latestVersion, nil
} */
+88
View File
@@ -0,0 +1,88 @@
package logger
import (
"io"
"log"
"os"
"sync"
"time"
)
var (
infoLogger *log.Logger
warnLogger *log.Logger
errorLogger *log.Logger
debugLogger *log.Logger
once sync.Once
)
func initLogger() {
if err := os.MkdirAll("logs", 0755); err != nil {
log.Printf("Failed to create logs directory: %v. Falling back to standard output.", err)
}
// 1. Setup Info Logger
var infoOutput io.Writer = os.Stdout
if infoFile, err := os.OpenFile("logs/info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
infoOutput = io.MultiWriter(os.Stdout, infoFile)
writeHeader(infoFile)
}
infoLogger = log.New(infoOutput, "INFO: ", log.Ldate|log.Ltime|log.LUTC)
// 2. Setup Warn Logger
var warnOutput io.Writer = os.Stdout
if warnFile, err := os.OpenFile("logs/warn.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
warnOutput = io.MultiWriter(os.Stdout, warnFile)
writeHeader(warnFile)
}
warnLogger = log.New(warnOutput, "WARN: ", log.Ldate|log.Ltime|log.LUTC)
// 3. Setup Error Logger
var errorOutput io.Writer = os.Stderr
if errorFile, err := os.OpenFile("logs/error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
errorOutput = io.MultiWriter(os.Stderr, errorFile)
writeHeader(errorFile)
}
// Note: increased call depth isn't an issue for Lshortfile here because
// log.Logger.Printf correctly tracks the caller frame right above it.
errorLogger = log.New(errorOutput, "ERROR: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile)
// 4. Setup Debug Logger
var debugOutput io.Writer = os.Stdout
if debugFile, err := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
debugOutput = io.MultiWriter(os.Stdout, debugFile)
writeHeader(debugFile)
}
debugLogger = log.New(debugOutput, "DEBUG: ", log.Ldate|log.Ltime|log.LUTC)
}
func writeHeader(file *os.File) {
file.WriteString("========== Start Log ==========\n")
file.WriteString("[INFO] Logger initialized\n")
file.WriteString("[" + time.Now().Format(time.RFC3339) + "]\n")
}
// Info accepts a format string and an optional, arbitrary number of arguments.
func Info(format string, v ...any) {
once.Do(initLogger)
infoLogger.Printf(format, v...)
}
// Warn accepts a format string and an optional, arbitrary number of arguments.
func Warn(format string, v ...any) {
once.Do(initLogger)
warnLogger.Printf(format, v...)
}
// Error accepts a format string and an optional, arbitrary number of arguments.
func Error(format string, v ...any) {
once.Do(initLogger)
errorLogger.Printf(format, v...)
}
// Debug accepts a format string and an optional, arbitrary number of arguments.
func Debug(format string, v ...any) {
once.Do(initLogger)
debugLogger.Printf(format, v...)
}
+42
View File
@@ -0,0 +1,42 @@
package types
type PortMapping struct {
Host int `json:"host"`
Guest int `json:"guest"`
}
type IPVersion string
const (
IPv4 IPVersion = "v4"
IPv6 IPVersion = "v6"
)
type IPAddress struct {
Version IPVersion `json:"version"`
Address string `json:"address"`
}
type Network struct {
Name string `json:"name"`
IPAddress IPAddress `json:"ipAddress"`
Mode string `json:"mode"` // e.g., "bridge", "host", "none", "overlay", "ipvlan", "macvlan"
Ports []PortMapping `json:"ports"`
}
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
DockerName string `json:"docker_name"`
Image string `json:"image"`
Version string `json:"version"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
Network Network `json:"network"`
Title string `json:"title,omitempty"`
Vendor string `json:"vendor,omitempty"`
Author string `json:"author,omitempty"`
Description string `json:"description,omitempty"`
Source string `json:"source,omitempty"`
}
+11
View File
@@ -0,0 +1,11 @@
package types
type Manifest struct {
Name string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
Capabilities []string `json:"capabilities"`
HasAuth bool `json:"has_auth"`
PublicKey string `json:"public_key"`
Icon string `json:"icon"`
}