// Copyright 2020 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package docker

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/go-units"
	"golang.org/x/sys/unix"

	"github.com/charmbracelet/log"
	dockerTypes "github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	networkapi "github.com/docker/docker/api/types/network"
	dockerC "github.com/docker/docker/client"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/pkg/jsonmessage"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/dustin/go-humanize"
	"github.com/google/shlex"
	clabexec "github.com/srl-labs/containerlab/exec"
	clablinks "github.com/srl-labs/containerlab/links"
	clabruntime "github.com/srl-labs/containerlab/runtime"
	clabtypes "github.com/srl-labs/containerlab/types"
	clabutils "github.com/srl-labs/containerlab/utils"
	clabutilsprogress "github.com/srl-labs/containerlab/utils/progress"
	"github.com/vishvananda/netlink"
	"golang.org/x/mod/semver"
)

const (
	RuntimeName    = "docker"
	sysctlBase     = "/proc/sys"
	defaultTimeout = 30 * time.Second
	rLimitMaxValue = 1048576
	// defaultDockerNetwork is a name of a docker network that docker uses by default when creating
	// containers.
	defaultDockerNetwork = "bridge"

	natUnprotectedValue         = "nat-unprotected"
	bridgeNameOption            = "com.docker.network.bridge.name"
	bridgeGatewayModeIPv4Option = "com.docker.network.bridge.gateway_mode_ipv4"
	bridgeGatewayModeIPv6Option = "com.docker.network.bridge.gateway_mode_ipv6"
)

// DeviceMapping represents the device mapping between the host and the container.
type DeviceMapping struct {
	PathOnHost        string
	PathInContainer   string
	CgroupPermissions string
}

func init() {
	clabruntime.Register(RuntimeName, func() clabruntime.ContainerRuntime {
		return &DockerRuntime{
			mgmt: new(clabtypes.MgmtNet),
		}
	})
}

type DockerRuntime struct {
	config  clabruntime.RuntimeConfig
	Client  *dockerC.Client
	mgmt    *clabtypes.MgmtNet
	version string
}

func (d *DockerRuntime) Init(opts ...clabruntime.RuntimeOption) error {
	var err error
	log.Debug("Runtime: Docker")
	d.Client, err = dockerC.NewClientWithOpts(dockerC.FromEnv, dockerC.WithAPIVersionNegotiation())
	if err != nil {
		return err
	}
	for _, o := range opts {
		o(d)
	}
	d.config.VerifyLinkParams = clablinks.NewVerifyLinkParams()

	// Retrieve Docker version to determine whether to apply certain overrides
	dockerVersion, err := d.Client.ServerVersion(context.Background())
	if err != nil {
		return err
	}

	// Needs to be proper SemVer for comparison
	d.version = "v" + dockerVersion.Version

	return nil
}

func (d *DockerRuntime) WithKeepMgmtNet() {
	d.config.KeepMgmtNet = true
}
func (*DockerRuntime) GetName() string                     { return RuntimeName }
func (d *DockerRuntime) Config() clabruntime.RuntimeConfig { return d.config }

// Mgmt return management network struct of a runtime.
func (d *DockerRuntime) Mgmt() *clabtypes.MgmtNet { return d.mgmt }

func (d *DockerRuntime) WithConfig(cfg *clabruntime.RuntimeConfig) {
	d.config.Timeout = cfg.Timeout
	d.config.Debug = cfg.Debug
	d.config.GracefulShutdown = cfg.GracefulShutdown
	if d.config.Timeout <= 0 {
		d.config.Timeout = defaultTimeout
	}
}

func (d *DockerRuntime) WithMgmtNet(n *clabtypes.MgmtNet) {
	d.mgmt = n
	// return if MTU value was set by a user via config file
	if n.MTU != 0 {
		return
	}

	// if the network name is not "clab", which is a default network name used by containerlab
	// then likely a user wants to keep the custom network mtu value.
	// however, if the network name is "clab" and mtu is not provided in the topology file
	// we should detect the mtu value of the default docker network and set it for the clab network
	// as most often this is desired.
	if n.Network == "clab" {
		netRes, err := d.Client.NetworkInspect(
			context.TODO(),
			defaultDockerNetwork,
			networkapi.InspectOptions{},
		)
		if err != nil {
			d.mgmt.MTU = 1500
			log.Debugf("an error occurred when trying to detect docker default network mtu")
		} else if mtu, ok := netRes.Options["com.docker.network.driver.mtu"]; ok {
			log.Debugf("detected docker network mtu value - %s", mtu)
			d.mgmt.MTU, err = strconv.Atoi(mtu)
			if err != nil {
				log.Errorf("Error parsing MTU value of %q as int", mtu)
			}
		}

		// If MTU is still 0 after inspection (e.g. the default bridge network has no
		// explicit MTU option set), fall back to 1500 to avoid recording MTU=0 in the
		// Docker network options which results in a non-functional network.
		if d.mgmt.MTU == 0 {
			log.Debugf("docker default network MTU not detected, falling back to 1500")
			d.mgmt.MTU = 1500
		}
	}

	// if bridge was not set in the topo file, find out the bridge name
	// used by the network used in the topology
	if d.mgmt.Bridge == "" && d.mgmt.Network != "" {
		// fetch the network by the name set in the topo and populate the bridge name used by this
		// network
		netRes, err := d.Client.NetworkInspect(
			context.TODO(),
			d.mgmt.Network,
			networkapi.InspectOptions{},
		)
		// if the network is successfully found, set the bridge used by it
		if err == nil {
			if name, exists := netRes.Options[bridgeNameOption]; exists {
				d.mgmt.Bridge = name
			} else {
				d.mgmt.Bridge = "br-" + netRes.ID[:12]
			}
			log.Debugf(
				"detected network name in use: %s, backed by a bridge %s",
				d.mgmt.Network,
				d.mgmt.Bridge,
			)
		}
	}
}

// CreateNet creates a docker network or reusing if it exists.
func (d *DockerRuntime) CreateNet(ctx context.Context) (err error) {
	nctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
	defer cancel()

	// linux bridge name that is used by docker network
	bridgeName := d.mgmt.Bridge

	log.Debugf("Checking if docker network %q exists", d.mgmt.Network)
	netResource, err := d.Client.NetworkInspect(nctx, d.mgmt.Network, networkapi.InspectOptions{})
	switch {
	case dockerC.IsErrNotFound(err):
		bridgeName, err = d.createMgmtBridge(nctx, bridgeName)
		if err != nil {
			return err
		}
	case err == nil:
		log.Debugf("network %q was found. Reusing it...", d.mgmt.Network)
		bridgeName, err = bridgeNameFromInspect(&netResource, d.mgmt.Network)
		if err != nil {
			return err
		}

	default:
		return err
	}

	if d.mgmt.Bridge == "" {
		d.mgmt.Bridge = bridgeName
	}

	// get management bridge v4/6 addresses and save it under mgmt struct
	// so that nodes can use this information prior to being deployed
	// this was added to allow mgmt network gw ip to be available in a startup config templation
	// step (ceos)
	d.mgmt.IPv4Gw, d.mgmt.IPv6Gw, err = getMgmtBridgeIPs(bridgeName, &netResource)
	if err != nil {
		return err
	}

	log.Debugf("Docker network %q, bridge name %q", d.mgmt.Network, bridgeName)

	return d.postCreateNetActions()
}

