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/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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue