real init
This commit is contained in:
@@ -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
|
||||
} */
|
||||
Reference in New Issue
Block a user