// skipcq: GO-R1005
func (d *DockerRuntime) createMgmtBridge( //nolint: funlen
	nctx context.Context,
	bridgeName string,
) (string, error) {
	var err error
	log.Debug("Network does not exist", "name", d.mgmt.Network)
	log.Info("Creating docker network",
		"name", d.mgmt.Network,
		"IPv4 subnet", d.mgmt.IPv4Subnet,
		"IPv6 subnet", d.mgmt.IPv6Subnet,
		"MTU", d.mgmt.MTU)

	enableIPv6 := false
	var ipamConfig []networkapi.IPAMConfig

	var v4gw, v6gw string
	// check if IPv4/6 addr are assigned to a mgmt bridge
	if d.mgmt.Bridge != "" {
		v4gw, v6gw, err = clabutils.FirstLinkIPs(d.mgmt.Bridge)
		if err != nil {
			// only return error if the error is not about link not found
			// we will create the bridge if it doesn't exist
			if !errors.As(err, &netlink.LinkNotFoundError{}) {
				return "", err
			}
		}
		log.Debugf("bridge %q has ipv4 addr of %q and ipv6 addr of %q", d.mgmt.Bridge, v4gw, v6gw)
	}

	if d.mgmt.IPv4Subnet != "" && d.mgmt.IPv4Subnet != "auto" {
		if d.mgmt.IPv4Gw != "" {
			v4gw = d.mgmt.IPv4Gw
		}
		ipamCfg := networkapi.IPAMConfig{
			Subnet:  d.mgmt.IPv4Subnet,
			Gateway: v4gw,
		}
		if d.mgmt.IPv4Range != "" {
			ipamCfg.IPRange = d.mgmt.IPv4Range
		}
		ipamConfig = append(ipamConfig, ipamCfg)
	}

	var ipv6_subnet string
	if d.mgmt.IPv6Subnet == "auto" {
		ipv6_subnet, err = clabutils.GenerateIPv6ULASubnet()
		if err != nil {
			return "", err
		}
	} else {
		ipv6_subnet = d.mgmt.IPv6Subnet
	}

	if ipv6_subnet != "" {
		if d.mgmt.IPv6Gw != "" {
			v6gw = d.mgmt.IPv6Gw
		}
		ipamCfg := networkapi.IPAMConfig{
			Subnet:  ipv6_subnet,
			Gateway: v6gw,
		}
		if d.mgmt.IPv6Range != "" {
			ipamCfg.IPRange = d.mgmt.IPv6Range
		}
		ipamConfig = append(ipamConfig, ipamCfg)
		enableIPv6 = true
	}

	ipam := &networkapi.IPAM{
		Driver: "default",
		Config: ipamConfig,
	}

	netwOpts := map[string]string{
		"com.docker.network.driver.mtu": strconv.Itoa(d.mgmt.MTU),
	}

	if bridgeName != "" {
		netwOpts[bridgeNameOption] = bridgeName
	}

	// nat-unprotected mode is needed starting in Docker release 28 to access all ports without
	// exposing them explicitly
	// see https://github.com/srl-labs/containerlab/issues/2638
	if semver.Compare(d.version, "v28.0.0") > 0 {
		log.Debug("Using Docker version 28 or later, enabling NAT unprotected mode on bridge")
		netwOpts[bridgeGatewayModeIPv4Option] = natUnprotectedValue
		netwOpts[bridgeGatewayModeIPv6Option] = natUnprotectedValue
	}

	// Merge in bridge network driver options from topology file
	for k, v := range d.mgmt.DriverOpts {
		log.Debug("Adding bridge network driver option", "option", k, "value", v)
		netwOpts[k] = v
	}

	opts := networkapi.CreateOptions{
		Driver:     "bridge",
		EnableIPv6: new(enableIPv6),
		IPAM:       ipam,
		Internal:   false,
		Attachable: false,
		Labels: map[string]string{
			"containerlab": "",
		},
		Options: netwOpts,
	}

	netCreateResponse, err := d.Client.NetworkCreate(nctx, d.mgmt.Network, opts)
	if err != nil {
		// Another clab process created the same management network between
		// our Inspect and our Create. Re-inspect and reuse it.
		if errdefs.IsConflict(err) {
			log.Debug("docker network was created concurrently by another clab deploy; reusing it",
				"name", d.mgmt.Network)

			netResource, ierr := d.Client.NetworkInspect(
				nctx, d.mgmt.Network, networkapi.InspectOptions{},
			)
			if ierr != nil {
				return "", fmt.Errorf(
					"re-inspect %q after concurrent create: %w", d.mgmt.Network, ierr,
				)
			}

			return bridgeNameFromInspect(&netResource, d.mgmt.Network)
		}

		// Handle subnet overlap error
		if strings.Contains(strings.ToLower(err.Error()), strings.ToLower("Pool overlaps")) ||
			strings.Contains(strings.ToLower(err.Error()), strings.ToLower("subnet")) {
			networksAndAddresses := map[string][]string{}
			nets, listErr := d.Client.NetworkList(nctx, networkapi.ListOptions{})
			if listErr != nil {
				return "", fmt.Errorf(
					"failed to list Docker networks while handling subnet overlap error: %w (original error: %v)",
					listErr,
					err,
				)
			}
			for _, n := range nets {
				for _, cfg := range n.IPAM.Config {
					// store existing networks and their subnets
					networksAndAddresses[n.Name] = append(networksAndAddresses[n.Name], cfg.Subnet)
					if cfg.Subnet == d.mgmt.IPv4Subnet || cfg.Subnet == d.mgmt.IPv6Subnet {
						return "", fmt.Errorf(
							"subnet %s already in use by Docker network %q. See https://containerlab.dev/manual/network/",
							cfg.Subnet,
							n.Name,
						)
					}
				}
			}
			// fallback: no exact match, but clarify
			// Show both IPv4 and IPv6 subnets in the error message if present
			requestedSubnets := d.mgmt.IPv4Subnet
			if d.mgmt.IPv6Subnet != "" {
				if requestedSubnets != "" {
					requestedSubnets += ", "
				}
				requestedSubnets += d.mgmt.IPv6Subnet
			}
			return "", fmt.Errorf(
				"requested subnet(s) %s overlap an existing Docker network. Existing networks: %v. Original error: %w. See https://containerlab.dev/manual/network/",
				requestedSubnets,
				networksAndAddresses,
				err,
			)
		}

		return "", err
	}

	if len(netCreateResponse.ID) < 12 {
		return "", fmt.Errorf("could not get bridge ID")
	}
	// when bridge is not set by a user explicitly
	// we use the 12 chars of docker net as its name
	if bridgeName == "" {
		bridgeName = "br-" + netCreateResponse.ID[:12]
	}
	return bridgeName, nil
}

// bridgeNameFromInspect resolves the underlying linux bridge name from a docker network inspect
// response.
func bridgeNameFromInspect(netResource *networkapi.Inspect, mgmtNetwork string) (string, error) {
	if len(netResource.ID) < 12 {
		return "", fmt.Errorf("could not get bridge ID")
	}

	if mgmtNetwork == "bridge" {
		return "docker0", nil
	}

	if name := netResource.Options[bridgeNameOption]; name != "" {
		return name, nil
	}

	return "br-" + netResource.ID[:12], nil
}

