mirror of https://github.com/harness/drone.git
feat: [CDE-572]: Using features for devcontainers. (#3260)
* feat: [CDE-572]: Using features for devcontainers. Adding changes to parse features from the devcontainer.json and build a new docker image from them. Also adding the support for new devcontainer.json properties- init, privileged, capAdd, securityOpt, mounts. Adding support for three runArgs- privileged, capAdd, mount. Also making the DownloadFeature method context aware, cancelling the goroutines when the ctx is cancelled.pull/3616/head
parent
f8957318f6
commit
b94a78c795
|
@ -40,6 +40,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/strslice"
|
"github.com/docker/docker/api/types/strslice"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/gotidy/ptr"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,35 +143,49 @@ func CreateContainer(
|
||||||
runArgsMap map[types.RunArg]*types.RunArgValue,
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
containerUser string,
|
containerUser string,
|
||||||
remoteUser string,
|
remoteUser string,
|
||||||
) error {
|
features []*types.ResolvedFeature,
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) (map[PostAction][]*LifecycleHookStep, error) {
|
||||||
exposedPorts, portBindings := applyPortMappings(portMappings)
|
exposedPorts, portBindings := applyPortMappings(portMappings)
|
||||||
|
|
||||||
gitspaceLogger.Info("Creating container: " + containerName)
|
gitspaceLogger.Info(fmt.Sprintf("Creating container %s with image %s", containerName, imageName))
|
||||||
|
|
||||||
hostConfig, err := prepareHostConfig(bindMountSource, bindMountTarget, mountType, portBindings, runArgsMap)
|
hostConfig, err := prepareHostConfig(bindMountSource, bindMountTarget, mountType, portBindings, runArgsMap,
|
||||||
|
features, devcontainerConfig, metadataFromImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
healthCheckConfig, err := getHealthCheckConfig(runArgsMap)
|
healthCheckConfig, err := getHealthCheckConfig(runArgsMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
stopTimeout, err := getStopTimeout(runArgsMap)
|
stopTimeout, err := getStopTimeout(runArgsMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
entrypoint := getEntrypoint(runArgsMap)
|
entrypoint := mergeEntrypoints(features, runArgsMap)
|
||||||
var cmd strslice.StrSlice
|
var cmd strslice.StrSlice
|
||||||
if len(entrypoint) == 0 {
|
if len(entrypoint) == 0 {
|
||||||
entrypoint = []string{"/bin/sh"}
|
entrypoint = []string{"/bin/sh"}
|
||||||
cmd = []string{"-c", "trap 'exit 0' 15; sleep infinity & wait $!"}
|
cmd = []string{"-c", "trap 'exit 0' 15; sleep infinity & wait $!"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleHookSteps := mergeLifeCycleHooks(devcontainerConfig, features)
|
||||||
|
lifecycleHookStepsStr, err := json.Marshal(lifecycleHookSteps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
labels := getLabels(runArgsMap)
|
labels := getLabels(runArgsMap)
|
||||||
|
|
||||||
// Setting the following so that it can be read later to form gitspace URL.
|
// Setting the following so that it can be read later to form gitspace URL.
|
||||||
labels[gitspaceRemoteUserLabel] = remoteUser
|
labels[gitspaceRemoteUserLabel] = remoteUser
|
||||||
|
|
||||||
|
// Setting the following so that it can be read later to run the postStartCommands during restarts.
|
||||||
|
labels[gitspaceLifeCycleHooksLabel] = string(lifecycleHookStepsStr)
|
||||||
|
|
||||||
// Create the container
|
// Create the container
|
||||||
containerConfig := &container.Config{
|
containerConfig := &container.Config{
|
||||||
Hostname: getHostname(runArgsMap),
|
Hostname: getHostname(runArgsMap),
|
||||||
|
@ -190,10 +205,74 @@ func CreateContainer(
|
||||||
|
|
||||||
_, err = dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
|
_, err = dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return logStreamWrapError(gitspaceLogger, "Error while creating container", err)
|
return nil, logStreamWrapError(gitspaceLogger, "Error while creating container", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return lifecycleHookSteps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeLifeCycleHooks(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
) map[PostAction][]*LifecycleHookStep {
|
||||||
|
var postCreateHooks []*LifecycleHookStep
|
||||||
|
var postStartHooks []*LifecycleHookStep
|
||||||
|
for _, feature := range features {
|
||||||
|
if len(feature.DownloadedFeature.DevcontainerFeatureConfig.PostCreateCommand.ToCommandArray()) > 0 {
|
||||||
|
postCreateHooks = append(postCreateHooks, &LifecycleHookStep{
|
||||||
|
Source: feature.DownloadedFeature.Source,
|
||||||
|
Command: feature.DownloadedFeature.DevcontainerFeatureConfig.PostCreateCommand,
|
||||||
|
ActionType: PostCreateAction,
|
||||||
|
StopOnFailure: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(feature.DownloadedFeature.DevcontainerFeatureConfig.PostStartCommand.ToCommandArray()) > 0 {
|
||||||
|
postStartHooks = append(postStartHooks, &LifecycleHookStep{
|
||||||
|
Source: feature.DownloadedFeature.Source,
|
||||||
|
Command: feature.DownloadedFeature.DevcontainerFeatureConfig.PostStartCommand,
|
||||||
|
ActionType: PostStartAction,
|
||||||
|
StopOnFailure: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devcontainerConfig.PostCreateCommand.ToCommandArray()) > 0 {
|
||||||
|
postCreateHooks = append(postCreateHooks, &LifecycleHookStep{
|
||||||
|
Source: "devcontainer.json",
|
||||||
|
Command: devcontainerConfig.PostCreateCommand,
|
||||||
|
ActionType: PostCreateAction,
|
||||||
|
StopOnFailure: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devcontainerConfig.PostStartCommand.ToCommandArray()) > 0 {
|
||||||
|
postStartHooks = append(postStartHooks, &LifecycleHookStep{
|
||||||
|
Source: "devcontainer.json",
|
||||||
|
Command: devcontainerConfig.PostStartCommand,
|
||||||
|
ActionType: PostStartAction,
|
||||||
|
StopOnFailure: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[PostAction][]*LifecycleHookStep{
|
||||||
|
PostCreateAction: postCreateHooks,
|
||||||
|
PostStartAction: postStartHooks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeEntrypoints(
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
) strslice.StrSlice {
|
||||||
|
entrypoints := strslice.StrSlice{}
|
||||||
|
for _, feature := range features {
|
||||||
|
entrypoint := feature.DownloadedFeature.DevcontainerFeatureConfig.Entrypoint
|
||||||
|
if entrypoint != "" {
|
||||||
|
entrypoints = append(entrypoints, entrypoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entrypoints = append(entrypoints, getEntrypoint(runArgsMap)...)
|
||||||
|
return entrypoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare port mappings for container creation.
|
// Prepare port mappings for container creation.
|
||||||
|
@ -221,6 +300,9 @@ func prepareHostConfig(
|
||||||
mountType mount.Type,
|
mountType mount.Type,
|
||||||
portBindings nat.PortMap,
|
portBindings nat.PortMap,
|
||||||
runArgsMap map[types.RunArg]*types.RunArgValue,
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
) (*container.HostConfig, error) {
|
) (*container.HostConfig, error) {
|
||||||
hostResources, err := getHostResources(runArgsMap)
|
hostResources, err := getHostResources(runArgsMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -247,21 +329,27 @@ func prepareHostConfig(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultMount := mount.Mount{
|
||||||
|
Type: mountType,
|
||||||
|
Source: bindMountSource,
|
||||||
|
Target: bindMountTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedMounts, err := mergeMounts(devcontainerConfig, runArgsMap, features, defaultMount, metadataFromImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to merge mounts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
Mounts: []mount.Mount{
|
Mounts: mergedMounts,
|
||||||
{
|
|
||||||
Type: mountType,
|
|
||||||
Source: bindMountSource,
|
|
||||||
Target: bindMountTarget,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Resources: hostResources,
|
Resources: hostResources,
|
||||||
Annotations: getAnnotations(runArgsMap),
|
Annotations: getAnnotations(runArgsMap),
|
||||||
ExtraHosts: extraHosts,
|
ExtraHosts: extraHosts,
|
||||||
NetworkMode: getNetworkMode(runArgsMap),
|
NetworkMode: getNetworkMode(runArgsMap),
|
||||||
RestartPolicy: restartPolicy,
|
RestartPolicy: restartPolicy,
|
||||||
AutoRemove: getAutoRemove(runArgsMap),
|
AutoRemove: getAutoRemove(runArgsMap),
|
||||||
|
CapAdd: mergeCapAdd(devcontainerConfig, runArgsMap, features, metadataFromImage),
|
||||||
CapDrop: getCapDrop(runArgsMap),
|
CapDrop: getCapDrop(runArgsMap),
|
||||||
CgroupnsMode: getCgroupNSMode(runArgsMap),
|
CgroupnsMode: getCgroupNSMode(runArgsMap),
|
||||||
DNS: getDNS(runArgsMap),
|
DNS: getDNS(runArgsMap),
|
||||||
|
@ -269,12 +357,13 @@ func prepareHostConfig(
|
||||||
DNSSearch: getDNSSearch(runArgsMap),
|
DNSSearch: getDNSSearch(runArgsMap),
|
||||||
IpcMode: getIPCMode(runArgsMap),
|
IpcMode: getIPCMode(runArgsMap),
|
||||||
Isolation: getIsolation(runArgsMap),
|
Isolation: getIsolation(runArgsMap),
|
||||||
Init: getInit(runArgsMap),
|
Init: mergeInit(devcontainerConfig, runArgsMap, features, metadataFromImage),
|
||||||
Links: getLinks(runArgsMap),
|
Links: getLinks(runArgsMap),
|
||||||
OomScoreAdj: oomScoreAdj,
|
OomScoreAdj: oomScoreAdj,
|
||||||
PidMode: getPIDMode(runArgsMap),
|
PidMode: getPIDMode(runArgsMap),
|
||||||
|
Privileged: mergePriviledged(devcontainerConfig, runArgsMap, features, metadataFromImage),
|
||||||
Runtime: getRuntime(runArgsMap),
|
Runtime: getRuntime(runArgsMap),
|
||||||
SecurityOpt: getSecurityOpt(runArgsMap),
|
SecurityOpt: mergeSecurityOpts(devcontainerConfig, runArgsMap, features, metadataFromImage),
|
||||||
StorageOpt: getStorageOpt(runArgsMap),
|
StorageOpt: getStorageOpt(runArgsMap),
|
||||||
ShmSize: shmSize,
|
ShmSize: shmSize,
|
||||||
Sysctls: getSysctls(runArgsMap),
|
Sysctls: getSysctls(runArgsMap),
|
||||||
|
@ -283,6 +372,186 @@ func prepareHostConfig(
|
||||||
return hostConfig, nil
|
return hostConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mergeMounts(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
defaultMount mount.Mount,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) ([]mount.Mount, error) {
|
||||||
|
var allMountsRaw []*types.Mount
|
||||||
|
for _, feature := range features {
|
||||||
|
if len(feature.DownloadedFeature.DevcontainerFeatureConfig.Mounts) > 0 {
|
||||||
|
allMountsRaw = append(allMountsRaw, feature.DownloadedFeature.DevcontainerFeatureConfig.Mounts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mountsFromRunArgs, err := getMounts(runArgsMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if mounts have been overridden in the runArgs, then check if the devcontainer.json
|
||||||
|
// provides any security options, if not, only then check the image metadata.
|
||||||
|
switch {
|
||||||
|
case len(mountsFromRunArgs) > 0:
|
||||||
|
allMountsRaw = append(allMountsRaw, mountsFromRunArgs...)
|
||||||
|
case len(devcontainerConfig.Mounts) > 0:
|
||||||
|
allMountsRaw = append(allMountsRaw, devcontainerConfig.Mounts...)
|
||||||
|
default:
|
||||||
|
if values, ok := metadataFromImage["mounts"].([]any); ok {
|
||||||
|
parsedMounts, err := types.ParseMountsFromRawSlice(values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allMountsRaw = append(allMountsRaw, parsedMounts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allMounts []mount.Mount
|
||||||
|
for _, rawMount := range allMountsRaw {
|
||||||
|
if rawMount.Type == "" {
|
||||||
|
rawMount.Type = string(mount.TypeVolume)
|
||||||
|
}
|
||||||
|
parsedMount := mount.Mount{
|
||||||
|
Type: mount.Type(rawMount.Type),
|
||||||
|
Source: rawMount.Source,
|
||||||
|
Target: rawMount.Target,
|
||||||
|
}
|
||||||
|
allMounts = append(allMounts, parsedMount)
|
||||||
|
}
|
||||||
|
|
||||||
|
allMounts = append(allMounts, defaultMount)
|
||||||
|
|
||||||
|
return allMounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeSecurityOpts(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) []string {
|
||||||
|
var allOpts []string
|
||||||
|
for _, feature := range features {
|
||||||
|
allOpts = append(allOpts, feature.DownloadedFeature.DevcontainerFeatureConfig.SecurityOpt...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if security options have been overridden in the runArgs, then check if the devcontainer.json
|
||||||
|
// provides any security options, if not, only then check the image metadata.
|
||||||
|
securityOptsFromRunArgs := getSecurityOpt(runArgsMap)
|
||||||
|
switch {
|
||||||
|
case len(securityOptsFromRunArgs) > 0:
|
||||||
|
allOpts = append(allOpts, securityOptsFromRunArgs...)
|
||||||
|
case len(devcontainerConfig.SecurityOpt) > 0:
|
||||||
|
allOpts = append(allOpts, devcontainerConfig.SecurityOpt...)
|
||||||
|
default:
|
||||||
|
if value, ok := metadataFromImage["securityOpt"].([]string); ok {
|
||||||
|
allOpts = append(allOpts, value...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeCapAdd(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) strslice.StrSlice {
|
||||||
|
allCaps := strslice.StrSlice{}
|
||||||
|
for _, feature := range features {
|
||||||
|
allCaps = append(allCaps, feature.DownloadedFeature.DevcontainerFeatureConfig.CapAdd...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if capAdd have been overridden in the runArgs, then check if the devcontainer.json
|
||||||
|
// provides any capAdd, if not, only then check the image metadata.
|
||||||
|
capAddFromRunArgs := getCapAdd(runArgsMap)
|
||||||
|
switch {
|
||||||
|
case len(capAddFromRunArgs) > 0:
|
||||||
|
allCaps = append(allCaps, capAddFromRunArgs...)
|
||||||
|
case len(devcontainerConfig.CapAdd) > 0:
|
||||||
|
allCaps = append(allCaps, devcontainerConfig.CapAdd...)
|
||||||
|
default:
|
||||||
|
if value, ok := metadataFromImage["capAdd"].([]string); ok {
|
||||||
|
allCaps = append(allCaps, value...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allCaps
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeInit(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) *bool {
|
||||||
|
// First check if init has been overridden in the runArgs, if not, then check in the devcontainer.json
|
||||||
|
// lastly check in the image metadata.
|
||||||
|
var initPtr = getInit(runArgsMap)
|
||||||
|
|
||||||
|
if initPtr == nil {
|
||||||
|
if devcontainerConfig.Init != nil {
|
||||||
|
initPtr = devcontainerConfig.Init
|
||||||
|
} else {
|
||||||
|
if value, ok := metadataFromImage["init"].(bool); ok {
|
||||||
|
initPtr = ptr.Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var init = ptr.ToBool(initPtr)
|
||||||
|
|
||||||
|
// Merge this valye with the value from the features.
|
||||||
|
if !init {
|
||||||
|
for _, feature := range features {
|
||||||
|
if feature.DownloadedFeature.DevcontainerFeatureConfig.Init {
|
||||||
|
init = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptr.Bool(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePriviledged(
|
||||||
|
devcontainerConfig types.DevcontainerConfig,
|
||||||
|
runArgsMap map[types.RunArg]*types.RunArgValue,
|
||||||
|
features []*types.ResolvedFeature,
|
||||||
|
metadataFromImage map[string]any,
|
||||||
|
) bool {
|
||||||
|
// First check if privileged has been overridden in the runArgs, if not, then check in the devcontainer.json
|
||||||
|
// lastly check in the image metadata.
|
||||||
|
var privilegedPtr = getPrivileged(runArgsMap)
|
||||||
|
|
||||||
|
if privilegedPtr == nil {
|
||||||
|
if devcontainerConfig.Privileged != nil {
|
||||||
|
privilegedPtr = devcontainerConfig.Privileged
|
||||||
|
} else {
|
||||||
|
if value, ok := metadataFromImage["privileged"].(bool); ok {
|
||||||
|
privilegedPtr = ptr.Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var privileged = ptr.ToBool(privilegedPtr)
|
||||||
|
|
||||||
|
// Merge this valye with the value from the features.
|
||||||
|
if !privileged {
|
||||||
|
for _, feature := range features {
|
||||||
|
if feature.DownloadedFeature.DevcontainerFeatureConfig.Privileged {
|
||||||
|
privileged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return privileged
|
||||||
|
}
|
||||||
|
|
||||||
func GetContainerInfo(
|
func GetContainerInfo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
containerName string,
|
containerName string,
|
||||||
|
@ -542,17 +811,22 @@ func GetContainerResponse(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRemoteUserFromContainerLabel(
|
func GetGitspaceInfoFromContainerLabels(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
containerName string,
|
containerName string,
|
||||||
dockerClient *client.Client,
|
dockerClient *client.Client,
|
||||||
) (string, error) {
|
) (string, map[PostAction][]*LifecycleHookStep, error) {
|
||||||
inspectResp, err := dockerClient.ContainerInspect(ctx, containerName)
|
inspectResp, err := dockerClient.ContainerInspect(ctx, containerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("could not inspect container %s: %w", containerName, err)
|
return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExtractRemoteUserFromLabels(inspectResp), nil
|
remoteUser := ExtractRemoteUserFromLabels(inspectResp)
|
||||||
|
lifecycleHooks, err := ExtractLifecycleHooksFromLabels(inspectResp)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("could not extract lifecycle hooks: %w", err)
|
||||||
|
}
|
||||||
|
return remoteUser, lifecycleHooks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to encode the AuthConfig into a Base64 string.
|
// Helper function to encode the AuthConfig into a Base64 string.
|
||||||
|
|
|
@ -53,6 +53,13 @@ type step struct {
|
||||||
StopOnFailure bool // Flag to control whether execution should stop on failure
|
StopOnFailure bool // Flag to control whether execution should stop on failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LifecycleHookStep struct {
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Command types.LifecycleCommand `json:"command,omitempty"`
|
||||||
|
ActionType PostAction `json:"action_type,omitempty"`
|
||||||
|
StopOnFailure bool `json:"stop_on_failure,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteSteps executes all registered steps in sequence, respecting stopOnFailure flag.
|
// ExecuteSteps executes all registered steps in sequence, respecting stopOnFailure flag.
|
||||||
func (e *EmbeddedDockerOrchestrator) ExecuteSteps(
|
func (e *EmbeddedDockerOrchestrator) ExecuteSteps(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
@ -181,7 +188,7 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
|
||||||
}
|
}
|
||||||
defer e.flushLogStream(logStreamInstance, gitspaceConfig.ID)
|
defer e.flushLogStream(logStreamInstance, gitspaceConfig.ID)
|
||||||
|
|
||||||
remoteUser, err := GetRemoteUserFromContainerLabel(ctx, containerName, dockerClient)
|
remoteUser, lifecycleHooks, err := GetGitspaceInfoFromContainerLabels(ctx, containerName, dockerClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting remote user for gitspace instance %s: %w",
|
return fmt.Errorf("error getting remote user for gitspace instance %s: %w",
|
||||||
gitspaceConfig.GitspaceInstance.Identifier, err)
|
gitspaceConfig.GitspaceInstance.Identifier, err)
|
||||||
|
@ -217,13 +224,24 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute post-start command
|
if len(lifecycleHooks) > 0 && len(lifecycleHooks[PostStartAction]) > 0 {
|
||||||
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
|
for _, lifecycleHook := range lifecycleHooks[PostStartAction] {
|
||||||
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
|
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance,
|
||||||
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance, command, PostStartAction)
|
lifecycleHook.Command.ToCommandArray(), PostStartAction)
|
||||||
if startErr != nil {
|
if startErr != nil {
|
||||||
log.Warn().Msgf("Error is post-start command, continuing : %s", startErr.Error())
|
log.Warn().Msgf("Error in post-start command, continuing : %s", startErr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Execute post-start command for the containers before this label was introduced
|
||||||
|
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
|
||||||
|
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
|
||||||
|
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance, command, PostStartAction)
|
||||||
|
if startErr != nil {
|
||||||
|
log.Warn().Msgf("Error in post-start command, continuing : %s", startErr.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,26 +440,43 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
|
||||||
containerUser := GetContainerUser(runArgsMap, devcontainerConfig, metadataFromImage, imageUser)
|
containerUser := GetContainerUser(runArgsMap, devcontainerConfig, metadataFromImage, imageUser)
|
||||||
remoteUser := GetRemoteUser(devcontainerConfig, metadataFromImage, containerUser)
|
remoteUser := GetRemoteUser(devcontainerConfig, metadataFromImage, containerUser)
|
||||||
|
|
||||||
homeDir := GetUserHomeDir(remoteUser)
|
containerUserHomeDir := GetUserHomeDir(containerUser)
|
||||||
|
remoteUserHomeDir := GetUserHomeDir(remoteUser)
|
||||||
|
|
||||||
gitspaceLogger.Info(fmt.Sprintf("Container user: %s", containerUser))
|
gitspaceLogger.Info(fmt.Sprintf("Container user: %s", containerUser))
|
||||||
gitspaceLogger.Info(fmt.Sprintf("Remote user: %s", remoteUser))
|
gitspaceLogger.Info(fmt.Sprintf("Remote user: %s", remoteUser))
|
||||||
|
var features []*types.ResolvedFeature
|
||||||
|
if devcontainerConfig.Features != nil && len(*devcontainerConfig.Features) > 0 {
|
||||||
|
sortedFeatures, newImageName, err := InstallFeatures(ctx, gitspaceConfig.GitspaceInstance.Identifier,
|
||||||
|
dockerClient, *devcontainerConfig.Features, devcontainerConfig.OverrideFeatureInstallOrder, imageName,
|
||||||
|
containerUser, remoteUser, containerUserHomeDir, remoteUserHomeDir, gitspaceLogger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
features = sortedFeatures
|
||||||
|
imageName = newImageName
|
||||||
|
} else {
|
||||||
|
gitspaceLogger.Info("No features found")
|
||||||
|
}
|
||||||
|
|
||||||
// Create the container
|
// Create the container
|
||||||
err = CreateContainer(
|
lifecycleHookSteps, err := CreateContainer(
|
||||||
ctx,
|
ctx,
|
||||||
dockerClient,
|
dockerClient,
|
||||||
imageName,
|
imageName,
|
||||||
containerName,
|
containerName,
|
||||||
gitspaceLogger,
|
gitspaceLogger,
|
||||||
storage,
|
storage,
|
||||||
homeDir,
|
remoteUserHomeDir,
|
||||||
mount.TypeVolume,
|
mount.TypeVolume,
|
||||||
portMappings,
|
portMappings,
|
||||||
environment,
|
environment,
|
||||||
runArgsMap,
|
runArgsMap,
|
||||||
containerUser,
|
containerUser,
|
||||||
remoteUser,
|
remoteUser,
|
||||||
|
features,
|
||||||
|
resolvedRepoDetails.DevcontainerConfig,
|
||||||
|
metadataFromImage,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -456,7 +491,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
|
||||||
exec := &devcontainer.Exec{
|
exec := &devcontainer.Exec{
|
||||||
ContainerName: containerName,
|
ContainerName: containerName,
|
||||||
DockerClient: dockerClient,
|
DockerClient: dockerClient,
|
||||||
DefaultWorkingDir: homeDir,
|
DefaultWorkingDir: remoteUserHomeDir,
|
||||||
RemoteUser: remoteUser,
|
RemoteUser: remoteUser,
|
||||||
AccessKey: *gitspaceConfig.GitspaceInstance.AccessKey,
|
AccessKey: *gitspaceConfig.GitspaceInstance.AccessKey,
|
||||||
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
|
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
|
||||||
|
@ -471,6 +506,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
|
||||||
resolvedRepoDetails,
|
resolvedRepoDetails,
|
||||||
defaultBaseImage,
|
defaultBaseImage,
|
||||||
environment,
|
environment,
|
||||||
|
lifecycleHookSteps,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return logStreamWrapError(gitspaceLogger, "Error while setting up gitspace", err)
|
return logStreamWrapError(gitspaceLogger, "Error while setting up gitspace", err)
|
||||||
}
|
}
|
||||||
|
@ -478,18 +514,65 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InstallFeatures(
|
||||||
|
ctx context.Context,
|
||||||
|
gitspaceInstanceIdentifier string,
|
||||||
|
dockerClient *client.Client,
|
||||||
|
features types.Features,
|
||||||
|
overrideFeatureInstallOrder []string,
|
||||||
|
imageName string,
|
||||||
|
containerUser string,
|
||||||
|
remoteUser string,
|
||||||
|
containerUserHomeDir string,
|
||||||
|
remoteUserHomeDir string,
|
||||||
|
gitspaceLogger gitspaceTypes.GitspaceLogger,
|
||||||
|
) ([]*types.ResolvedFeature, string, error) {
|
||||||
|
gitspaceLogger.Info("Downloading features...")
|
||||||
|
downloadedFeatures, err := utils.DownloadFeatures(ctx, gitspaceInstanceIdentifier, features)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", logStreamWrapError(gitspaceLogger, "Error downloading features", err)
|
||||||
|
}
|
||||||
|
gitspaceLogger.Info(fmt.Sprintf("Downloaded %d features", len(*downloadedFeatures)))
|
||||||
|
|
||||||
|
gitspaceLogger.Info("Resolving features...")
|
||||||
|
resolvedFeatures, err := utils.ResolveFeatures(features, *downloadedFeatures)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", logStreamWrapError(gitspaceLogger, "Error resolving features", err)
|
||||||
|
}
|
||||||
|
gitspaceLogger.Info(fmt.Sprintf("Resolved to %d features", len(resolvedFeatures)))
|
||||||
|
|
||||||
|
gitspaceLogger.Info("Determining feature installation order...")
|
||||||
|
sortedFeatures, err := utils.SortFeatures(resolvedFeatures, overrideFeatureInstallOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", logStreamWrapError(gitspaceLogger, "Error sorting features", err)
|
||||||
|
}
|
||||||
|
gitspaceLogger.Info("Feature installation order is:")
|
||||||
|
for index, feature := range sortedFeatures {
|
||||||
|
gitspaceLogger.Info(fmt.Sprintf("%d. %s", index, feature.Print()))
|
||||||
|
}
|
||||||
|
|
||||||
|
gitspaceLogger.Info("Installing features...")
|
||||||
|
imageName, err = utils.BuildWithFeatures(ctx, dockerClient, imageName, sortedFeatures, gitspaceInstanceIdentifier,
|
||||||
|
containerUser, remoteUser, containerUserHomeDir, remoteUserHomeDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", logStreamWrapError(gitspaceLogger, "Error building with features", err)
|
||||||
|
}
|
||||||
|
gitspaceLogger.Info(fmt.Sprintf("Installed features, built new docker image %s", imageName))
|
||||||
|
|
||||||
|
return sortedFeatures, imageName, nil
|
||||||
|
}
|
||||||
|
|
||||||
// buildSetupSteps constructs the steps to be executed in the setup process.
|
// buildSetupSteps constructs the steps to be executed in the setup process.
|
||||||
func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
|
func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
|
||||||
_ context.Context,
|
|
||||||
ideService ide.IDE,
|
ideService ide.IDE,
|
||||||
gitspaceConfig types.GitspaceConfig,
|
gitspaceConfig types.GitspaceConfig,
|
||||||
resolvedRepoDetails scm.ResolvedDetails,
|
resolvedRepoDetails scm.ResolvedDetails,
|
||||||
defaultBaseImage string,
|
defaultBaseImage string,
|
||||||
environment []string,
|
environment []string,
|
||||||
devcontainerConfig types.DevcontainerConfig,
|
|
||||||
codeRepoDir string,
|
codeRepoDir string,
|
||||||
|
lifecycleHookSteps map[PostAction][]*LifecycleHookStep,
|
||||||
) []step {
|
) []step {
|
||||||
return []step{
|
steps := []step{
|
||||||
{
|
{
|
||||||
Name: "Validate Supported OS",
|
Name: "Validate Supported OS",
|
||||||
Execute: utils.ValidateSupportedOS,
|
Execute: utils.ValidateSupportedOS,
|
||||||
|
@ -583,33 +666,41 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
|
||||||
return ideService.Run(ctx, exec, args, gitspaceLogger)
|
return ideService.Run(ctx, exec, args, gitspaceLogger)
|
||||||
},
|
},
|
||||||
StopOnFailure: true,
|
StopOnFailure: true,
|
||||||
},
|
}}
|
||||||
// Post-create and Post-start steps
|
|
||||||
{
|
// Add the postCreateCommand lifecycle hooks to the steps
|
||||||
Name: "Execute PostCreate Command",
|
for _, lifecycleHook := range lifecycleHookSteps[PostCreateAction] {
|
||||||
|
steps = append(steps, step{
|
||||||
|
Name: fmt.Sprintf("Execute postCreateCommand from %s", lifecycleHook.Source),
|
||||||
Execute: func(
|
Execute: func(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
exec *devcontainer.Exec,
|
exec *devcontainer.Exec,
|
||||||
gitspaceLogger gitspaceTypes.GitspaceLogger,
|
gitspaceLogger gitspaceTypes.GitspaceLogger,
|
||||||
) error {
|
) error {
|
||||||
command := ExtractLifecycleCommands(PostCreateAction, devcontainerConfig)
|
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger,
|
||||||
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger, command, PostCreateAction)
|
lifecycleHook.Command.ToCommandArray(), PostCreateAction)
|
||||||
},
|
},
|
||||||
StopOnFailure: false,
|
StopOnFailure: lifecycleHook.StopOnFailure,
|
||||||
},
|
})
|
||||||
{
|
|
||||||
Name: "Execute PostStart Command",
|
|
||||||
Execute: func(
|
|
||||||
ctx context.Context,
|
|
||||||
exec *devcontainer.Exec,
|
|
||||||
gitspaceLogger gitspaceTypes.GitspaceLogger,
|
|
||||||
) error {
|
|
||||||
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
|
|
||||||
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger, command, PostStartAction)
|
|
||||||
},
|
|
||||||
StopOnFailure: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the postStartCommand lifecycle hooks to the steps
|
||||||
|
for _, lifecycleHook := range lifecycleHookSteps[PostStartAction] {
|
||||||
|
steps = append(steps, step{
|
||||||
|
Name: fmt.Sprintf("Execute postStartCommand from %s", lifecycleHook.Source),
|
||||||
|
Execute: func(
|
||||||
|
ctx context.Context,
|
||||||
|
exec *devcontainer.Exec,
|
||||||
|
gitspaceLogger gitspaceTypes.GitspaceLogger,
|
||||||
|
) error {
|
||||||
|
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger,
|
||||||
|
lifecycleHook.Command.ToCommandArray(), PostStartAction)
|
||||||
|
},
|
||||||
|
StopOnFailure: lifecycleHook.StopOnFailure,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupGitspaceAndIDE initializes Gitspace and IdeType by registering and executing the setup steps.
|
// setupGitspaceAndIDE initializes Gitspace and IdeType by registering and executing the setup steps.
|
||||||
|
@ -622,20 +713,20 @@ func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE(
|
||||||
resolvedRepoDetails scm.ResolvedDetails,
|
resolvedRepoDetails scm.ResolvedDetails,
|
||||||
defaultBaseImage string,
|
defaultBaseImage string,
|
||||||
environment []string,
|
environment []string,
|
||||||
|
lifecycleHookSteps map[PostAction][]*LifecycleHookStep,
|
||||||
) error {
|
) error {
|
||||||
homeDir := GetUserHomeDir(exec.RemoteUser)
|
homeDir := GetUserHomeDir(exec.RemoteUser)
|
||||||
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
|
|
||||||
codeRepoDir := filepath.Join(homeDir, resolvedRepoDetails.RepoName)
|
codeRepoDir := filepath.Join(homeDir, resolvedRepoDetails.RepoName)
|
||||||
|
|
||||||
steps := e.buildSetupSteps(
|
steps := e.buildSetupSteps(
|
||||||
ctx,
|
|
||||||
ideService,
|
ideService,
|
||||||
gitspaceConfig,
|
gitspaceConfig,
|
||||||
resolvedRepoDetails,
|
resolvedRepoDetails,
|
||||||
defaultBaseImage,
|
defaultBaseImage,
|
||||||
environment,
|
environment,
|
||||||
devcontainerConfig,
|
codeRepoDir,
|
||||||
codeRepoDir)
|
lifecycleHookSteps,
|
||||||
|
)
|
||||||
|
|
||||||
// Execute the registered steps
|
// Execute the registered steps
|
||||||
if err := e.ExecuteSteps(ctx, exec, gitspaceLogger, steps); err != nil {
|
if err := e.ExecuteSteps(ctx, exec, gitspaceLogger, steps); err != nil {
|
||||||
|
|
|
@ -159,6 +159,10 @@ func getNetworkMode(runArgsMap map[types.RunArg]*types.RunArgValue) container.Ne
|
||||||
return container.NetworkMode(getArgValueString(runArgsMap, types.RunArgNetwork))
|
return container.NetworkMode(getArgValueString(runArgsMap, types.RunArgNetwork))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCapAdd(runArgsMap map[types.RunArg]*types.RunArgValue) strslice.StrSlice {
|
||||||
|
return getArgValueStringSlice(runArgsMap, types.RunArgCapAdd)
|
||||||
|
}
|
||||||
|
|
||||||
func getCapDrop(runArgsMap map[types.RunArg]*types.RunArgValue) strslice.StrSlice {
|
func getCapDrop(runArgsMap map[types.RunArg]*types.RunArgValue) strslice.StrSlice {
|
||||||
return getArgValueStringSlice(runArgsMap, types.RunArgCapDrop)
|
return getArgValueStringSlice(runArgsMap, types.RunArgCapDrop)
|
||||||
}
|
}
|
||||||
|
@ -253,6 +257,10 @@ func getAutoRemove(runArgsMap map[types.RunArg]*types.RunArgValue) bool {
|
||||||
return getArgValueBool(runArgsMap, types.RunArgRm)
|
return getArgValueBool(runArgsMap, types.RunArgRm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPrivileged(runArgsMap map[types.RunArg]*types.RunArgValue) *bool {
|
||||||
|
return getArgValueBoolPtr(runArgsMap, types.RunArgPrivileged)
|
||||||
|
}
|
||||||
|
|
||||||
func getInit(runArgsMap map[types.RunArg]*types.RunArgValue) *bool {
|
func getInit(runArgsMap map[types.RunArg]*types.RunArgValue) *bool {
|
||||||
return getArgValueBoolPtr(runArgsMap, types.RunArgInit)
|
return getArgValueBoolPtr(runArgsMap, types.RunArgInit)
|
||||||
}
|
}
|
||||||
|
@ -329,6 +337,19 @@ func getEntrypoint(runArgsMap map[types.RunArg]*types.RunArgValue) []string {
|
||||||
return getArgValueStringSlice(runArgsMap, types.RunArgEntrypoint)
|
return getArgValueStringSlice(runArgsMap, types.RunArgEntrypoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMounts(runArgsMap map[types.RunArg]*types.RunArgValue) ([]*types.Mount, error) {
|
||||||
|
rawMounts, ok := runArgsMap[types.RunArgMount]
|
||||||
|
var mounts []*types.Mount
|
||||||
|
if ok {
|
||||||
|
parsedMounts, err := types.ParseMountsFromStringSlice(rawMounts.Values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parsedMounts, nil
|
||||||
|
}
|
||||||
|
return mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getHealthCheckConfig(runArgsMap map[types.RunArg]*types.RunArgValue) (*container.HealthConfig, error) {
|
func getHealthCheckConfig(runArgsMap map[types.RunArg]*types.RunArgValue) (*container.HealthConfig, error) {
|
||||||
var healthConfig = &container.HealthConfig{}
|
var healthConfig = &container.HealthConfig{}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -28,9 +29,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
linuxHome = "/home"
|
linuxHome = "/home"
|
||||||
deprecatedRemoteUser = "harness"
|
deprecatedRemoteUser = "harness"
|
||||||
gitspaceRemoteUserLabel = "gitspace.remote.user"
|
gitspaceRemoteUserLabel = "gitspace.remote.user"
|
||||||
|
gitspaceLifeCycleHooksLabel = "gitspace.lifecycle.hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetGitspaceContainerName(config types.GitspaceConfig) string {
|
func GetGitspaceContainerName(config types.GitspaceConfig) string {
|
||||||
|
@ -85,6 +87,20 @@ func ExtractRemoteUserFromLabels(inspectResp dockerTypes.ContainerJSON) string {
|
||||||
return remoteUser
|
return remoteUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractLifecycleHooksFromLabels(
|
||||||
|
inspectResp dockerTypes.ContainerJSON,
|
||||||
|
) (map[PostAction][]*LifecycleHookStep, error) {
|
||||||
|
var lifecycleHooks = make(map[PostAction][]*LifecycleHookStep)
|
||||||
|
|
||||||
|
if lifecycleHooksStr, ok := inspectResp.Config.Labels[gitspaceLifeCycleHooksLabel]; ok {
|
||||||
|
err := json.Unmarshal([]byte(lifecycleHooksStr), &lifecycleHooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lifecycleHooks, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteLifecycleCommands executes commands in parallel, logs with numbers, and prefixes all logs.
|
// ExecuteLifecycleCommands executes commands in parallel, logs with numbers, and prefixes all logs.
|
||||||
func ExecuteLifecycleCommands(
|
func ExecuteLifecycleCommands(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
- name: --cap-add
|
- name: --cap-add
|
||||||
short_hand:
|
short_hand:
|
||||||
supported: false
|
supported: true
|
||||||
blocked_values: { }
|
blocked_values: { }
|
||||||
allowed_values: { }
|
allowed_values: { }
|
||||||
allow_multiple_occurrences: true
|
allow_multiple_occurrences: true
|
||||||
|
@ -396,6 +396,7 @@
|
||||||
supported: true
|
supported: true
|
||||||
blocked_values:
|
blocked_values:
|
||||||
^gitspace\.remote\.user=: true
|
^gitspace\.remote\.user=: true
|
||||||
|
^gitspace\.lifecycle\.hooks=: true
|
||||||
allowed_values: { }
|
allowed_values: { }
|
||||||
allow_multiple_occurrences: true
|
allow_multiple_occurrences: true
|
||||||
|
|
||||||
|
@ -471,7 +472,7 @@
|
||||||
|
|
||||||
- name: --mount
|
- name: --mount
|
||||||
short_hand:
|
short_hand:
|
||||||
supported: false
|
supported: true
|
||||||
blocked_values: { }
|
blocked_values: { }
|
||||||
allowed_values: { }
|
allowed_values: { }
|
||||||
allow_multiple_occurrences: true
|
allow_multiple_occurrences: true
|
||||||
|
@ -543,7 +544,7 @@
|
||||||
|
|
||||||
- name: --privileged
|
- name: --privileged
|
||||||
short_hand:
|
short_hand:
|
||||||
supported: false
|
supported: true
|
||||||
blocked_values: { }
|
blocked_values: { }
|
||||||
allowed_values: { }
|
allowed_values: { }
|
||||||
allow_multiple_occurrences: true
|
allow_multiple_occurrences: true
|
||||||
|
|
|
@ -172,7 +172,12 @@ func generateDockerFileWithFeatures(
|
||||||
containerUserHomeDir string,
|
containerUserHomeDir string,
|
||||||
remoteUserHomeDir string,
|
remoteUserHomeDir string,
|
||||||
) error {
|
) error {
|
||||||
dockerFile := fmt.Sprintf("FROM %s\nARG %s=%s\nARG %s=%s\nARG %s=%s\nARG %s=%s\nCOPY ./devcontainer-features %s",
|
dockerFile := fmt.Sprintf(`FROM %s
|
||||||
|
ARG %s=%s
|
||||||
|
ARG %s=%s
|
||||||
|
ARG %s=%s
|
||||||
|
ARG %s=%s
|
||||||
|
COPY ./devcontainer-features %s`,
|
||||||
imageName, convertOptionsToEnvVariables("_CONTAINER_USER"), containerUser,
|
imageName, convertOptionsToEnvVariables("_CONTAINER_USER"), containerUser,
|
||||||
convertOptionsToEnvVariables("_REMOTE_USER"), remoteUser,
|
convertOptionsToEnvVariables("_REMOTE_USER"), remoteUser,
|
||||||
convertOptionsToEnvVariables("_CONTAINER_USER_HOME"), containerUserHomeDir,
|
convertOptionsToEnvVariables("_CONTAINER_USER_HOME"), containerUserHomeDir,
|
||||||
|
|
|
@ -68,21 +68,24 @@ func DownloadFeatures(
|
||||||
downloadQueue <- featureSource{sourceURL: key, sourceType: value.SourceType}
|
downloadQueue <- featureSource{sourceURL: key, sourceType: value.SourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add ctx based cancellations to the below goroutines.
|
|
||||||
|
|
||||||
// NOTE: The following logic might see performance issues with spikes in memory and CPU usage.
|
// NOTE: The following logic might see performance issues with spikes in memory and CPU usage.
|
||||||
// If there are such issues, we can introduce throttling on the basis of memory, CPU, etc.
|
// If there are such issues, we can introduce throttling on the basis of memory, CPU, etc.
|
||||||
go func() {
|
go func(ctx context.Context) {
|
||||||
for source := range downloadQueue {
|
for source := range downloadQueue {
|
||||||
startCh <- 1
|
select {
|
||||||
go func(source featureSource) {
|
case <-ctx.Done():
|
||||||
defer func(endCh chan int) { endCh <- 1 }(endCh)
|
return
|
||||||
err := downloadFeature(ctx, gitspaceInstanceIdentifier, &source, &featuresToBeDownloaded,
|
default:
|
||||||
downloadQueue, &downloadedFeatures)
|
startCh <- 1
|
||||||
errorCh <- err
|
go func(source featureSource) {
|
||||||
}(source)
|
defer func(endCh chan int) { endCh <- 1 }(endCh)
|
||||||
|
err := downloadFeature(ctx, gitspaceInstanceIdentifier, &source, &featuresToBeDownloaded,
|
||||||
|
downloadQueue, &downloadedFeatures)
|
||||||
|
errorCh <- err
|
||||||
|
}(source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}(ctx)
|
||||||
|
|
||||||
var totalStart int
|
var totalStart int
|
||||||
var totalEnd int
|
var totalEnd int
|
||||||
|
@ -90,6 +93,8 @@ func DownloadFeatures(
|
||||||
waitLoop:
|
waitLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
case start := <-startCh:
|
case start := <-startCh:
|
||||||
totalStart += start
|
totalStart += start
|
||||||
case end := <-endCh:
|
case end := <-endCh:
|
||||||
|
@ -116,6 +121,7 @@ waitLoop:
|
||||||
close(startCh)
|
close(startCh)
|
||||||
close(endCh)
|
close(endCh)
|
||||||
close(downloadQueue)
|
close(downloadQueue)
|
||||||
|
close(errorCh)
|
||||||
|
|
||||||
if downloadError != nil {
|
if downloadError != nil {
|
||||||
return nil, downloadError
|
return nil, downloadError
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
|
@ -40,8 +42,8 @@ type DevcontainerConfig struct {
|
||||||
RemoteUser string `json:"remoteUser,omitempty"`
|
RemoteUser string `json:"remoteUser,omitempty"`
|
||||||
Features *Features `json:"features,omitempty"`
|
Features *Features `json:"features,omitempty"`
|
||||||
OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"`
|
OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"`
|
||||||
Privileged bool `json:"privileged,omitempty"`
|
Privileged *bool `json:"privileged,omitempty"`
|
||||||
Init bool `json:"init,omitempty"`
|
Init *bool `json:"init,omitempty"`
|
||||||
CapAdd []string `json:"capAdd,omitempty"`
|
CapAdd []string `json:"capAdd,omitempty"`
|
||||||
SecurityOpt []string `json:"securityOpt,omitempty"`
|
SecurityOpt []string `json:"securityOpt,omitempty"`
|
||||||
Mounts []*Mount `json:"mounts,omitempty"`
|
Mounts []*Mount `json:"mounts,omitempty"`
|
||||||
|
@ -246,11 +248,79 @@ type Mount struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mount) UnmarshalJSON(data []byte) error {
|
func (m *Mount) UnmarshalJSON(data []byte) error {
|
||||||
// TODO: Add support for unmarshalling mount data from a string input
|
if err := json.Unmarshal(data, m); err == nil {
|
||||||
var mount Mount
|
return nil
|
||||||
err := json.Unmarshal(data, &mount)
|
}
|
||||||
|
dst, err := stringToObject(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*m = *dst
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseMountsFromRawSlice(values []any) ([]*Mount, error) {
|
||||||
|
var mounts []*Mount
|
||||||
|
for _, value := range values {
|
||||||
|
if mountValue, isObject := value.(*Mount); isObject {
|
||||||
|
mounts = append(mounts, mountValue)
|
||||||
|
} else if strVal, isString := value.(string); isString {
|
||||||
|
dst, err := stringToObject(strVal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mounts = append(mounts, dst)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid mount value: %+v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMountsFromStringSlice(values []string) ([]*Mount, error) {
|
||||||
|
var mounts []*Mount
|
||||||
|
for _, value := range values {
|
||||||
|
dst, err := stringToObject(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mounts = append(mounts, dst)
|
||||||
|
}
|
||||||
|
return mounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToObject(mountStr string) (*Mount, error) {
|
||||||
|
csvReader := csv.NewReader(strings.NewReader(mountStr))
|
||||||
|
fields, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newMount := Mount{Type: "volume"}
|
||||||
|
for _, field := range fields {
|
||||||
|
key, val, ok := strings.Cut(field, "=")
|
||||||
|
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid format for mount field: %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
newMount.Type = strings.ToLower(val)
|
||||||
|
case "source", "src":
|
||||||
|
newMount.Source = val
|
||||||
|
if strings.HasPrefix(val, "."+string(filepath.Separator)) || val == "." {
|
||||||
|
if abs, err := filepath.Abs(val); err == nil {
|
||||||
|
newMount.Source = abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "target", "dst", "destination":
|
||||||
|
newMount.Target = val
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &newMount, nil
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,9 @@ const (
|
||||||
RunArgSysctl = RunArg("--sysctl")
|
RunArgSysctl = RunArg("--sysctl")
|
||||||
RunArgUlimit = RunArg("--ulimit")
|
RunArgUlimit = RunArg("--ulimit")
|
||||||
RunArgUser = RunArg("--user")
|
RunArgUser = RunArg("--user")
|
||||||
|
RunArgPrivileged = RunArg("--privileged")
|
||||||
|
RunArgCapAdd = RunArg("--cap-add")
|
||||||
|
RunArgMount = RunArg("--mount")
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunArgDefinition struct {
|
type RunArgDefinition struct {
|
||||||
|
|
Loading…
Reference in New Issue