drone/app/gitspace/orchestrator/container/devcontainer_container_util...

426 lines
12 KiB
Go

// Copyright 2023 Harness, Inc.
//
// 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 container
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
goruntime "runtime"
"strconv"
"strings"
"github.com/harness/gitness/app/gitspace/orchestrator/runarg"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/rs/zerolog/log"
)
const (
catchAllIP = "0.0.0.0"
)
var containerStateMapping = map[string]State{
"running": ContainerStateRunning,
"exited": ContainerStateStopped,
"dead": ContainerStateDead,
"created": ContainerStateCreated,
"paused": ContainerStatePaused,
}
// Helper function to log messages and handle error wrapping.
func logStreamWrapError(gitspaceLogger gitspaceTypes.GitspaceLogger, msg string, err error) error {
gitspaceLogger.Error(msg, err)
return fmt.Errorf("%s: %w", msg, err)
}
// Generalized Docker Container Management.
func ManageContainer(
ctx context.Context,
action Action,
containerName string,
dockerClient *client.Client,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
var err error
switch action {
case ContainerActionStop:
err = dockerClient.ContainerStop(ctx, containerName, container.StopOptions{})
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while stopping container", err)
}
gitspaceLogger.Info("Successfully stopped container")
case ContainerActionStart:
err = dockerClient.ContainerStart(ctx, containerName, container.StartOptions{})
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while starting container", err)
}
gitspaceLogger.Info("Successfully started container")
case ContainerActionRemove:
err = dockerClient.ContainerRemove(ctx, containerName, container.RemoveOptions{Force: true})
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while removing container", err)
}
gitspaceLogger.Info("Successfully removed container")
default:
return fmt.Errorf("unknown action: %s", action)
}
return nil
}
func FetchContainerState(
ctx context.Context,
containerName string,
dockerClient *client.Client,
) (State, error) {
args := filters.NewArgs()
args.Add("name", containerName)
containers, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true, Filters: args})
if err != nil {
return "", fmt.Errorf("could not list container %s: %w", containerName, err)
}
if len(containers) == 0 {
return ContainerStateRemoved, nil
}
containerState := ContainerStateUnknown
for _, value := range containers {
name, _ := strings.CutPrefix(value.Names[0], "/")
if name == containerName {
if state, ok := containerStateMapping[value.State]; ok {
containerState = state
}
break
}
}
return containerState, nil
}
// Create a new Docker container.
func CreateContainer(
ctx context.Context,
dockerClient *client.Client,
imageName string,
containerName string,
gitspaceLogger gitspaceTypes.GitspaceLogger,
bindMountSource string,
bindMountTarget string,
mountType mount.Type,
portMappings map[int]*types.PortMapping,
env []string,
runArgsMap map[types.RunArg]*types.RunArgValue,
) error {
exposedPorts, portBindings := applyPortMappings(portMappings)
gitspaceLogger.Info("Creating container: " + containerName)
hostConfig, err := prepareHostConfig(bindMountSource, bindMountTarget, mountType, portBindings, runArgsMap)
if err != nil {
return err
}
healthCheckConfig, err := getHealthCheckConfig(runArgsMap)
if err != nil {
return err
}
stopTimeout, err := getStopTimeout(runArgsMap)
if err != nil {
return err
}
entrypoint := getEntrypoint(runArgsMap)
var cmd strslice.StrSlice
if len(entrypoint) == 0 {
entrypoint = []string{"/bin/sh"}
cmd = []string{"-c", "trap 'exit 0' 15; sleep infinity & wait $!"}
}
// Create the container
containerConfig := &container.Config{
Hostname: getHostname(runArgsMap),
Domainname: getDomainname(runArgsMap),
Image: imageName,
Env: env,
Entrypoint: entrypoint,
Cmd: cmd,
ExposedPorts: exposedPorts,
Labels: getLabels(runArgsMap),
Healthcheck: healthCheckConfig,
MacAddress: getMACAddress(runArgsMap),
StopSignal: getStopSignal(runArgsMap),
StopTimeout: stopTimeout,
User: getUser(runArgsMap),
}
_, err = dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while creating container", err)
}
return nil
}
// Prepare port mappings for container creation.
func applyPortMappings(portMappings map[int]*types.PortMapping) (nat.PortSet, nat.PortMap) {
exposedPorts := nat.PortSet{}
portBindings := nat.PortMap{}
for port, mapping := range portMappings {
natPort := nat.Port(strconv.Itoa(port) + "/tcp")
hostPortBindings := []nat.PortBinding{
{
HostIP: catchAllIP,
HostPort: strconv.Itoa(mapping.PublishedPort),
},
}
exposedPorts[natPort] = struct{}{}
portBindings[natPort] = hostPortBindings
}
return exposedPorts, portBindings
}
// Prepare the host configuration for container creation.
func prepareHostConfig(
bindMountSource string,
bindMountTarget string,
mountType mount.Type,
portBindings nat.PortMap,
runArgsMap map[types.RunArg]*types.RunArgValue,
) (*container.HostConfig, error) {
hostResources, err := getHostResources(runArgsMap)
if err != nil {
return nil, err
}
extraHosts := getExtraHosts(runArgsMap)
if goruntime.GOOS == "linux" {
extraHosts = append(extraHosts, "host.docker.internal:host-gateway")
}
restartPolicy, err := getRestartPolicy(runArgsMap)
if err != nil {
return nil, err
}
oomScoreAdj, err := getOomScoreAdj(runArgsMap)
if err != nil {
return nil, err
}
shmSize, err := getSHMSize(runArgsMap)
if err != nil {
return nil, err
}
hostConfig := &container.HostConfig{
PortBindings: portBindings,
Mounts: []mount.Mount{
{
Type: mountType,
Source: bindMountSource,
Target: bindMountTarget,
},
},
Resources: hostResources,
Annotations: getAnnotations(runArgsMap),
ExtraHosts: extraHosts,
NetworkMode: getNetworkMode(runArgsMap),
RestartPolicy: restartPolicy,
AutoRemove: getAutoRemove(runArgsMap),
CapDrop: getCapDrop(runArgsMap),
CgroupnsMode: getCgroupNSMode(runArgsMap),
DNS: getDNS(runArgsMap),
DNSOptions: getDNSOptions(runArgsMap),
DNSSearch: getDNSSearch(runArgsMap),
IpcMode: getIPCMode(runArgsMap),
Isolation: getIsolation(runArgsMap),
Init: getInit(runArgsMap),
Links: getLinks(runArgsMap),
OomScoreAdj: oomScoreAdj,
PidMode: getPIDMode(runArgsMap),
Runtime: getRuntime(runArgsMap),
SecurityOpt: getSecurityOpt(runArgsMap),
StorageOpt: getStorageOpt(runArgsMap),
ShmSize: shmSize,
Sysctls: getSysctls(runArgsMap),
}
return hostConfig, nil
}
func GetContainerInfo(
ctx context.Context,
containerName string,
dockerClient *client.Client,
portMappings map[int]*types.PortMapping,
) (string, map[int]string, error) {
inspectResp, err := dockerClient.ContainerInspect(ctx, containerName)
if err != nil {
return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err)
}
usedPorts := make(map[int]string)
for portAndProtocol, bindings := range inspectResp.NetworkSettings.Ports {
portRaw := strings.Split(string(portAndProtocol), "/")[0]
port, conversionErr := strconv.Atoi(portRaw)
if conversionErr != nil {
return "", nil, fmt.Errorf("could not convert port %s to int: %w", portRaw, conversionErr)
}
if portMappings[port] != nil {
usedPorts[port] = bindings[0].HostPort
}
}
return inspectResp.ID, usedPorts, nil
}
func PullImage(
ctx context.Context,
imageName string,
dockerClient *client.Client,
runArgsMap map[types.RunArg]*types.RunArgValue,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
imagePullRunArg := getImagePullPolicy(runArgsMap)
gitspaceLogger.Info("Image pull policy is: " + imagePullRunArg)
if imagePullRunArg == "never" {
return nil
}
if imagePullRunArg == "missing" {
gitspaceLogger.Info("Checking if image " + imageName + " is present locally")
filterArgs := filters.NewArgs()
filterArgs.Add("reference", imageName)
images, err := dockerClient.ImageList(ctx, image.ListOptions{Filters: filterArgs})
if err != nil {
gitspaceLogger.Error("Error listing images locally", err)
return err
}
if len(images) > 0 {
gitspaceLogger.Info("Image " + imageName + " is present locally")
return nil
}
gitspaceLogger.Info("Image " + imageName + " is not present locally")
}
gitspaceLogger.Info("Pulling image: " + imageName)
pullResponse, err := dockerClient.ImagePull(ctx, imageName, image.PullOptions{Platform: getPlatform(runArgsMap)})
defer func() {
if pullResponse == nil {
return
}
closingErr := pullResponse.Close()
if closingErr != nil {
log.Warn().Err(closingErr).Msg("failed to close image pull response")
}
}()
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while pulling image", err)
}
defer func() {
if pullResponse != nil {
if closingErr := pullResponse.Close(); closingErr != nil {
log.Warn().Err(closingErr).Msg("Failed to close image pull response")
}
}
}()
// Process JSON stream
decoder := json.NewDecoder(pullResponse)
layerStatus := make(map[string]string) // Track last status of each layer
for {
var pullEvent map[string]interface{}
if err := decoder.Decode(&pullEvent); err != nil {
if errors.Is(err, io.EOF) {
break
}
return logStreamWrapError(gitspaceLogger, "Error while decoding image pull response", err)
}
// Extract relevant fields from the JSON object
layerID, _ := pullEvent["id"].(string) // Layer ID (if available)
status, _ := pullEvent["status"].(string) // Current status
// Update logs only when the status changes
if layerID != "" {
if lastStatus, exists := layerStatus[layerID]; !exists || lastStatus != status {
layerStatus[layerID] = status
gitspaceLogger.Info(fmt.Sprintf("Layer %s: %s", layerID, status))
}
} else if status != "" {
// Log non-layer-specific status
gitspaceLogger.Info(status)
}
}
gitspaceLogger.Info("Image pull completed successfully")
return nil
}
func ExtractRunArgsWithLogging(
ctx context.Context,
spaceID int64,
runArgProvider runarg.Provider,
runArgsRaw []string,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) (map[types.RunArg]*types.RunArgValue, error) {
gitspaceLogger.Info("Extracting runsArgs")
runArgsMap, err := ExtractRunArgs(ctx, spaceID, runArgProvider, runArgsRaw)
if err != nil {
return nil, logStreamWrapError(gitspaceLogger, "Error while extracting runArgs", err)
}
if len(runArgsMap) > 0 {
st := ""
for key, value := range runArgsMap {
st = fmt.Sprintf("%s%s: %s\n", st, key, value)
}
gitspaceLogger.Info(fmt.Sprintf("Extracted following runArgs\n%v", st))
}
return runArgsMap, nil
}
// getContainerResponse retrieves container information and prepares the start response.
func GetContainerResponse(
ctx context.Context,
dockerClient *client.Client,
containerName string,
portMappings map[int]*types.PortMapping,
codeRepoDir string,
) (*StartResponse, error) {
id, ports, err := GetContainerInfo(ctx, containerName, dockerClient, portMappings)
if err != nil {
return nil, err
}
return &StartResponse{
ContainerID: id,
ContainerName: containerName,
PublishedPorts: ports,
AbsoluteRepoPath: codeRepoDir,
}, nil
}