// getMgmtBridgeIPs gets the management bridge v4/6 addresses.
func getMgmtBridgeIPs(
	bridgeName string,
	netResource *networkapi.Inspect,
) (v4, v6 string, err error) {
	if v4, v6, err = clabutils.FirstLinkIPs(bridgeName); err != nil {
		log.Warn(
			"failed gleaning v4 and/or v6 addresses from bridge via netlink," +
				" falling back to docker network inspect data",
		)

		for _, ipamEntry := range netResource.IPAM.Config {
			addr := ipamEntry.Gateway

			// this is terribly janky, *but* we know docker will be only putting valid v4/v6
			// addresses in here and we only care about differentiating between the two flavors
			if strings.Count(addr, ".") == 3 {
				v4 = addr
			} else if netResource.EnableIPv6 {
				v6 = addr
			}

			if v4 != "" && v6 != "" {
				break
			}
		}
	}

	// didnt find any gateways, fallthrough to returning the error
	if v4 == "" && v6 == "" {
		return "", "", err
	}

	return v4, v6, nil
}

// postCreateNetActions performs additional actions after the network has been created.
func (d *DockerRuntime) postCreateNetActions() (err error) {
	log.Debug("Disable RPF check on the docker host")
	err = setSysctl("net/ipv4/conf/all/rp_filter", 0)
	if err != nil {
		return fmt.Errorf("failed to disable RP filter on docker host for the 'all' scope: %v", err)
	}
	err = setSysctl("net/ipv4/conf/default/rp_filter", 0)
	if err != nil {
		return fmt.Errorf(
			"failed to disable RP filter on docker host for the 'default' scope: %v",
			err,
		)
	}

	log.Debugf("Enable LLDP on the linux bridge %s", d.mgmt.Bridge)
	file := "/sys/class/net/" + d.mgmt.Bridge + "/bridge/group_fwd_mask"

	err = os.WriteFile(file, []byte(strconv.Itoa(16384)), 0o640) // skipcq: GO-S2306
	if err != nil {
		log.Warnf("failed to enable LLDP on docker bridge: %v", err)
	}

	// TODO: consider if that is useful or not later.
	// Enable nf_call_iptables and nf_call_ip6tables on the management bridge so that
	// bridged traffic passes through netfilter/conntrack. Without this, per-bridge sysfs
	// settings (which default to 0 on custom bridges) override the global
	// net.bridge.bridge-nf-call-iptables sysctl on newer kernels, breaking Docker's
	// embedded DNS proxy: the masquerade conntrack mapping is not maintained for return
	// packets, so DNS responses arriving at the container's eth0 are never delivered to
	// the waiting socket even though they are visible to tcpdump.
	// for _, knob := range []string{"nf_call_iptables", "nf_call_ip6tables"} {
	// 	p := "/sys/class/net/" + d.mgmt.Bridge + "/bridge/" + knob
	// 	if werr := os.WriteFile(p, []byte("1"), 0o640); werr != nil { // skipcq: GO-S2306
	// 		log.Warnf("failed to set %s on bridge %s: %v", knob, d.mgmt.Bridge, werr)
	// 	}
	// }

	// Note: we intentionally do NOT disable TX checksum offloading on the bridge interface
	// itself. Doing so corrupts UDP/TCP checksums on NAT-forwarded packets (e.g. DNS replies
	// from external resolvers) because after SNAT de-translation the kernel performs an
	// incremental checksum update on top of a CHECKSUM_PARTIAL skb, but the bridge can no
	// longer finalize it. TX checksum offload is disabled on the host-side of each veth
	// pair instead (see links/link_veth.go), which is sufficient to ensure software
	// checksum computation for traffic entering containers.
	err = d.installMgmtNetworkFwdRule()
	if err != nil {
		log.Warnf("errors during iptables rules install: %v", err)
	}

	return nil
}

// DeleteNet deletes a docker bridge.
func (d *DockerRuntime) DeleteNet(ctx context.Context) (err error) {
	network := d.mgmt.Network
	if network == "bridge" || d.config.KeepMgmtNet {
		log.Debugf("Skipping deletion of %q network", network)
		return nil
	}
	nctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
	defer cancel()

	nres, err := d.Client.NetworkInspect(ctx, network, networkapi.InspectOptions{})
	if err != nil {
		return err
	}
	numEndpoints := len(nres.Containers)
	if numEndpoints > 0 {
		if d.config.Debug {
			log.Debugf(
				"network %q has %d active endpoints, deletion skipped",
				d.mgmt.Network,
				numEndpoints,
			)
			for _, endp := range nres.Containers {
				log.Debugf("%q is connected to %s", endp.Name, network)
			}
		}
		return nil
	}
	err = d.Client.NetworkRemove(nctx, network)
	if err != nil {
		return err
	}

	err = d.deleteMgmtNetworkFwdRule()
	if err != nil {
		log.Warnf("errors during iptables rules removal: %v", err)
	}

	return nil
}

// PauseContainer Pauses a container identified by its name.
func (d *DockerRuntime) PauseContainer(ctx context.Context, cID string) error {
	return d.Client.ContainerPause(ctx, cID)
}

// UnpauseContainer UnPauses / resumes a container identified by its name.
func (d *DockerRuntime) UnpauseContainer(ctx context.Context, cID string) error {
	return d.Client.ContainerUnpause(ctx, cID)
}

