200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
} |