continuous-integration/drone/push Build is passing
Replace ContainerInspect with ContainerList and name filters for exact and prefix matching. This improves efficiency and correctness by leveraging Docker's filtering capabilities, matching the old shell script's grep behavior more accurately. Add regexp import to properly escape container names in filters.
269 lines
8.2 KiB
Go
269 lines
8.2 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/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"
|
|
|
|
"firewall_containers/network-go/config"
|
|
)
|
|
|
|
// DockerAPI defines the interface for Docker operations, enabling mock implementations for testing
|
|
type DockerAPI interface {
|
|
Close() error
|
|
EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error
|
|
RemoveNetwork(ctx context.Context, networkName string) error
|
|
ConnectContainer(ctx context.Context, containerName, networkName, ip string) error
|
|
DisconnectContainer(ctx context.Context, containerName, networkName string) error
|
|
InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error)
|
|
WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error
|
|
GetContainerPID(ctx context.Context, containerName string) (int, error)
|
|
AddRouteInContainer(ctx context.Context, containerName, network, gateway string) error
|
|
FindContainerName(ctx context.Context, name, selector string) (string, error)
|
|
}
|
|
|
|
// Client wraps the Docker SDK client
|
|
type Client struct {
|
|
cli *client.Client
|
|
}
|
|
|
|
// Ensure Client implements DockerAPI
|
|
var _ DockerAPI = (*Client)(nil)
|
|
|
|
// NewClient creates a new Docker client
|
|
func NewClient() (*Client, error) {
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
|
}
|
|
return &Client{cli: cli}, nil
|
|
}
|
|
|
|
// Close closes the Docker client
|
|
func (c *Client) Close() error {
|
|
return c.cli.Close()
|
|
}
|
|
|
|
// EnsureNetwork creates a Docker network if it does not already exist
|
|
func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error {
|
|
existingNetworks, err := c.cli.NetworkList(ctx, network.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("name", netCfg.NetworkName)),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list networks: %w", err)
|
|
}
|
|
|
|
for _, n := range existingNetworks {
|
|
if n.Name == netCfg.NetworkName {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
_, ipNet, err := net.ParseCIDR(netCfg.Subnet)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse subnet %s: %w", netCfg.Subnet, err)
|
|
}
|
|
|
|
gatewayIP := net.ParseIP(netCfg.Gateway)
|
|
if gatewayIP == nil {
|
|
return fmt.Errorf("failed to parse gateway IP %s", netCfg.Gateway)
|
|
}
|
|
|
|
createOpts := network.CreateOptions{
|
|
Driver: "bridge",
|
|
IPAM: &network.IPAM{
|
|
Config: []network.IPAMConfig{
|
|
{
|
|
Subnet: ipNet.String(),
|
|
Gateway: gatewayIP.String(),
|
|
},
|
|
},
|
|
},
|
|
Labels: map[string]string{
|
|
"managed-by": "firewall-network-go",
|
|
},
|
|
Attachable: true,
|
|
}
|
|
|
|
resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err)
|
|
}
|
|
|
|
_ = resp
|
|
return nil
|
|
}
|
|
|
|
// RemoveNetwork removes a Docker network
|
|
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
|
|
err := c.cli.NetworkRemove(ctx, networkName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove network %s: %w", networkName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConnectContainer connects a container to a network with a specific IP
|
|
func (c *Client) ConnectContainer(ctx context.Context, containerName, networkName, ip string) error {
|
|
endpointSettings := &network.EndpointSettings{
|
|
IPAMConfig: &network.EndpointIPAMConfig{
|
|
IPv4Address: ip,
|
|
},
|
|
}
|
|
|
|
err := c.cli.NetworkConnect(ctx, networkName, containerName, endpointSettings)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DisconnectContainer disconnects a container from a network
|
|
func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error {
|
|
err := c.cli.NetworkDisconnect(ctx, networkName, containerName, true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InspectContainer returns the container's details
|
|
func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) {
|
|
container, err := c.cli.ContainerInspect(ctx, containerName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
|
|
}
|
|
return &container, nil
|
|
}
|
|
|
|
// WaitForContainerRunning waits until a container is in running state, with a timeout
|
|
func (c *Client) WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("timeout waiting for container %s to be running", containerName)
|
|
case <-ticker.C:
|
|
container, err := c.cli.ContainerInspect(ctx, containerName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if container.State != nil && container.State.Running {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetContainerPID returns the PID of a container for nsenter operations
|
|
func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) {
|
|
cont, err := c.cli.ContainerInspect(ctx, containerName)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
|
|
}
|
|
if cont.State == nil || !cont.State.Running {
|
|
return 0, fmt.Errorf("container %s is not running", containerName)
|
|
}
|
|
return cont.State.Pid, nil
|
|
}
|
|
|
|
// AddRouteInContainer adds a route inside a container's network namespace using nsenter
|
|
func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network, gateway string) error {
|
|
pid, err := c.GetContainerPID(ctx, containerName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get PID for container %s: %w", containerName, err)
|
|
}
|
|
|
|
args := []string{
|
|
"-t", fmt.Sprintf("%d", pid),
|
|
"-n", "--",
|
|
"ip", "route", "add", network, "via", gateway,
|
|
}
|
|
|
|
cmd := exec.Command("nsenter", args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add route in container %s: %w\noutput: %s", containerName, err, string(output))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FindContainerName attempts to find a running container by name or selector.
|
|
// First tries exact name match, then exact selector, then prefix matching
|
|
// (matching the old shell script's grep $D"-" behavior).
|
|
func (c *Client) FindContainerName(ctx context.Context, name, selector string) (string, error) {
|
|
// Try exact name match using ContainerList with a name filter
|
|
// Docker API name filter does an exact match by default
|
|
containers, err := c.cli.ContainerList(ctx, container.ListOptions{
|
|
Filters: filters.NewArgs(
|
|
filters.Arg("name", "^/?"+regexp.QuoteMeta(name)+"$"),
|
|
filters.Arg("status", "running"),
|
|
),
|
|
})
|
|
if err == nil && len(containers) > 0 {
|
|
cName := strings.TrimPrefix(containers[0].Names[0], "/")
|
|
return cName, nil
|
|
}
|
|
|
|
// Try exact selector match
|
|
if selector != "" && selector != name {
|
|
containers, err = c.cli.ContainerList(ctx, container.ListOptions{
|
|
Filters: filters.NewArgs(
|
|
filters.Arg("name", "^/?"+regexp.QuoteMeta(selector)+"$"),
|
|
filters.Arg("status", "running"),
|
|
),
|
|
})
|
|
if err == nil && len(containers) > 0 {
|
|
cName := strings.TrimPrefix(containers[0].Names[0], "/")
|
|
return cName, nil
|
|
}
|
|
}
|
|
|
|
// Try prefix matching on both name and selector (shell script behavior: grep $D"-")
|
|
candidates := []string{name, selector}
|
|
for _, candidate := range candidates {
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
// Extract prefix before first dash if present
|
|
prefix := candidate
|
|
if strings.Contains(candidate, "-") {
|
|
prefix = candidate[:strings.Index(candidate, "-")]
|
|
}
|
|
|
|
containers, err = c.cli.ContainerList(ctx, container.ListOptions{
|
|
Filters: filters.NewArgs(
|
|
filters.Arg("name", prefix+"-"),
|
|
filters.Arg("status", "running"),
|
|
),
|
|
})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, c := range containers {
|
|
for _, cName := range c.Names {
|
|
cName = strings.TrimPrefix(cName, "/")
|
|
return cName, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("no running container found matching name=%q selector=%q", name, selector)
|
|
}
|