// CreateContainer creates a docker container (but does not start it).
func (d *DockerRuntime) CreateContainer( //nolint: funlen
	ctx context.Context,
	node *clabtypes.NodeConfig,
) (string, error) { // skipcq: GO-R1005
	log.Info("Creating container", "name", node.ShortName)
	nctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
	defer cancel()

	cmd, err := shlex.Split(node.Cmd)
	if err != nil {
		return "", err
	}

	var entrypoint []string
	if node.Entrypoint != "" {
		entrypoint, err = shlex.Split(node.Entrypoint)
		if err != nil {
			return "", err
		}
	}

	containerConfig := &container.Config{
		Image:        node.Image,
		Entrypoint:   entrypoint,
		Cmd:          cmd,
		Env:          clabutils.ConvertEnvs(node.Env),
		AttachStdout: true,
		AttachStderr: true,
		Hostname:     node.GetHostname(),
		Tty:          true,
		OpenStdin:    true,
		User:         node.User,
		Labels:       node.Labels,
		ExposedPorts: node.PortSet,
		MacAddress:   node.MacAddress,
	}

	if node.Healthcheck != nil {
		containerConfig.Healthcheck = &container.HealthConfig{
			Test:        node.Healthcheck.Test,
			Interval:    node.Healthcheck.GetIntervalDuration(),
			Timeout:     node.Healthcheck.GetTimeoutDuration(),
			StartPeriod: node.Healthcheck.GetStartPeriodDuration(),
			Retries:     node.Healthcheck.Retries,
		}
	}

	var resources container.Resources
	if node.Memory != "" {
		mem, err := humanize.ParseBytes(node.Memory)
		if err != nil {
			return "", err
		}
		resources.Memory = int64(mem)
	}
	if node.CPU != 0 {
		resources.CPUQuota = int64(node.CPU * 100000)
		resources.CPUPeriod = 100000
	}
	if node.CPUSet != "" {
		resources.CpusetCpus = node.CPUSet
	}
	var rlimit unix.Rlimit
	if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rlimit); err != nil {
		log.Warnf("Unable to retrieve rlimit_NOFILE value: %v", err)
		rlimit.Max = rLimitMaxValue
	}
	if rlimit.Max > rLimitMaxValue {
		rlimit.Max = rLimitMaxValue
	}
	// Iterate through each Device
	for _, str := range node.Devices {
		// Perform your action on each string
		mappings := container.DeviceMapping{}
		mappings.PathOnHost = str
		mappings.PathInContainer = str
		mappings.CgroupPermissions = "rwm"
		resources.Devices = append(resources.Devices, mappings)
	}

	ulimit := units.Ulimit{
		Name: "nofile",
		Hard: int64(rlimit.Max),
		Soft: int64(rlimit.Max),
	}
	resources.Ulimits = []*units.Ulimit{&ulimit}

	volumeMounts, err := d.convertVolumeMounts(node.Volumes)
	if err != nil {
		return "", fmt.Errorf("failed to convert volume mounts: %w", err)
	}

	containerHostConfig := &container.HostConfig{
		Binds:        node.Binds,
		Mounts:       volumeMounts,
		PortBindings: node.PortBindings,
		Sysctls:      node.Sysctls,
		Privileged:   node.Privileged,
		Tmpfs:        node.Tmpfs,
		PidMode:      "",
		SecurityOpt:  node.SecurityOpts,
		// Network mode will be defined below via switch
		NetworkMode: "",
		ExtraHosts:  node.ExtraHosts, // add static /etc/hosts entries
		Resources:   resources,
		AutoRemove:  node.AutoRemove,
	}

	if node.DNS != nil {
		containerHostConfig.DNS = node.DNS.Servers
		containerHostConfig.DNSSearch = node.DNS.Search
		containerHostConfig.DNSOptions = node.DNS.Options
	}

	containerNetworkingConfig := &networkapi.NetworkingConfig{}

	if node.ShmSize != "" {
		shmsize, err := humanize.ParseBytes(node.ShmSize)
		if err != nil {
			return "", err
		}
		containerHostConfig.ShmSize = int64(shmsize)
	}

	if len(node.CapAdd) > 0 {
		containerHostConfig.CapAdd = append(containerHostConfig.CapAdd, node.CapAdd...)
	}

	if err := d.processNetworkMode(
		ctx,
		containerNetworkingConfig,
		containerHostConfig,
		containerConfig,
		node,
	); err != nil {
		return "", err
	}

	if err := d.processPidMode(node, containerHostConfig); err != nil {
		return "", err
	}

	if err := d.processCgroupnsMode(node, containerHostConfig); err != nil {
		return "", err
	}
	d.processCgroupParent(node, containerHostConfig)

	// regular linux containers may benefit from automatic restart on failure
	// note, that veth pairs added to this container (outside of eth0) will be lost on restart
	if !node.AutoRemove && node.RestartPolicy != "" {
		var rp container.RestartPolicyMode
		switch node.RestartPolicy {
		case "no":
			rp = container.RestartPolicyDisabled
		case "always":
			rp = container.RestartPolicyAlways
		case "on-failure":
			rp = container.RestartPolicyOnFailure
		case "unless-stopped":
			rp = container.RestartPolicyUnlessStopped
		}
		containerHostConfig.RestartPolicy.Name = rp
	}

	cont, err := d.Client.ContainerCreate(
		nctx,
		containerConfig,
		containerHostConfig,
		containerNetworkingConfig,
		nil,
		node.LongName,
	)
	log.Debugf("Container %q create response: %+v", node.ShortName, cont)
	if err != nil {
		return "", err
	}
	return cont.ID, nil
}

// GetNSPath inspects a container by its name/id and returns a netns path using the pid of a
// container.
func (d *DockerRuntime) GetNSPath(ctx context.Context, cID string) (string, error) {
	nctx, cancelFn := context.WithTimeout(ctx, d.config.Timeout)
	defer cancelFn()
	cJSON, err := d.Client.ContainerInspect(nctx, cID)
	if err != nil {
		return "", err
	}

	displayName := strings.TrimPrefix(cJSON.Name, "/")
	if displayName == "" {
		displayName = cID
	}

	// If the container is running we use /proc/$pid/ns/net.
	// For stopped containers the PID is 0 and we fall back to a /run/netns/<name> link
	// (containerlab creates and manages it, and node lifecycle may repoint it to a parking netns).
	if cJSON.State != nil && cJSON.State.Pid > 0 {
		nspath := filepath.Join("/proc", strconv.Itoa(cJSON.State.Pid), "ns/net")
		if clabutils.FileOrDirExists(nspath) {
			return nspath, nil
		}
	}

	runNetns := filepath.Join("/run/netns", cID)
	if clabutils.FileOrDirExists(runNetns) {
		return runNetns, nil
	}

	d.logExitedContainerOutput(nctx, cID, displayName, cJSON.Config.Tty)
	return "", fmt.Errorf("namespace path not available for container %q", displayName)
}

// LogNonRunningContainerOutput implements runtime.ContainerRuntime.
func (d *DockerRuntime) LogNonRunningContainerOutput(ctx context.Context, containerName string) {
	nctx, cancelFn := context.WithTimeout(ctx, d.config.Timeout)
	defer cancelFn()
	cJSON, err := d.Client.ContainerInspect(nctx, containerName)
	if err != nil {
		return
	}
	if cJSON.State.Running {
		return
	}
	displayName := strings.TrimPrefix(cJSON.Name, "/")
	if displayName == "" {
		displayName = containerName
	}
	d.logExitedContainerOutput(nctx, cJSON.ID, displayName, cJSON.Config.Tty)
}

// logExitedContainerOutput fetches recent stdout/stderr from a non-running container and prints it
// so deploy failures (e.g. bad cmd) surface in the CLI without a separate docker logs step.
// When the container was created with a TTY, Docker returns a raw stream; otherwise logs are
// stdout/stderr multiplexed (see stdcopy).
func (d *DockerRuntime) logExitedContainerOutput(
	ctx context.Context,
	cID, displayName string,
	tty bool,
) {
	logReader, err := d.Client.ContainerLogs(ctx, cID, container.LogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		Tail:       "100",
	})
	if err != nil {
		log.Warnf("could not read logs for exited container %q: %v", displayName, err)
		return
	}
	defer logReader.Close()

	var combined string
	if tty {
		var buf bytes.Buffer
		if _, err := io.Copy(&buf, logReader); err != nil {
			log.Warnf("could not read logs for exited container %q: %v", displayName, err)
			return
		}
		combined = strings.TrimSpace(buf.String())
	} else {
		var outBuf, errBuf bytes.Buffer
		if _, err := stdcopy.StdCopy(&outBuf, &errBuf, logReader); err != nil {
			log.Warnf("could not decode logs for exited container %q: %v", displayName, err)
			return
		}
		combined = strings.TrimSpace(outBuf.String() + errBuf.String())
	}

	if combined == "" {
		log.Errorf("container %q exited immediately with no log output", displayName)
		return
	}

	log.Errorf("container %q exited; container output:\n%s", displayName, combined)
}

