/*
Copyright 2019 The Skaffold Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package docker

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/containerd/errdefs"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/image"
	"github.com/moby/moby/api/types/jsonstream"
	"github.com/moby/moby/api/types/mount"
	"github.com/moby/moby/api/types/network"
	"github.com/moby/moby/api/types/volume"
	"github.com/moby/moby/client"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"

	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker/progress"
	sErrors "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/errors"
	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util/term"
)

const (
	retries   = 5
	sleepTime = 1 * time.Second
)

type ContainerRun struct {
	Image       string
	User        string
	Command     []string
	Mounts      []mount.Mount
	Env         []string
	BeforeStart func(context.Context, string) error
}

type ContainerCreateOpts struct {
	Name            string
	Network         string
	VolumesFrom     []string
	Wait            bool
	Bindings        network.PortMap
	Mounts          []mount.Mount
	ContainerConfig *container.Config
	VerifyTestName  string
	Labels          map[string]string
}

// LocalDaemon talks to a local Docker API.
type LocalDaemon interface {
	Close() error
	ExtraEnv() []string
	ServerVersion(ctx context.Context) (client.ServerVersionResult, error)
	ConfigFile(ctx context.Context, image string) (*v1.ConfigFile, error)
	Build(ctx context.Context, out io.Writer, workspace string, artifact string, a *latest.DockerArtifact, opts BuildOptions) (string, error)
	ContainerLogs(ctx context.Context, w *io.PipeWriter, id string) error
	ContainerExists(ctx context.Context, name string) bool
	ContainerInspect(ctx context.Context, id string) (container.InspectResponse, error)
	ContainerList(ctx context.Context, options client.ContainerListOptions) ([]container.Summary, error)
	Push(ctx context.Context, out io.Writer, ref string) (string, error)
	Pull(ctx context.Context, out io.Writer, ref string, platform v1.Platform) error
	Load(ctx context.Context, out io.Writer, input io.Reader, ref string) (string, error)
	Run(ctx context.Context, out io.Writer, opts ContainerCreateOpts) (<-chan container.WaitResponse, <-chan error, string, error)
	Delete(ctx context.Context, out io.Writer, id string) error
	Tag(ctx context.Context, image, ref string) error
	TagWithImageID(ctx context.Context, ref string, imageID string) (string, error)
	ImageID(ctx context.Context, ref string) (string, error)
	ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error)
	ImageRemove(ctx context.Context, image string, opts client.ImageRemoveOptions) ([]image.DeleteResponse, error)
	ImageExists(ctx context.Context, ref string) bool
	ImageList(ctx context.Context, ref string) ([]image.Summary, error)
	NetworkCreate(ctx context.Context, name string, labels map[string]string) error
	NetworkRemove(ctx context.Context, name string) error
	NetworkList(ctx context.Context, opts client.NetworkListOptions) ([]network.Summary, error)
	Prune(ctx context.Context, images []string, pruneChildren bool) ([]string, error)
	DiskUsage(ctx context.Context) (uint64, error)
	RawClient() client.APIClient
	VolumeCreate(ctx context.Context, opts client.VolumeCreateOptions) (volume.Volume, error)
	VolumeRemove(ctx context.Context, id string) error
	Stop(ctx context.Context, id string, stopTimeout *time.Duration) error
	Remove(ctx context.Context, id string) error
}

// BuildOptions provides parameters related to the LocalDaemon build.
type BuildOptions struct {
	Tag            string
	Mode           config.RunMode
	ExtraBuildArgs map[string]*string
}

type localDaemon struct {
	cfg            Config
	forceRemove    bool
	apiClient      client.APIClient
	extraEnv       []string
	imageCache     map[string]*v1.ConfigFile
	imageCacheLock sync.Mutex
}

// NewLocalDaemon creates a new LocalDaemon.
func NewLocalDaemon(apiClient client.APIClient, extraEnv []string, forceRemove bool, cfg Config) LocalDaemon {
	return &localDaemon{
		cfg:         cfg,
		apiClient:   apiClient,
		extraEnv:    extraEnv,
		forceRemove: forceRemove,
		imageCache:  make(map[string]*v1.ConfigFile),
	}
}

// ExtraEnv returns the env variables needed to point at this local Docker
// eg. minikube. This has be set in addition to the current environment.
func (l *localDaemon) ExtraEnv() []string {
	return l.extraEnv
}

// PushResult gives the information on an image that has been pushed.
type PushResult struct {
	Digest string
}

// BuildResult gives the information on an image that has been built.
type BuildResult struct {
	ID string
}

func (l *localDaemon) RawClient() client.APIClient {
	return l.apiClient
}

// Close closes the connection with the local daemon.
func (l *localDaemon) Close() error {
	return l.apiClient.Close()
}

func (l *localDaemon) VolumeCreate(ctx context.Context, opts client.VolumeCreateOptions) (volume.Volume, error) {
	res, err := l.apiClient.VolumeCreate(ctx, opts)
	return res.Volume, err
}

func (l *localDaemon) VolumeRemove(ctx context.Context, id string) error {
	_, err := l.apiClient.VolumeRemove(ctx, id, client.VolumeRemoveOptions{})
	return err
}

func (l *localDaemon) ContainerInspect(ctx context.Context, id string) (container.InspectResponse, error) {
	res, err := l.apiClient.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
	return res.Container, err
}

func (l *localDaemon) ContainerList(ctx context.Context, options client.ContainerListOptions) ([]container.Summary, error) {
	res, err := l.apiClient.ContainerList(ctx, options)
	if err != nil {
		return nil, err
	}
	return res.Items, nil
}

// ContainerLogs streams logs line by line from a container in the local daemon to the provided PipeWriter.
func (l *localDaemon) ContainerLogs(ctx context.Context, w *io.PipeWriter, id string) error {
	r, err := l.apiClient.ContainerLogs(ctx, id, client.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true})
	if err != nil {
		return err
	}
	rd := bufio.NewReader(r)
	for {
		s, err := rd.ReadString('\n')
		if err != nil {
			if err != io.EOF {
				return err
			}
			return nil
		}
		w.Write([]byte(s))
	}
}

func (l *localDaemon) ContainerExists(ctx context.Context, name string) bool {
	_, err := l.apiClient.ContainerInspect(ctx, name, client.ContainerInspectOptions{})
	return err == nil
}

// Delete stops, removes, and prunes a running container
func (l *localDaemon) Delete(ctx context.Context, out io.Writer, id string) error {
	if _, err := l.apiClient.ContainerStop(ctx, id, client.ContainerStopOptions{}); err != nil {
		log.Entry(ctx).Debugf("unable to stop running container: %s", err.Error())
	}
	if _, err := l.apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{}); err != nil {
		log.Entry(ctx).Warnf("unable to remove container: %s", err.Error())
	}
	_, err := l.apiClient.ContainerPrune(ctx, client.ContainerPruneOptions{})
	if err != nil {
		return fmt.Errorf("pruning removed container: %w", err)
	}
	return nil
}

// Run creates a container from a given image reference, and returns a wait channel and the container ID.
func (l *localDaemon) Run(ctx context.Context, out io.Writer, opts ContainerCreateOpts) (<-chan container.WaitResponse, <-chan error, string, error) {
	if opts.ContainerConfig == nil {
		return nil, nil, "", fmt.Errorf("cannot call Run with empty container config")
	}
	c, err := l.apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
		Config: opts.ContainerConfig,
		HostConfig: &container.HostConfig{
			NetworkMode:  container.NetworkMode(opts.Network),
			VolumesFrom:  opts.VolumesFrom,
			PortBindings: opts.Bindings,
			Mounts:       opts.Mounts,
		},
		Name: opts.Name,
	})
	if err != nil {
		return nil, nil, "", err
	}
	if _, err := l.apiClient.ContainerStart(ctx, c.ID, client.ContainerStartOptions{}); err != nil {
		return nil, nil, "", err
	}
	if opts.Wait {
		waitResult := l.apiClient.ContainerWait(ctx, c.ID, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning})
		return waitResult.Result, waitResult.Error, c.ID, nil
	}
	return nil, nil, c.ID, nil
}

func (l *localDaemon) NetworkCreate(ctx context.Context, name string, labels map[string]string) error {
	nr, err := l.apiClient.NetworkList(ctx, client.NetworkListOptions{})
	if err != nil {
		return err
	}
	for _, netSummary := range nr.Items {
		if netSummary.Name == name {
			return nil
		}
	}

	r, err := l.apiClient.NetworkCreate(ctx, name, client.NetworkCreateOptions{Labels: labels})
	if err != nil {
		return err
	}
	for _, warning := range r.Warning {
		if warning != "" {
			log.Entry(ctx).Warn(warning)
		}
	}
	return nil
}

func (l *localDaemon) NetworkRemove(ctx context.Context, name string) error {
	_, err := l.apiClient.NetworkRemove(ctx, name, client.NetworkRemoveOptions{})
	return err
}

func (l *localDaemon) NetworkList(ctx context.Context, opts client.NetworkListOptions) ([]network.Summary, error) {
	res, err := l.apiClient.NetworkList(ctx, opts)
	if err != nil {
		return nil, err
	}
	return res.Items, nil
}

// ServerVersion retrieves the version information from the server.
func (l *localDaemon) ServerVersion(ctx context.Context) (client.ServerVersionResult, error) {
	return l.apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
}

// ConfigFile retrieves and caches image configurations.
func (l *localDaemon) ConfigFile(ctx context.Context, image string) (*v1.ConfigFile, error) {
	l.imageCacheLock.Lock()
	defer l.imageCacheLock.Unlock()

	cachedCfg, present := l.imageCache[image]
	if present {
		return cachedCfg, nil
	}

	cfg := &v1.ConfigFile{}

	_, raw, err := l.ImageInspectWithRaw(ctx, image)
	if err == nil {
		if err := json.Unmarshal(raw, cfg); err != nil {
			return nil, err
		}
	} else {
		cfg, err = RetrieveRemoteConfig(image, l.cfg, v1.Platform{})
		if err != nil {
			return nil, err
		}
	}

	l.imageCache[image] = cfg

	return cfg, nil
}

func (l *localDaemon) CheckCompatible(a *latest.DockerArtifact) error {
	if len(a.Secrets) > 0 || a.SSH != "" {
		return fmt.Errorf("docker build options, secrets and ssh, require BuildKit - set `useBuildkit: true` in your config, or run with `DOCKER_BUILDKIT=1`")
	}
	return nil
}

// Build performs a docker build and returns the imageID.
func (l *localDaemon) Build(ctx context.Context, out io.Writer, workspace string, artifact string, a *latest.DockerArtifact, opts BuildOptions) (string, error) {
	log.Entry(ctx).Debugf("Running docker build: context: %s, dockerfile: %s", workspace, a.DockerfilePath)

	if err := l.CheckCompatible(a); err != nil {
		return "", err
	}
	imageInfoEnv, err := EnvTags(opts.Tag)
	if err != nil {
		return "", fmt.Errorf("couldn't parse image tag: %w", err)
	}
	buildArgs, err := EvalBuildArgsWithEnv(opts.Mode, workspace, a.DockerfilePath, a.BuildArgs, opts.ExtraBuildArgs, imageInfoEnv)
	if err != nil {
		return "", fmt.Errorf("unable to evaluate build args: %w", err)
	}

	// Like `docker build`, we ignore the errors
	// See https://github.com/docker/cli/blob/75c1bb1f33d7cedbaf48404597d5bf9818199480/cli/command/image/build.go#L364
	authConfigs, _ := DefaultAuthHelper.GetAllAuthConfigs(ctx)

	buildCtx, buildCtxWriter := io.Pipe()
	go func() {
		err := CreateDockerTarContext(ctx, buildCtxWriter,
			NewBuildConfig(workspace, artifact, a.DockerfilePath, buildArgs), l.cfg)
		if err != nil {
			buildCtxWriter.CloseWithError(fmt.Errorf("creating docker context: %w", err))
			return
		}
		buildCtxWriter.Close()
	}()

	progressOutput := progress.NewProgressOutput(out)
	body := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")

	resp, err := l.apiClient.ImageBuild(ctx, body, client.ImageBuildOptions{
		Tags:        []string{opts.Tag},
		Dockerfile:  a.DockerfilePath,
		BuildArgs:   buildArgs,
		CacheFrom:   a.CacheFrom,
		AuthConfigs: authConfigs,
		Target:      a.Target,
		ForceRemove: l.forceRemove,
		NetworkMode: strings.ToLower(a.NetworkMode),
		ExtraHosts:  a.AddHost,
		NoCache:     a.NoCache,
		PullParent:  a.PullParent,
	})
	if err != nil {
		return "", fmt.Errorf("docker build: %w", err)
	}
	defer resp.Body.Close()

	var imageID string
	auxCallback := func(msg jsonstream.Message) {
		if msg.Aux == nil {
			return
		}

		var result BuildResult
		if err := json.Unmarshal(*msg.Aux, &result); err != nil {
			log.Entry(ctx).Debug("Unable to parse build output:", err)
			return
		}
		imageID = result.ID
	}

	if err := streamDockerMessages(out, resp.Body, auxCallback); err != nil {
		var jm *jsonstream.Error
		if errors.As(err, &jm) {
			return "", fmt.Errorf("docker build failure: %w", err)
		}
		return "", fmt.Errorf("unable to stream build output: %w", err)
	}

	if imageID == "" {
		// Maybe this version of Docker doesn't return the digest of the image
		// that has been built.
		imageID, err = l.ImageID(ctx, opts.Tag)
		if err != nil {
			return "", fmt.Errorf("getting digest: %w", err)
		}
	}

	return imageID, nil
}

// streamDockerMessages streams formatted json output from the docker daemon
func streamDockerMessages(dst io.Writer, src io.Reader, auxCallback func(jsonstream.Message)) error {
	termFd, isTerm := term.IsTerminal(dst)
	return progress.DisplayJSONMessagesStream(src, dst, termFd, isTerm, auxCallback)
}

// Push pushes an image reference to a registry. Returns the image digest.
func (l *localDaemon) Push(ctx context.Context, out io.Writer, ref string) (string, error) {
	registryAuth, err := l.encodedRegistryAuth(ctx, DefaultAuthHelper, ref)
	if err != nil {
		return "", fmt.Errorf("getting auth config for %q: %w", ref, err)
	}

	// Quick check if the image was already pushed (ignore any error).
	if alreadyPushed, digest, err := l.isAlreadyPushed(ctx, ref, registryAuth); alreadyPushed && err == nil {
		return digest, nil
	}

	rc, err := l.apiClient.ImagePush(ctx, ref, client.ImagePushOptions{
		RegistryAuth: registryAuth,
	})
	if err != nil {
		return "", fmt.Errorf("%s %q: %w", sErrors.PushImageErr, ref, err)
	}
	defer rc.Close()

	var digest string
	auxCallback := func(msg jsonstream.Message) {
		if msg.Aux == nil {
			return
		}

		var result PushResult
		if err := json.Unmarshal(*msg.Aux, &result); err != nil {
			log.Entry(ctx).Debug("Unable to parse push output:", err)
			return
		}
		digest = result.Digest
	}

	if err := streamDockerMessages(out, rc, auxCallback); err != nil {
		return "", fmt.Errorf("%s %q: %w", sErrors.PushImageErr, ref, err)
	}

	if digest == "" {
		// Maybe this version of Docker doesn't return the digest of the image
		// that has been pushed.
		digest, err = RemoteDigest(ref, l.cfg, nil)
		if err != nil {
			return "", fmt.Errorf("getting digest: %w", err)
		}
	}

	return digest, nil
}

// isAlreadyPushed quickly checks if the local image has already been pushed.
func (l *localDaemon) isAlreadyPushed(ctx context.Context, ref, registryAuth string) (bool, string, error) {
	localImage, _, err := l.ImageInspectWithRaw(ctx, ref)
	if err != nil {
		return false, "", err
	}

	if len(localImage.RepoDigests) == 0 {
		return false, "", nil
	}

	remoteImage, err := l.apiClient.DistributionInspect(ctx, ref, client.DistributionInspectOptions{
		EncodedRegistryAuth: registryAuth,
	})
	if err != nil {
		return false, "", err
	}
	digest := remoteImage.Descriptor.Digest.String()

	for _, repoDigest := range localImage.RepoDigests {
		if parsed, err := ParseReference(repoDigest); err == nil {
			if parsed.Digest == digest {
				return true, parsed.Digest, nil
			}
		}
	}

	return false, "", nil
}

// Pull pulls an image reference from a registry.
func (l *localDaemon) Pull(ctx context.Context, out io.Writer, ref string, platform v1.Platform) error {
	// We first try pulling the image with credentials.  If that fails then retry
	// without credentials in case the image is public.

	// Set CLOUDSDK_CORE_VERBOSITY to suppress error messages emitted by docker-credential-gcloud
	// when the user is not authenticated or lacks credentials to pull the given image.  The errors
	// are irrelevant when the image is public (e.g., `gcr.io/buildpacks/builder:v1`).`
	// If the image is private, the error from GCR directs the user to the GCR authentication
	// page which provides steps to rememdy the situation.
	if v, found := os.LookupEnv("CLOUDSDK_CORE_VERBOSITY"); found {
		defer os.Setenv("CLOUDSDK_CORE_VERBOSITY", v)
	} else {
		defer os.Unsetenv("CLOUDSDK_CORE_VERBOSITY")
	}
	os.Setenv("CLOUDSDK_CORE_VERBOSITY", "critical")

	// Eargerly create credentials.
	registryAuth, err := l.encodedRegistryAuth(ctx, DefaultAuthHelper, ref)
	// Let's ignore the error because maybe the image is public
	// and can be pulled without credentials.
	var platforms []ocispec.Platform
	if platform.OS != "" {
		platforms = append(platforms, ocispec.Platform{
			OS:           platform.OS,
			OSVersion:    platform.OSVersion,
			Architecture: platform.Architecture,
			Variant:      platform.Variant,
		})
	}

	rc, err := l.apiClient.ImagePull(ctx, ref, client.ImagePullOptions{
		RegistryAuth: registryAuth,
		PrivilegeFunc: func(ctx context.Context) (string, error) {
			// The first pull is unauthorized. There are two situations:
			//   1. if `encodedRegistryAuth()` errored, then `registryAuth == ""` and so we've
			//     tried an anonymous pull which has failed.  So return the original error from
			//     `encodedRegistryAuth()`.
			//   2. If `encodedRegistryAuth()` succeeded (so `err == nil`), then our credential was rejected, so
			//     return "" to retry as an anonymous pull.
			return "", err
		},
		Platforms: platforms,
	})
	if err != nil {
		return fmt.Errorf("pulling image from repository: %w", err)
	}
	defer rc.Close()

	return streamDockerMessages(out, rc, nil)
}

// Load loads an image from a tar file. Returns the imageID for the loaded image.
func (l *localDaemon) Load(ctx context.Context, out io.Writer, input io.Reader, ref string) (string, error) {
	resp, err := l.apiClient.ImageLoad(ctx, input)
	if err != nil {
		return "", fmt.Errorf("loading image into docker daemon: %w", err)
	}
	defer resp.Close()

	if err := streamDockerMessages(out, resp, nil); err != nil {
		return "", fmt.Errorf("reading from image load response: %w", err)
	}

	return l.ImageID(ctx, ref)
}

// Tag adds a tag to an image.
func (l *localDaemon) Tag(ctx context.Context, image, ref string) error {
	_, err := l.apiClient.ImageTag(ctx, client.ImageTagOptions{Source: image, Target: ref})
	return err
}

// For k8s, we need a unique, immutable ID for the image.
// k8s doesn't recognize the imageID or any combination of the image name
// suffixed with the imageID, as a valid image name.
// So, the solution we chose is to create a tag, just for Skaffold, from
// the imageID, and use that in the manifests.
func (l *localDaemon) TagWithImageID(ctx context.Context, ref string, imageID string) (string, error) {
	parsed, err := ParseReference(ref)
	if err != nil {
		return "", err
	}

	if imageID == "" {
		log.Entry(ctx).Debugf("generating tag for %s: empty image id\n", ref)
		return "", nil
	}
	uniqueTag := parsed.BaseName + ":" + strings.TrimPrefix(imageID, "sha256:")
	if err := l.Tag(ctx, imageID, uniqueTag); err != nil {
		return "", err
	}

	return uniqueTag, nil
}

// ImageID returns the image ID for a corresponding reference.
func (l *localDaemon) ImageID(ctx context.Context, ref string) (string, error) {
	image, _, err := l.ImageInspectWithRaw(ctx, ref)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return "", nil
		}
		return "", localDigestGetErr(ref, err)
	}

	return image.ID, nil
}

func (l *localDaemon) ImageExists(ctx context.Context, ref string) bool {
	_, _, err := l.ImageInspectWithRaw(ctx, ref)
	return err == nil
}

func (l *localDaemon) ImageInspectWithRaw(ctx context.Context, img string) (image.InspectResponse, []byte, error) {
	// If we look at the container.Config struct definition, there is no Image field. We need the unaltered JSON to extract the Image field.
	var raw bytes.Buffer
	res, err := l.apiClient.ImageInspect(ctx, img, client.ImageInspectWithRawResponse(&raw))
	if err != nil {
		return image.InspectResponse{}, nil, err
	}
	return res.InspectResponse, raw.Bytes(), nil
}

func (l *localDaemon) ImageRemove(ctx context.Context, image string, opts client.ImageRemoveOptions) ([]image.DeleteResponse, error) {
	for i := 0; i < retries; i++ {
		res, err := l.apiClient.ImageRemove(ctx, image, opts)
		if err == nil {
			return res.Items, nil
		}
		if !errdefs.IsConflict(err) {
			return nil, err
		}
		time.Sleep(sleepTime)
	}
	return nil, fmt.Errorf("could not remove image %q after %d retries", image, retries)
}

func (l *localDaemon) ImageList(ctx context.Context, ref string) ([]image.Summary, error) {
	res, err := l.apiClient.ImageList(ctx, client.ImageListOptions{
		Filters: client.Filters{}.Add("reference", ref),
	})
	if err != nil {
		return nil, err
	}
	return res.Items, nil
}

func (l *localDaemon) DiskUsage(ctx context.Context) (uint64, error) {
	usage, err := l.apiClient.DiskUsage(ctx, client.DiskUsageOptions{})
	if err != nil {
		return 0, err
	}
	return uint64(usage.Images.TotalSize), nil
}

func ToCLIBuildArgs(a *latest.DockerArtifact, evaluatedArgs map[string]*string, env map[string]string) ([]string, error) {
	var args []string
	var keys []string
	for k := range evaluatedArgs {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		args = append(args, "--build-arg")

		v := evaluatedArgs[k]
		if v == nil {
			args = append(args, k)
		} else {
			args = append(args, fmt.Sprintf("%s=%s", k, *v))
		}
	}

	for _, host := range a.AddHost {
		args = append(args, "--add-host", host)
	}

	for _, from := range a.CacheFrom {
		args = append(args, "--cache-from", from)
	}

	for _, cliFlag := range a.CliFlags {
		cliFlag, err := util.ExpandEnvTemplate(cliFlag, env)
		if err != nil {
			return nil, fmt.Errorf("unable to evaluate cli flags: %w", err)
		}
		args = append(args, cliFlag)
	}

	if a.Target != "" {
		args = append(args, "--target", a.Target)
	}

	if a.NetworkMode != "" {
		args = append(args, "--network", strings.ToLower(a.NetworkMode))
	}

	if a.NoCache {
		args = append(args, "--no-cache")
	}

	if a.Squash {
		args = append(args, "--squash")
	}

	if a.PullParent {
		args = append(args, "--pull")
	}

	for _, secret := range a.Secrets {
		secretString := fmt.Sprintf("id=%s", secret.ID)
		if secret.Source != "" {
			secretString += ",src=" + util.ExpandHomePath(secret.Source)
		}
		if secret.Env != "" {
			secretString += ",env=" + secret.Env
		}
		args = append(args, "--secret", secretString)
	}

	if a.SSH != "" {
		args = append(args, "--ssh", a.SSH)
	}

	return args, nil
}

func (l *localDaemon) Prune(ctx context.Context, images []string, pruneChildren bool) ([]string, error) {
	var pruned []string
	var errRt error
	for _, id := range images {
		resp, err := l.ImageRemove(ctx, id, client.ImageRemoveOptions{
			Force:         true,
			PruneChildren: pruneChildren,
		})
		if err == nil {
			pruned = append(pruned, id)
		} else if errRt == nil {
			// save the first error
			errRt = fmt.Errorf("pruning images: %w", err)
		}

		for _, r := range resp {
			if r.Deleted != "" {
				log.Entry(ctx).Debugf("deleted image %s\n", r.Deleted)
			}
			if r.Untagged != "" {
				log.Entry(ctx).Debugf("untagged image %s\n", r.Untagged)
			}
		}
	}
	return pruned, errRt
}

func (l *localDaemon) Stop(ctx context.Context, id string, stopTimeout *time.Duration) error {
	var so client.ContainerStopOptions
	if stopTimeout != nil {
		so.Timeout = util.Ptr[int](int(stopTimeout.Seconds()))
	}
	if _, err := l.apiClient.ContainerStop(ctx, id, so); err != nil {
		log.Entry(ctx).Debugf("unable to stop running container: %s", err.Error())
		return err
	}

	return nil
}

func (l *localDaemon) Remove(ctx context.Context, id string) error {
	if _, err := l.apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{}); err != nil {
		log.Entry(ctx).Debugf("unable to remove container: %s", err.Error())
		return fmt.Errorf("unable to remove container: %w", err)
	}

	return nil
}
