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
Dhruv Dhruv 2025-01-15 07:22:55 +00:00 committed by Harness
parent f8957318f6
commit b94a78c795
9 changed files with 571 additions and 84 deletions

View File

@ -40,6 +40,7 @@ import (
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/gotidy/ptr"
"github.com/rs/zerolog/log"
)
@ -142,35 +143,49 @@ func CreateContainer(
runArgsMap map[types.RunArg]*types.RunArgValue,
containerUser string,
remoteUser string,
) error {
features []*types.ResolvedFeature,
devcontainerConfig types.DevcontainerConfig,
metadataFromImage map[string]any,
) (map[PostAction][]*LifecycleHookStep, error) {
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 {
return err
return nil, err
}
healthCheckConfig, err := getHealthCheckConfig(runArgsMap)
if err != nil {
return err
return nil, err
}
stopTimeout, err := getStopTimeout(runArgsMap)
if err != nil {
return err
return nil, err
}
entrypoint := getEntrypoint(runArgsMap)
entrypoint := mergeEntrypoints(features, runArgsMap)
var cmd strslice.StrSlice
if len(entrypoint) == 0 {
entrypoint = []string{"/bin/sh"}
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)
// Setting the following so that it can be read later to form gitspace URL.
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
containerConfig := &container.Config{
Hostname: getHostname(runArgsMap),
@ -190,10 +205,74 @@ func CreateContainer(
_, err = dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
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.
@ -221,6 +300,9 @@ func prepareHostConfig(
mountType mount.Type,
portBindings nat.PortMap,
runArgsMap map[types.RunArg]*types.RunArgValue,
features []*types.ResolvedFeature,
devcontainerConfig types.DevcontainerConfig,
metadataFromImage map[string]any,
) (*container.HostConfig, error) {
hostResources, err := getHostResources(runArgsMap)
if err != nil {
@ -247,21 +329,27 @@ func prepareHostConfig(
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{
PortBindings: portBindings,
Mounts: []mount.Mount{
{
Type: mountType,
Source: bindMountSource,
Target: bindMountTarget,
},
},
PortBindings: portBindings,
Mounts: mergedMounts,
Resources: hostResources,
Annotations: getAnnotations(runArgsMap),
ExtraHosts: extraHosts,
NetworkMode: getNetworkMode(runArgsMap),
RestartPolicy: restartPolicy,
AutoRemove: getAutoRemove(runArgsMap),
CapAdd: mergeCapAdd(devcontainerConfig, runArgsMap, features, metadataFromImage),
CapDrop: getCapDrop(runArgsMap),
CgroupnsMode: getCgroupNSMode(runArgsMap),
DNS: getDNS(runArgsMap),
@ -269,12 +357,13 @@ func prepareHostConfig(
DNSSearch: getDNSSearch(runArgsMap),
IpcMode: getIPCMode(runArgsMap),
Isolation: getIsolation(runArgsMap),
Init: getInit(runArgsMap),
Init: mergeInit(devcontainerConfig, runArgsMap, features, metadataFromImage),
Links: getLinks(runArgsMap),
OomScoreAdj: oomScoreAdj,
PidMode: getPIDMode(runArgsMap),
Privileged: mergePriviledged(devcontainerConfig, runArgsMap, features, metadataFromImage),
Runtime: getRuntime(runArgsMap),
SecurityOpt: getSecurityOpt(runArgsMap),
SecurityOpt: mergeSecurityOpts(devcontainerConfig, runArgsMap, features, metadataFromImage),
StorageOpt: getStorageOpt(runArgsMap),
ShmSize: shmSize,
Sysctls: getSysctls(runArgsMap),
@ -283,6 +372,186 @@ func prepareHostConfig(
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(
ctx context.Context,
containerName string,
@ -542,17 +811,22 @@ func GetContainerResponse(
}, nil
}
func GetRemoteUserFromContainerLabel(
func GetGitspaceInfoFromContainerLabels(
ctx context.Context,
containerName string,
dockerClient *client.Client,
) (string, error) {
) (string, map[PostAction][]*LifecycleHookStep, error) {
inspectResp, err := dockerClient.ContainerInspect(ctx, containerName)
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.

View File

@ -53,6 +53,13 @@ type step struct {
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.
func (e *EmbeddedDockerOrchestrator) ExecuteSteps(
ctx context.Context,
@ -181,7 +188,7 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
}
defer e.flushLogStream(logStreamInstance, gitspaceConfig.ID)
remoteUser, err := GetRemoteUserFromContainerLabel(ctx, containerName, dockerClient)
remoteUser, lifecycleHooks, err := GetGitspaceInfoFromContainerLabels(ctx, containerName, dockerClient)
if err != nil {
return fmt.Errorf("error getting remote user for gitspace instance %s: %w",
gitspaceConfig.GitspaceInstance.Identifier, err)
@ -217,13 +224,24 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
return err
}
// Execute post-start command
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance, command, PostStartAction)
if startErr != nil {
log.Warn().Msgf("Error is post-start command, continuing : %s", startErr.Error())
if len(lifecycleHooks) > 0 && len(lifecycleHooks[PostStartAction]) > 0 {
for _, lifecycleHook := range lifecycleHooks[PostStartAction] {
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance,
lifecycleHook.Command.ToCommandArray(), PostStartAction)
if startErr != nil {
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
}
@ -422,26 +440,43 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
containerUser := GetContainerUser(runArgsMap, devcontainerConfig, metadataFromImage, imageUser)
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("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
err = CreateContainer(
lifecycleHookSteps, err := CreateContainer(
ctx,
dockerClient,
imageName,
containerName,
gitspaceLogger,
storage,
homeDir,
remoteUserHomeDir,
mount.TypeVolume,
portMappings,
environment,
runArgsMap,
containerUser,
remoteUser,
features,
resolvedRepoDetails.DevcontainerConfig,
metadataFromImage,
)
if err != nil {
return err
@ -456,7 +491,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
exec := &devcontainer.Exec{
ContainerName: containerName,
DockerClient: dockerClient,
DefaultWorkingDir: homeDir,
DefaultWorkingDir: remoteUserHomeDir,
RemoteUser: remoteUser,
AccessKey: *gitspaceConfig.GitspaceInstance.AccessKey,
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
@ -471,6 +506,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
resolvedRepoDetails,
defaultBaseImage,
environment,
lifecycleHookSteps,
); err != nil {
return logStreamWrapError(gitspaceLogger, "Error while setting up gitspace", err)
}
@ -478,18 +514,65 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
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.
func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
_ context.Context,
ideService ide.IDE,
gitspaceConfig types.GitspaceConfig,
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
environment []string,
devcontainerConfig types.DevcontainerConfig,
codeRepoDir string,
lifecycleHookSteps map[PostAction][]*LifecycleHookStep,
) []step {
return []step{
steps := []step{
{
Name: "Validate Supported OS",
Execute: utils.ValidateSupportedOS,
@ -583,33 +666,41 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
return ideService.Run(ctx, exec, args, gitspaceLogger)
},
StopOnFailure: true,
},
// Post-create and Post-start steps
{
Name: "Execute PostCreate Command",
}}
// Add the postCreateCommand lifecycle hooks to the steps
for _, lifecycleHook := range lifecycleHookSteps[PostCreateAction] {
steps = append(steps, step{
Name: fmt.Sprintf("Execute postCreateCommand from %s", lifecycleHook.Source),
Execute: func(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
command := ExtractLifecycleCommands(PostCreateAction, devcontainerConfig)
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger, command, PostCreateAction)
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger,
lifecycleHook.Command.ToCommandArray(), PostCreateAction)
},
StopOnFailure: false,
},
{
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,
},
StopOnFailure: lifecycleHook.StopOnFailure,
})
}
// 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.
@ -622,20 +713,20 @@ func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE(
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
environment []string,
lifecycleHookSteps map[PostAction][]*LifecycleHookStep,
) error {
homeDir := GetUserHomeDir(exec.RemoteUser)
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
codeRepoDir := filepath.Join(homeDir, resolvedRepoDetails.RepoName)
steps := e.buildSetupSteps(
ctx,
ideService,
gitspaceConfig,
resolvedRepoDetails,
defaultBaseImage,
environment,
devcontainerConfig,
codeRepoDir)
codeRepoDir,
lifecycleHookSteps,
)
// Execute the registered steps
if err := e.ExecuteSteps(ctx, exec, gitspaceLogger, steps); err != nil {

View File

@ -159,6 +159,10 @@ func getNetworkMode(runArgsMap map[types.RunArg]*types.RunArgValue) container.Ne
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 {
return getArgValueStringSlice(runArgsMap, types.RunArgCapDrop)
}
@ -253,6 +257,10 @@ func getAutoRemove(runArgsMap map[types.RunArg]*types.RunArgValue) bool {
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 {
return getArgValueBoolPtr(runArgsMap, types.RunArgInit)
}
@ -329,6 +337,19 @@ func getEntrypoint(runArgsMap map[types.RunArg]*types.RunArgValue) []string {
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) {
var healthConfig = &container.HealthConfig{}

View File

@ -16,6 +16,7 @@ package container
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sync"
@ -28,9 +29,10 @@ import (
)
const (
linuxHome = "/home"
deprecatedRemoteUser = "harness"
gitspaceRemoteUserLabel = "gitspace.remote.user"
linuxHome = "/home"
deprecatedRemoteUser = "harness"
gitspaceRemoteUserLabel = "gitspace.remote.user"
gitspaceLifeCycleHooksLabel = "gitspace.lifecycle.hooks"
)
func GetGitspaceContainerName(config types.GitspaceConfig) string {
@ -85,6 +87,20 @@ func ExtractRemoteUserFromLabels(inspectResp dockerTypes.ContainerJSON) string {
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.
func ExecuteLifecycleCommands(
ctx context.Context,

View File

@ -36,7 +36,7 @@
- name: --cap-add
short_hand:
supported: false
supported: true
blocked_values: { }
allowed_values: { }
allow_multiple_occurrences: true
@ -396,6 +396,7 @@
supported: true
blocked_values:
^gitspace\.remote\.user=: true
^gitspace\.lifecycle\.hooks=: true
allowed_values: { }
allow_multiple_occurrences: true
@ -471,7 +472,7 @@
- name: --mount
short_hand:
supported: false
supported: true
blocked_values: { }
allowed_values: { }
allow_multiple_occurrences: true
@ -543,7 +544,7 @@
- name: --privileged
short_hand:
supported: false
supported: true
blocked_values: { }
allowed_values: { }
allow_multiple_occurrences: true

View File

@ -172,7 +172,12 @@ func generateDockerFileWithFeatures(
containerUserHomeDir string,
remoteUserHomeDir string,
) 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,
convertOptionsToEnvVariables("_REMOTE_USER"), remoteUser,
convertOptionsToEnvVariables("_CONTAINER_USER_HOME"), containerUserHomeDir,

View File

@ -68,21 +68,24 @@ func DownloadFeatures(
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.
// 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 {
startCh <- 1
go func(source featureSource) {
defer func(endCh chan int) { endCh <- 1 }(endCh)
err := downloadFeature(ctx, gitspaceInstanceIdentifier, &source, &featuresToBeDownloaded,
downloadQueue, &downloadedFeatures)
errorCh <- err
}(source)
select {
case <-ctx.Done():
return
default:
startCh <- 1
go func(source featureSource) {
defer func(endCh chan int) { endCh <- 1 }(endCh)
err := downloadFeature(ctx, gitspaceInstanceIdentifier, &source, &featuresToBeDownloaded,
downloadQueue, &downloadedFeatures)
errorCh <- err
}(source)
}
}
}()
}(ctx)
var totalStart int
var totalEnd int
@ -90,6 +93,8 @@ func DownloadFeatures(
waitLoop:
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case start := <-startCh:
totalStart += start
case end := <-endCh:
@ -116,6 +121,7 @@ waitLoop:
close(startCh)
close(endCh)
close(downloadQueue)
close(errorCh)
if downloadError != nil {
return nil, downloadError

View File

@ -15,9 +15,11 @@
package types
import (
"encoding/csv"
"encoding/json"
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/harness/gitness/types/enum"
@ -40,8 +42,8 @@ type DevcontainerConfig struct {
RemoteUser string `json:"remoteUser,omitempty"`
Features *Features `json:"features,omitempty"`
OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"`
Privileged bool `json:"privileged,omitempty"`
Init bool `json:"init,omitempty"`
Privileged *bool `json:"privileged,omitempty"`
Init *bool `json:"init,omitempty"`
CapAdd []string `json:"capAdd,omitempty"`
SecurityOpt []string `json:"securityOpt,omitempty"`
Mounts []*Mount `json:"mounts,omitempty"`
@ -246,11 +248,79 @@ type Mount struct {
}
func (m *Mount) UnmarshalJSON(data []byte) error {
// TODO: Add support for unmarshalling mount data from a string input
var mount Mount
err := json.Unmarshal(data, &mount)
if err := json.Unmarshal(data, m); err == nil {
return nil
}
dst, err := stringToObject(string(data))
if err != nil {
return err
}
*m = *dst
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
}

View File

@ -82,6 +82,9 @@ const (
RunArgSysctl = RunArg("--sysctl")
RunArgUlimit = RunArg("--ulimit")
RunArgUser = RunArg("--user")
RunArgPrivileged = RunArg("--privileged")
RunArgCapAdd = RunArg("--cap-add")
RunArgMount = RunArg("--mount")
)
type RunArgDefinition struct {