// PullImage pulls the container image using the provided image pull policy value.
func (d *DockerRuntime) PullImage(
	ctx context.Context,
	imageName string,
	pullPolicy clabtypes.PullPolicyValue,
) error {
	log.Debugf("Looking up %s Docker image", imageName)

	canonicalImageName := clabutils.GetCanonicalImageName(imageName)

	_, b, _ := d.Client.ImageInspectWithRaw(ctx, canonicalImageName)
	switch pullPolicy {
	case clabtypes.PullPolicyNever:
		if b == nil {
			// image not found but pull policy = never
			return fmt.Errorf(
				"image %s not found locally, and image-pull-policy=%s prevents containerlab from pulling it",
				imageName,
				pullPolicy,
			)
		}
		// image present, all good
		log.Debugf("Image %s present, skip pulling", imageName)
		return nil
	case clabtypes.PullPolicyIfNotPresent:
		if b != nil {
			// pull policy == IfNotPresent and image is present
			log.Debugf("Image %s present, skip pulling", imageName)
			return nil
		}
	}

	// If Image doesn't exist or pullPolicy=always, we need to pull it
	authString := ""

	// get docker config based on an empty path (default docker config path will be assumed)
	dockerConfig, err := GetDockerConfig("")
	if err != nil {
		log.Debug("docker config file not found")
	} else {
		authString, err = GetDockerAuth(dockerConfig, canonicalImageName)
		if err != nil {
			return err
		}
	}

	// The progress reporter renders this image's pull as a single aggregated bar.
	// When pulls are orchestrated concurrently the reporter is shared via the
	// context; for standalone pulls (e.g. tool images) we spin up our own.
	reporter := clabutilsprogress.FromContext(ctx)
	if reporter == nil {
		var stop func()

		reporter, stop = clabutilsprogress.New(os.Stderr)
		defer stop()
	}

	reporter.Start(canonicalImageName)

	reader, err := d.Client.ImagePull(ctx, canonicalImageName, image.PullOptions{
		RegistryAuth: authString,
	})
	if err != nil {
		reporter.Done(canonicalImageName, err)
		return err
	}
	defer reader.Close()

	err = streamPullProgress(reader, reporter, canonicalImageName)
	reporter.Done(canonicalImageName, err)

	return err
}

type pullLayer struct {
	frac   float64
	status string
}

func streamPullProgress(
	reader io.Reader,
	r clabutilsprogress.Reporter,
	image string,
) error {
	dec := json.NewDecoder(reader)
	layers := map[string]*pullLayer{}

	var lastPercent float64

	var lastStatus string

	for {
		var jm jsonmessage.JSONMessage

		if err := dec.Decode(&jm); err != nil {
			if errors.Is(err, io.EOF) {
				return nil
			}

			return err
		}

		if jm.Error != nil {
			return errors.New(jm.Error.Message)
		}

		// Messages without an ID are top-level lines ("Pulling from ...",
		// "Digest: ...", "Status: ...") that don't contribute to layer progress.
		if jm.ID == "" {
			continue
		}

		l, ok := layers[jm.ID]
		if !ok {
			l = &pullLayer{}
			layers[jm.ID] = l
		}

		l.status = jm.Status
		l.frac = layerFraction(jm.Status, jm.Progress)

		percent, status := aggregateProgress(layers)

		// Throttle updates: only report meaningful changes.
		if status != lastStatus || percent-lastPercent >= 0.01 || percent >= 1 {
			r.Update(image, status, percent)
			lastPercent = percent
			lastStatus = status
		}
	}
}

// layerFraction maps a single layer's status/progress to a completion fraction
// in [0,1], splitting the work evenly between the download and extract phases so
// the bar advances smoothly across both.
func layerFraction(status string, p *jsonmessage.JSONProgress) float64 {
	switch status {
	case "Already exists", "Pull complete":
		return 1.0
	case "Extracting":
		return 0.5 + 0.5*progressFraction(p)
	case "Download complete", "Verifying Checksum":
		return 0.5
	case "Downloading":
		return 0.5 * progressFraction(p)
	default: // "Pulling fs layer", "Waiting", "Retrying", ...
		return 0.0
	}
}

func progressFraction(p *jsonmessage.JSONProgress) float64 {
	if p == nil || p.Total <= 0 {
		return 0
	}

	f := float64(p.Current) / float64(p.Total)

	switch {
	case f < 0:
		return 0
	case f > 1:
		return 1
	default:
		return f
	}
}

// aggregateProgress reduces all known layers into one overall percent and a
// single status word for the image. Docker emits a "Pulling fs layer" line for
// every layer up front, so the layer count is stable early and the mean is
// effectively monotonic.
func aggregateProgress(layers map[string]*pullLayer) (float64, string) {
	if len(layers) == 0 {
		return 0, "Waiting"
	}

	var sum float64

	var anyDownloading, anyExtracting bool

	allComplete := true

	for _, l := range layers {
		sum += l.frac

		switch l.status {
		case "Downloading":
			anyDownloading = true
		case "Extracting":
			anyExtracting = true
		}

		if l.frac < 1 {
			allComplete = false
		}
	}

	status := "Waiting"

	switch {
	case anyDownloading:
		status = "Downloading"
	case anyExtracting:
		status = "Extracting"
	case allComplete:
		status = "Pulled"
	}

	return sum / float64(len(layers)), status
}

// StartContainer starts a docker container.
func (d *DockerRuntime) StartContainer(
	ctx context.Context,
	cID string,
	node clabruntime.Node,
) (any, error) {
	nctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
	defer cancel()

	nodecfg := node.Config()

	log.Debugf("Start container: %q", nodecfg.LongName)

	// ContainerStart occasionally returns 'file exists' error on the IPv6 gateway
	// route in large topologies with an IPv6 management network configured.
	// A short retry seems to mitigate the error.
	const (
		ipv6GwRetries      = 3
		ipv6GwRetryBackoff = 100 * time.Millisecond
	)

	var err error
	for attempt := range ipv6GwRetries {
		err = d.Client.ContainerStart(nctx, cID, container.StartOptions{})
		if err == nil || !isIPv6GatewayExistsErr(err) {
			break
		}
		log.Debugf("transient IPv6 gateway error starting %q (attempt %d/%d): %v",
			nodecfg.ShortName, attempt+1, ipv6GwRetries, err)
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(ipv6GwRetryBackoff << attempt):
		}
	}
	if err != nil {
		return nil, err
	}
	log.Debugf("Container started: %q", nodecfg.LongName)
	err = d.postStartActions(ctx, cID, nodecfg)
	return nil, err
}

// isIPv6GatewayExistsErr reports whether err is the transient "file exists" error.
func isIPv6GatewayExistsErr(err error) bool {
	s := err.Error()
	return strings.Contains(s, "failed to set IPv6 gateway") && strings.Contains(s, "file exists")
}

// postStartActions performs misc. tasks that are needed after the container starts.
func (d *DockerRuntime) postStartActions(
	ctx context.Context,
	cID string,
	node *clabtypes.NodeConfig,
) error {
	nspath, err := d.GetNSPath(ctx, cID)
	if err != nil {
		return err
	}
	err = clabutils.LinkContainerNS(nspath, node.LongName)
	return err
}

