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
+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
} */