mirror of
https://github.com/fosrl/newt.git
synced 2026-02-08 05:56:40 +00:00
450 lines
14 KiB
Go
450 lines
14 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/events"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/client"
|
|
"github.com/fosrl/newt/logger"
|
|
)
|
|
|
|
// Container represents a Docker container
|
|
type Container struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
State string `json:"state"`
|
|
Status string `json:"status"`
|
|
Ports []Port `json:"ports"`
|
|
Labels map[string]string `json:"labels"`
|
|
Created int64 `json:"created"`
|
|
Networks map[string]Network `json:"networks"`
|
|
Hostname string `json:"hostname"` // added to use hostname if available instead of network address
|
|
|
|
}
|
|
|
|
// Port represents a port mapping for a Docker container
|
|
type Port struct {
|
|
PrivatePort int `json:"privatePort"`
|
|
PublicPort int `json:"publicPort,omitempty"`
|
|
Type string `json:"type"`
|
|
IP string `json:"ip,omitempty"`
|
|
}
|
|
|
|
// Network represents network information for a Docker container
|
|
type Network struct {
|
|
NetworkID string `json:"networkId"`
|
|
EndpointID string `json:"endpointId"`
|
|
Gateway string `json:"gateway,omitempty"`
|
|
IPAddress string `json:"ipAddress,omitempty"`
|
|
IPPrefixLen int `json:"ipPrefixLen,omitempty"`
|
|
IPv6Gateway string `json:"ipv6Gateway,omitempty"`
|
|
GlobalIPv6Address string `json:"globalIPv6Address,omitempty"`
|
|
GlobalIPv6PrefixLen int `json:"globalIPv6PrefixLen,omitempty"`
|
|
MacAddress string `json:"macAddress,omitempty"`
|
|
Aliases []string `json:"aliases,omitempty"`
|
|
DNSNames []string `json:"dnsNames,omitempty"`
|
|
}
|
|
|
|
// Strcuture parts of docker api endpoint
|
|
type dockerHost struct {
|
|
protocol string // e.g. unix, http, tcp, ssh
|
|
address string // e.g. "/var/run/docker.sock" or "host:port"
|
|
}
|
|
|
|
// Parse the docker api endpoint into its parts
|
|
func parseDockerHost(raw string) (dockerHost, error) {
|
|
switch {
|
|
case strings.HasPrefix(raw, "unix://"):
|
|
return dockerHost{"unix", strings.TrimPrefix(raw, "unix://")}, nil
|
|
case strings.HasPrefix(raw, "ssh://"):
|
|
// SSH is treated as TCP-like transport by the docker client
|
|
return dockerHost{"ssh", strings.TrimPrefix(raw, "ssh://")}, nil
|
|
case strings.HasPrefix(raw, "tcp://"), strings.HasPrefix(raw, "http://"), strings.HasPrefix(raw, "https://"):
|
|
s := raw
|
|
s = strings.TrimPrefix(s, "tcp://")
|
|
s = strings.TrimPrefix(s, "http://")
|
|
s = strings.TrimPrefix(s, "https://")
|
|
return dockerHost{"tcp", s}, nil
|
|
case strings.HasPrefix(raw, "/"):
|
|
// Absolute path without scheme - treat as unix socket
|
|
return dockerHost{"unix", raw}, nil
|
|
default:
|
|
// For relative paths or other formats, also default to unix
|
|
return dockerHost{"unix", raw}, nil
|
|
}
|
|
}
|
|
|
|
// CheckSocket checks if Docker socket is available
|
|
func CheckSocket(socketPath string) bool {
|
|
// Use the provided socket path or default to standard location
|
|
if socketPath == "" {
|
|
socketPath = "unix:///var/run/docker.sock"
|
|
}
|
|
|
|
// Ensure the socket path is properly formatted
|
|
if !strings.Contains(socketPath, "://") {
|
|
// If no scheme provided, assume unix socket
|
|
socketPath = "unix://" + socketPath
|
|
}
|
|
|
|
host, err := parseDockerHost(socketPath)
|
|
if err != nil {
|
|
logger.Debug("Invalid Docker socket path '%s': %v", socketPath, err)
|
|
return false
|
|
}
|
|
protocol := host.protocol
|
|
addr := host.address
|
|
|
|
// ssh might need different verification, but tcp works for basic reachability
|
|
conn, err := net.DialTimeout(protocol, addr, 2*time.Second)
|
|
if err != nil {
|
|
logger.Debug("Docker not reachable via %s at %s: %v", protocol, addr, err)
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
|
|
logger.Debug("Docker reachable via %s at %s", protocol, addr)
|
|
return true
|
|
}
|
|
|
|
// IsWithinHostNetwork checks if a provided target is within the host container network
|
|
func IsWithinHostNetwork(socketPath string, targetAddress string, targetPort int) (bool, error) {
|
|
// Always enforce network validation
|
|
containers, err := ListContainers(socketPath, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Determine if given an IP address
|
|
var parsedTargetAddressIp = net.ParseIP(targetAddress)
|
|
|
|
// If we can find the passed hostname/IP address in the networks or as the container name, it is valid and can add it
|
|
for _, c := range containers {
|
|
for _, network := range c.Networks {
|
|
// If the target address is not an IP address, use the container name
|
|
if parsedTargetAddressIp == nil {
|
|
if c.Name == targetAddress {
|
|
for _, port := range c.Ports {
|
|
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
//If the IP address matches, check the ports being mapped too
|
|
if network.IPAddress == targetAddress {
|
|
for _, port := range c.Ports {
|
|
if port.PublicPort == targetPort || port.PrivatePort == targetPort {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
combinedTargetAddress := targetAddress + ":" + strconv.Itoa(targetPort)
|
|
return false, fmt.Errorf("target address not within host container network: %s", combinedTargetAddress)
|
|
}
|
|
|
|
// ListContainers lists all Docker containers with their network information
|
|
func ListContainers(socketPath string, enforceNetworkValidation bool) ([]Container, error) {
|
|
// Use the provided socket path or default to standard location
|
|
if socketPath == "" {
|
|
socketPath = "unix:///var/run/docker.sock"
|
|
}
|
|
|
|
// Ensure the socket path is properly formatted for the Docker client
|
|
if !strings.Contains(socketPath, "://") {
|
|
// If no scheme provided, assume unix socket
|
|
socketPath = "unix://" + socketPath
|
|
}
|
|
|
|
// Used to filter down containers returned to Pangolin
|
|
containerFilters := filters.NewArgs()
|
|
|
|
// Used to determine if we will send IP addresses or hostnames to Pangolin
|
|
useContainerIpAddresses := true
|
|
hostContainerId := ""
|
|
|
|
// Create a new Docker client
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Create client with custom socket path
|
|
cli, err := client.NewClientWithOpts(
|
|
client.WithHost(socketPath),
|
|
client.WithAPIVersionNegotiation(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Docker client: %v", err)
|
|
}
|
|
|
|
defer cli.Close()
|
|
|
|
hostContainer, err := getHostContainer(ctx, cli)
|
|
if enforceNetworkValidation && err != nil {
|
|
return nil, fmt.Errorf("network validation enforced, cannot validate due to: %w", err)
|
|
}
|
|
|
|
// We may not be able to get back host container in scenarios like running the container in network mode 'host'
|
|
if hostContainer != nil {
|
|
// We can use the host container to filter out the list of returned containers
|
|
hostContainerId = hostContainer.ID
|
|
|
|
for hostContainerNetworkName := range hostContainer.NetworkSettings.Networks {
|
|
// If we're enforcing network validation, we'll filter on the host containers networks
|
|
if enforceNetworkValidation {
|
|
containerFilters.Add("network", hostContainerNetworkName)
|
|
}
|
|
|
|
// If the container is on the docker bridge network, we will use IP addresses over hostnames
|
|
if useContainerIpAddresses && hostContainerNetworkName != "bridge" {
|
|
useContainerIpAddresses = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// List containers
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: containerFilters})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list containers: %v", err)
|
|
}
|
|
|
|
var dockerContainers []Container
|
|
for _, c := range containers {
|
|
// Short ID like docker ps
|
|
shortId := c.ID[:12]
|
|
|
|
// Inspect container to get hostname
|
|
hostname := ""
|
|
containerInfo, err := cli.ContainerInspect(ctx, c.ID)
|
|
if err == nil && containerInfo.Config != nil {
|
|
hostname = containerInfo.Config.Hostname
|
|
}
|
|
|
|
// Skip host container if set
|
|
if hostContainerId != "" && c.ID == hostContainerId {
|
|
continue
|
|
}
|
|
|
|
// Get container name (remove leading slash)
|
|
name := ""
|
|
if len(c.Names) > 0 {
|
|
name = strings.TrimPrefix(c.Names[0], "/")
|
|
}
|
|
|
|
// Convert ports
|
|
var ports []Port
|
|
for _, port := range c.Ports {
|
|
dockerPort := Port{
|
|
PrivatePort: int(port.PrivatePort),
|
|
Type: port.Type,
|
|
}
|
|
if port.PublicPort != 0 {
|
|
dockerPort.PublicPort = int(port.PublicPort)
|
|
}
|
|
if port.IP != "" {
|
|
dockerPort.IP = port.IP
|
|
}
|
|
ports = append(ports, dockerPort)
|
|
}
|
|
|
|
// Get network information by inspecting the container
|
|
networks := make(map[string]Network)
|
|
|
|
// Extract network information from inspection
|
|
if c.NetworkSettings != nil && c.NetworkSettings.Networks != nil {
|
|
for networkName, endpoint := range c.NetworkSettings.Networks {
|
|
dockerNetwork := Network{
|
|
NetworkID: endpoint.NetworkID,
|
|
EndpointID: endpoint.EndpointID,
|
|
Gateway: endpoint.Gateway,
|
|
IPPrefixLen: endpoint.IPPrefixLen,
|
|
IPv6Gateway: endpoint.IPv6Gateway,
|
|
GlobalIPv6Address: endpoint.GlobalIPv6Address,
|
|
GlobalIPv6PrefixLen: endpoint.GlobalIPv6PrefixLen,
|
|
MacAddress: endpoint.MacAddress,
|
|
Aliases: endpoint.Aliases,
|
|
DNSNames: endpoint.DNSNames,
|
|
}
|
|
|
|
// Use IPs over hostnames/containers as we're on the bridge network
|
|
if useContainerIpAddresses {
|
|
dockerNetwork.IPAddress = endpoint.IPAddress
|
|
}
|
|
|
|
networks[networkName] = dockerNetwork
|
|
}
|
|
}
|
|
|
|
dockerContainer := Container{
|
|
ID: shortId,
|
|
Name: name,
|
|
Image: c.Image,
|
|
State: c.State,
|
|
Status: c.Status,
|
|
Ports: ports,
|
|
Labels: c.Labels,
|
|
Created: c.Created,
|
|
Networks: networks,
|
|
Hostname: hostname, // added
|
|
}
|
|
|
|
dockerContainers = append(dockerContainers, dockerContainer)
|
|
}
|
|
|
|
return dockerContainers, nil
|
|
}
|
|
|
|
// getHostContainer gets the current container for the current host if possible
|
|
func getHostContainer(dockerContext context.Context, dockerClient *client.Client) (*container.InspectResponse, error) {
|
|
// Get hostname from the os
|
|
hostContainerName, err := os.Hostname()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find hostname for container")
|
|
}
|
|
|
|
// Get host container from the docker socket
|
|
hostContainer, err := dockerClient.ContainerInspect(dockerContext, hostContainerName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find host container")
|
|
}
|
|
|
|
return &hostContainer, nil
|
|
}
|
|
|
|
// EventCallback defines the function signature for handling Docker events
|
|
type EventCallback func(containers []Container)
|
|
|
|
// EventMonitor handles Docker event monitoring
|
|
type EventMonitor struct {
|
|
client *client.Client
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
callback EventCallback
|
|
socketPath string
|
|
enforceNetworkValidation bool
|
|
}
|
|
|
|
// NewEventMonitor creates a new Docker event monitor
|
|
func NewEventMonitor(socketPath string, enforceNetworkValidation bool, callback EventCallback) (*EventMonitor, error) {
|
|
if socketPath == "" {
|
|
socketPath = "unix:///var/run/docker.sock"
|
|
}
|
|
|
|
if !strings.Contains(socketPath, "://") {
|
|
socketPath = "unix://" + socketPath
|
|
}
|
|
|
|
cli, err := client.NewClientWithOpts(
|
|
client.WithHost(socketPath),
|
|
client.WithAPIVersionNegotiation(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Docker client: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &EventMonitor{
|
|
client: cli,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
callback: callback,
|
|
socketPath: socketPath,
|
|
enforceNetworkValidation: enforceNetworkValidation,
|
|
}, nil
|
|
}
|
|
|
|
// Start begins monitoring Docker events
|
|
func (em *EventMonitor) Start() error {
|
|
logger.Debug("Starting Docker event monitoring")
|
|
|
|
// Filter for container events we care about
|
|
eventFilters := filters.NewArgs()
|
|
eventFilters.Add("type", "container")
|
|
// eventFilters.Add("event", "create")
|
|
eventFilters.Add("event", "start")
|
|
eventFilters.Add("event", "stop")
|
|
// eventFilters.Add("event", "destroy")
|
|
// eventFilters.Add("event", "die")
|
|
// eventFilters.Add("event", "pause")
|
|
// eventFilters.Add("event", "unpause")
|
|
|
|
// Start listening for events
|
|
eventCh, errCh := em.client.Events(em.ctx, events.ListOptions{
|
|
Filters: eventFilters,
|
|
})
|
|
|
|
go func() {
|
|
defer func() {
|
|
if err := em.client.Close(); err != nil {
|
|
logger.Error("Error closing Docker client: %v", err)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
logger.Debug("Docker event received: %s %s for container %s", event.Action, event.Type, event.Actor.ID[:12])
|
|
|
|
// Fetch updated container list and trigger callback
|
|
go em.handleEvent(event)
|
|
|
|
case err := <-errCh:
|
|
if err != nil && err != context.Canceled {
|
|
logger.Error("Docker event stream error: %v", err)
|
|
// Try to reconnect after a brief delay
|
|
time.Sleep(5 * time.Second)
|
|
if em.ctx.Err() == nil {
|
|
logger.Info("Attempting to reconnect to Docker event stream")
|
|
eventCh, errCh = em.client.Events(em.ctx, events.ListOptions{
|
|
Filters: eventFilters,
|
|
})
|
|
}
|
|
}
|
|
return
|
|
|
|
case <-em.ctx.Done():
|
|
logger.Info("Docker event monitoring stopped")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleEvent processes a Docker event and triggers the callback with updated container list
|
|
func (em *EventMonitor) handleEvent(event events.Message) {
|
|
// Add a small delay to ensure Docker has fully processed the event
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
containers, err := ListContainers(em.socketPath, em.enforceNetworkValidation)
|
|
if err != nil {
|
|
logger.Error("Failed to list containers after Docker event %s: %v", event.Action, err)
|
|
return
|
|
}
|
|
|
|
logger.Debug("Triggering callback with %d containers after Docker event %s", len(containers), event.Action)
|
|
em.callback(containers)
|
|
}
|
|
|
|
// Stop stops the event monitoring
|
|
func (em *EventMonitor) Stop() {
|
|
logger.Info("Stopping Docker event monitoring")
|
|
if em.cancel != nil {
|
|
em.cancel()
|
|
}
|
|
}
|