// ListContainers lists all containers using the provided filters.
func (d *DockerRuntime) ListContainers(ctx context.Context,
	gfilters []*clabtypes.GenericFilter,
) ([]clabruntime.GenericContainer, error) {
	ctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
	defer cancel()

	filter := d.buildFilterString(gfilters)

	ctrs, err := d.Client.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filter,
	})
	if err != nil {
		return nil, err
	}

	var nr []networkapi.Inspect
	if d.mgmt.Network == "" {
		nctx, cancel := context.WithTimeout(ctx, d.config.Timeout)
		defer cancel()

		// fetch containerlab created networks
		f := filters.NewArgs()

		f.Add("label", "containerlab")

		nr, err = d.Client.NetworkList(nctx, networkapi.ListOptions{
			Filters: f,
		})
		if err != nil {
			return nil, err
		}

		// fetch default bridge network
		f = filters.NewArgs()

		f.Add("name", "bridge")

		bridgenet, err := d.Client.NetworkList(nctx, networkapi.ListOptions{
			Filters: f,
		})
		if err != nil {
			return nil, err
		}

		nr = append(nr, bridgenet...)
	}

	return d.produceGenericContainerList(ctx, ctrs, nr)
}

func (d *DockerRuntime) GetContainer(
	ctx context.Context,
	cID string,
) (*clabruntime.GenericContainer, error) {
	ctrs, err := d.ListContainers(ctx, []*clabtypes.GenericFilter{
		{
			FilterType: "name",
			Match:      cID,
		},
	})
	if err != nil {
		return nil, err
	}

	if len(ctrs) != 1 {
		return nil, fmt.Errorf("found unexpected number of containers: %d", len(ctrs))
	}

	return &ctrs[0], nil
}

func (*DockerRuntime) buildFilterString(gFilters []*clabtypes.GenericFilter) filters.Args {
	filter := filters.NewArgs()
	for _, gF := range gFilters {
		filterStr := ""

		switch {
		case gF.Operator == "exists":
			filterStr = gF.Field
		case gF.FilterType == "name":
			filterStr = fmt.Sprintf(
				"^%s$",
				gF.Match,
			) // this regexp ensure we have an exact match for name
		default:
			filterStr = gF.Field + gF.Operator + gF.Match
		}

		log.Debugf("Filter key: %s, filter value: %s", gF.FilterType, filterStr)
		filter.Add(gF.FilterType, filterStr)
	}

	return filter
}

// Transform docker-specific to generic container format.
func (d *DockerRuntime) produceGenericContainerList(
	ctx context.Context,
	inputContainers []dockerTypes.Container,
	inputNetworkResources []networkapi.Inspect,
) ([]clabruntime.GenericContainer, error) {
	var result []clabruntime.GenericContainer

	for idx := range inputContainers {
		i := inputContainers[idx]

		var names []string
		for _, n := range i.Names {
			// the docker names seem to always come with a "/" in the first position
			// we trim it as slashes are not required in a single host setting
			names = append(names, strings.TrimLeft(n, "/"))
		}

		ctr := clabruntime.GenericContainer{
			Names:           names,
			ID:              i.ID,
			ShortID:         i.ID[:12],
			Image:           i.Image,
			State:           i.State,
			Status:          i.Status,
			Labels:          i.Labels,
			NetworkSettings: clabruntime.GenericMgmtIPs{},
		}

		ctr.Ports = make([]*clabtypes.GenericPortBinding, len(i.Ports))
		for x, p := range i.Ports {
			ctr.Ports[x] = genericPortFromDockerPort(p)
		}

		ctr.SetRuntime(d)

		bridgeName := d.mgmt.Network

		var err error
		ctr.Pid, err = d.containerPid(ctx, i.ID)
		if errdefs.IsNotFound(err) {
			// Concurrent destroy removed it between List and Inspect.
			continue
		}
		if err != nil {
			return nil, err
		}

		// if bridgeName is empty, try to find a network created by clab that the container is
		// connected to
		if bridgeName == "" && inputNetworkResources != nil {
			for idx := range inputNetworkResources {
				nr := inputNetworkResources[idx]

				if _, ok := i.NetworkSettings.Networks[nr.Name]; ok {
					bridgeName = nr.Name
					break
				}
			}
		}

		// check if global bridge name belongs to a container network settings
		// applicable for ext-containers
		_, ok := i.NetworkSettings.Networks[bridgeName]

		// if by now we failed to find a docker network name using the network resources created by
		// docker or (in case of external containers) the clab's bridge name doesn't belong to the
		// container
		// we take whatever the first network is listed in the original container network settings
		// this is to derive the network name if the network is not created by clab
		if bridgeName == "" || !ok {
			// only if there is a single network associated with the container
			if len(i.NetworkSettings.Networks) == 1 {
				for n := range i.NetworkSettings.Networks {
					bridgeName = n
				}
			}
		}

		if bridgeName != "" {
			ctr.NetworkName = bridgeName
		} else {
			ctr.NetworkName = "unknown"
		}

		if ifcfg, ok := i.NetworkSettings.Networks[bridgeName]; ok {
			ctr.NetworkSettings.IPv4addr = ifcfg.IPAddress
			ctr.NetworkSettings.IPv4pLen = ifcfg.IPPrefixLen
			ctr.NetworkSettings.IPv6addr = ifcfg.GlobalIPv6Address
			ctr.NetworkSettings.IPv6pLen = ifcfg.GlobalIPv6PrefixLen
			ctr.NetworkSettings.IPv4Gw = ifcfg.Gateway
			ctr.NetworkSettings.IPv6Gw = ifcfg.IPv6Gateway
		}

		// populating mounts information
		var mount clabruntime.ContainerMount
		for _, m := range i.Mounts {
			mount.Source = m.Source
			mount.Destination = m.Destination
		}
		ctr.Mounts = append(ctr.Mounts, mount)

		result = append(result, ctr)
	}

	return result, nil
}

func genericPortFromDockerPort(p dockerTypes.Port) *clabtypes.GenericPortBinding {
	return &clabtypes.GenericPortBinding{
		HostIP:        p.IP,
		HostPort:      int(p.PublicPort),
		ContainerPort: int(p.PrivatePort),
		Protocol:      p.Type,
	}
}

// Exec executes cmd on container identified with id and returns stdout, stderr bytes and an error.
func (d *DockerRuntime) Exec(
	ctx context.Context,
	cID string,
	execCmd *clabexec.ExecCmd,
) (*clabexec.ExecResult, error) {
	cont, err := d.Client.ContainerInspect(ctx, cID)
	if err != nil {
		return nil, err
	}
	execID, err := d.Client.ContainerExecCreate(ctx, cID, container.ExecOptions{
		User:         "0", // root
		AttachStderr: true,
		AttachStdout: true,
		Cmd:          execCmd.GetCmd(),
	})
	if err != nil {
		log.Errorf("failed to create exec in container %s: %v", cont.Name, err)
		return nil, err
	}
	log.Debugf("%s exec created %v", cont.Name, cID)

	rsp, err := d.Client.ContainerExecAttach(ctx, execID.ID, container.ExecStartOptions{})
	if err != nil {
		log.Errorf("failed exec in container %s: %v", cont.Name, err)
		return nil, err
	}
	defer rsp.Close()
	log.Debugf("%s exec attached %v", cont.Name, cID)

	var outBuf, errBuf bytes.Buffer
	outputDone := make(chan error)

	go func() {
		_, err = stdcopy.StdCopy(&outBuf, &errBuf, rsp.Reader)
		outputDone <- err
	}()

	select {
	case err := <-outputDone:
		if err != nil {
			return nil, err
		}
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	// retrieve the exit code via inspect
	execInspect, err := d.Client.ContainerExecInspect(ctx, execID.ID)
	if err != nil {
		return nil, err
	}

	execResult := clabexec.NewExecResult(execCmd)

	// set the result fields in the exec struct
	execResult.SetReturnCode(execInspect.ExitCode)
	execResult.SetStdOut(outBuf.Bytes())
	execResult.SetStdErr(errBuf.Bytes())
	return execResult, nil
}

// ExecNotWait executes cmd on container identified with id but doesn't wait for output nor attaches
// stdout/err.
func (d *DockerRuntime) ExecNotWait(
	_ context.Context,
	cID string,
	execCmd *clabexec.ExecCmd,
) error {
	execConfig := container.ExecOptions{
		Tty:          false,
		AttachStdout: false,
		AttachStderr: false,
		Cmd:          execCmd.GetCmd(),
	}
	respID, err := d.Client.ContainerExecCreate(context.Background(), cID, execConfig)
	if err != nil {
		return err
	}

	execStartCheck := container.ExecStartOptions{}
	rsp, err := d.Client.ContainerExecAttach(context.Background(), respID.ID, execStartCheck)
	if err != nil {
		return err
	}
	rsp.Close()
	return nil
}

// DeleteContainer tries to stop a container then remove it.
func (d *DockerRuntime) DeleteContainer(ctx context.Context, cID string) error {
	var err error
	force := !d.config.GracefulShutdown
	if d.config.GracefulShutdown {
		log.Infof("Stopping container: %s", cID)
		timeout := int(d.config.Timeout.Seconds())
		err = d.Client.ContainerStop(ctx, cID, container.StopOptions{Timeout: &timeout})
		if err != nil {
			log.Errorf("could not stop container %q: %v", cID, err)
			force = true
		}
	}
	log.Debugf("Removing container: %s", cID)
	err = d.Client.ContainerRemove(
		ctx,
		cID,
		container.RemoveOptions{Force: force, RemoveVolumes: true},
	)
	if err != nil {
		return err
	}
	log.Info("Removed container", "name", cID)

	return nil
}

// setSysctl writes sysctl data by writing to a specific file.
func setSysctl(sysctl string, newVal int) error {
	return os.WriteFile(path.Join(sysctlBase, sysctl), []byte(strconv.Itoa(newVal)), 0o600)
}

func (d *DockerRuntime) StopContainer(
	ctx context.Context,
	name string,
	stopSignal clabtypes.Signal,
) error {
	timeout := int(d.config.Timeout.Seconds())
	stopOpts := container.StopOptions{Timeout: &timeout}

	if stopSignal != "" {
		stopOpts.Signal = string(stopSignal)
		log.Debugf("using custom stop signal %q for container %q", stopSignal, name)
	}

	return d.Client.ContainerStop(ctx, name, stopOpts)
}

// GetHostsPath returns fs path to a file which is mounted as /etc/hosts into a given container.
func (d *DockerRuntime) GetHostsPath(ctx context.Context, cID string) (string, error) {
	inspect, err := d.Client.ContainerInspect(ctx, cID)
	if err != nil {
		return "", err
	}
	hostsPath := inspect.HostsPath
	log.Debugf("Method GetHostsPath was called with a resulting path %q", hostsPath)
	return hostsPath, nil
}

func (*DockerRuntime) processPidMode(
	node *clabtypes.NodeConfig,
	containerHostConfig *container.HostConfig,
) error {
	pidMode := container.PidMode(node.PidMode)
	if !pidMode.Valid() {
		return fmt.Errorf("pid mode %q invalid", node.PidMode)
	}

	containerHostConfig.PidMode = pidMode

	return nil
}

func (*DockerRuntime) processCgroupnsMode(
	node *clabtypes.NodeConfig,
	containerHostConfig *container.HostConfig,
) error {
	cgroupnsMode := container.CgroupnsMode(node.CgroupnsMode)
	if !cgroupnsMode.Valid() {
		return fmt.Errorf("cgroupns mode %q invalid", node.CgroupnsMode)
	}

	containerHostConfig.CgroupnsMode = cgroupnsMode

	return nil
}

func (*DockerRuntime) processCgroupParent(
	node *clabtypes.NodeConfig,
	containerHostConfig *container.HostConfig,
) {
	containerHostConfig.CgroupParent = node.CgroupParent
}

func (d *DockerRuntime) processNetworkMode(
	ctx context.Context,
	containerNetworkingConfig *networkapi.NetworkingConfig,
	containerHostConfig *container.HostConfig,
	containerConfig *container.Config,
	node *clabtypes.NodeConfig,
) error {
	netMode := strings.SplitN(node.NetworkMode, ":", 2)

	switch netMode[0] {
	case "none":
		containerHostConfig.NetworkMode = "none"
	// clab allows its containers to be attached to a netns of another container
	// this can be a container that is managed by clab, or an external container.
	case "container":
		// We expect exactly two arguments in this case ("container" keyword & cont. name/ID)
		if len(netMode) != 2 {
			return fmt.Errorf(
				"container network mode was specified for container %q, but we failed to parse the network-mode instruction: %q",
				node.ShortName,
				netMode,
			)
		}

		// also cont. ID shouldn't be empty
		if netMode[1] == "" {
			return fmt.Errorf(
				"container network mode was specified for container %q, referenced container name appears to be empty: %q",
				node.ShortName,
				netMode,
			)
		}

		// to support using network stack of containers that are scheduled by clab
		// or containers deployed externally (i.e. by k8s kind)
		// we first check if container name referenced in network-mode node property exists
		// internally, aka within clab topology. If it doesn't, we assume that users referred to an
		// external container. extract lab/topo prefix to craft a full container name if an internal
		// container is referenced.
		contName := strings.SplitN(node.LongName, node.ShortName, 2)[0] + netMode[1]

		_, err := d.GetContainer(ctx, contName)
		if err != nil {
			log.Debugf(
				"container %q was not found by its name, assuming it is exists externally with unprefixed",
				contName,
			)

			// container doesn't exist internally, so we assume it exists externally
			// with a non-prefixed name.
			contName = netMode[1]

			if _, err := d.GetContainer(ctx, contName); err != nil {
				return fmt.Errorf(
					"container %q is referenced in network-mode, but was not found",
					netMode[1],
				)
			}
		}

		// compile the net spec
		containerHostConfig.NetworkMode = container.NetworkMode("container:" + contName)

		// unset the hostname as it is not supported in this case
		containerConfig.Hostname = ""
	case "host":
		containerHostConfig.NetworkMode = "host"
	default:
		containerHostConfig.NetworkMode = container.NetworkMode(d.mgmt.Network)

		containerNetworkingConfig.EndpointsConfig = map[string]*networkapi.EndpointSettings{
			d.mgmt.Network: {
				IPAMConfig: &networkapi.EndpointIPAMConfig{
					IPv4Address: node.MgmtIPv4Address,
					IPv6Address: node.MgmtIPv6Address,
				},
				Aliases:    node.Aliases,
				MacAddress: node.MacAddress,
			},
		}
	}

	return nil
}

// GetContainerStatus retrieves the ContainerStatus of the named container.
func (d *DockerRuntime) GetContainerStatus(
	ctx context.Context,
	cID string,
) clabruntime.ContainerStatus {
	inspect, err := d.Client.ContainerInspect(ctx, cID)
	if err != nil {
		return clabruntime.NotFound
	}
	switch inspect.State.Status {
	case "running":
		return clabruntime.Running
	case "paused":
		return clabruntime.Paused
	case "created":
		return clabruntime.Created
	case "restarting":
		return clabruntime.Restarting
	case "removing":
		return clabruntime.Removing
	case "exited", "dead":
		return clabruntime.Stopped
	default:
		return clabruntime.NotFound
	}
}

// containerPid returns the pid of a container by its ID using inspect.
func (d *DockerRuntime) containerPid(ctx context.Context, cID string) (int, error) {
	inspect, err := d.Client.ContainerInspect(ctx, cID)
	if err != nil {
		return 0, fmt.Errorf("container %q cannot be found: %w", cID, err)
	}
	return inspect.State.Pid, nil
}

// IsHealthy returns true is the container is reported as being healthy, false otherwise.
func (d *DockerRuntime) IsHealthy(ctx context.Context, cID string) (bool, error) {
	inspect, err := d.Client.ContainerInspect(ctx, cID)
	if err != nil {
		return false, err
	}
	// catch no healthchecks defined
	if inspect.State.Health == nil {
		return false, fmt.Errorf("no health information available for container: %s", cID)
	}
	return inspect.State.Health.Status == "healthy", nil
}

func (d *DockerRuntime) WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error {
	stdin, err := d.Client.ContainerAttach(ctx, cID, container.AttachOptions{
		Stdin:  true,
		Stream: true,
		Stdout: true,
		Stderr: true,
	})
	if err != nil {
		return err
	}

	log.Debugf("Writing to %s: %v", cID, data)

	_, err = stdin.Conn.Write(data)
	if err != nil {
		return err
	}

	return stdin.Conn.Close()
}

func (d *DockerRuntime) CheckConnection(ctx context.Context) error {
	_, err := d.Client.Ping(ctx)
	if err != nil {
		if errors.Is(err, os.ErrPermission) {
			return fmt.Errorf(
				"could not connect to the Docker runtime, make sure your user is part of the `docker` group: %w",
				err,
			)
		} else {
			return fmt.Errorf("could not connect to the Docker runtime: %w", err)
		}
	}

	return nil
}

func (*DockerRuntime) GetRuntimeSocket() (string, error) {
	return "/var/run/docker.sock", nil
}

func (*DockerRuntime) GetCooCBindMounts() clabtypes.Binds {
	return clabtypes.Binds{
		clabtypes.NewBind("/var/lib/docker/containers", "/var/lib/docker/containers", ""),
		clabtypes.NewBind("/run/netns", "/run/netns", ""),
	}
}

func (d *DockerRuntime) StreamLogs(
	ctx context.Context,
	containerName string,
) (io.ReadCloser, error) {
	logOptions := container.LogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		Follow:     true,
		Timestamps: false,
		Since:      time.Now().Format(time.RFC3339),
	}

	logStream, err := d.Client.ContainerLogs(ctx, containerName, logOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to get container logs: %v", err)
	}

	return logStream, nil
}

// InspectImage returns detailed information about a container image.
func (d *DockerRuntime) InspectImage(
	ctx context.Context,
	imageName string,
) (*clabruntime.ImageInspect, error) {
	imageData, _, err := d.Client.ImageInspectWithRaw(ctx, imageName)
	if err != nil {
		return nil, fmt.Errorf("failed to inspect image %s: %w", imageName, err)
	}

	// Convert Docker image inspect to our runtime ImageInspect
	labels := imageData.Config.Labels
	if labels == nil {
		labels = make(map[string]string)
	}

	// Extract GraphDriver data
	graphDriver := clabruntime.GraphDriver{
		Name: imageData.GraphDriver.Name,
		Data: clabruntime.GraphDriverData{},
	}

	// GraphDriver.Data is a map[string]string in Docker
	if upperDir, ok := imageData.GraphDriver.Data["UpperDir"]; ok {
		graphDriver.Data.UpperDir = upperDir
	}
	if workDir, ok := imageData.GraphDriver.Data["WorkDir"]; ok {
		graphDriver.Data.WorkDir = workDir
	}
	if mergedDir, ok := imageData.GraphDriver.Data["MergedDir"]; ok {
		graphDriver.Data.MergedDir = mergedDir
	}

	return &clabruntime.ImageInspect{
		ID: imageData.ID,
		Config: clabruntime.ImageConfig{
			Labels: labels,
		},
		RootFS: clabruntime.RootFS{
			Type:   imageData.RootFS.Type,
			Layers: imageData.RootFS.Layers,
		},
		GraphDriver: graphDriver,
	}, err
}

func (d *DockerRuntime) CopyToContainer(
	ctx context.Context,
	cID string,
	dstPath string,
	srcPath string,
) error {
	tarBuf, err := clabutils.FileToTarStream(dstPath, srcPath)
	if err != nil {
		return fmt.Errorf("error creating tar stream from source file %s: %w", srcPath, err)
	}

	opts := container.CopyToContainerOptions{
		AllowOverwriteDirWithFile: false,
	}

	log.Debugf("copying path %v -> %v to container %v", srcPath, dstPath, cID)
	err = d.Client.CopyToContainer(ctx, cID, filepath.Dir(dstPath), tarBuf, opts)
	if err != nil {
		return fmt.Errorf(
			"error copying path %v -> %v to container (%v): %w",
			srcPath,
			dstPath,
			cID,
			err,
		)
	}

	return nil
}

// convertVolumeMount takes a list of volumes in docker/clab format (src:dest:options)
// and converts them into Docker API mount.Mount structures.
func (d *DockerRuntime) convertVolumeMounts(mounts []string) ([]mount.Mount, error) {
	if len(mounts) == 0 {
		return nil, nil
	}

	docker_mounts := make([]mount.Mount, 0, len(mounts))

	for _, vol := range mounts {
		spec, err := clabtypes.NewVolumeFromString(vol)
		if err != nil {
			return nil, err
		}

		opts := clabtypes.ParseVolumeOptions(spec.Options())
		if len(opts.Unknown) > 0 {
			return nil, fmt.Errorf("unsupported volume option(s) %q in %q", opts.Unknown, vol)
		}

		m := mount.Mount{
			Type:     mount.TypeVolume,
			Target:   spec.Dst(),
			ReadOnly: opts.ReadOnly,
		}

		if opts.NoCopy {
			m.VolumeOptions = &mount.VolumeOptions{
				NoCopy: true,
			}
		}

		if spec.Src() != "" {
			m.Source = spec.Src()
		}

		log.Debugf("parsed volume %q into mount %+v", vol, m)
		docker_mounts = append(docker_mounts, m)
	}

	return docker_mounts, nil
}
