feat: [CDE-470]: use stream logger and buffer the results for gitspaces exec (#3008)

* feat: [CDE-470]: set env variable for flavours
* feat: [CDE-470]: set env variable for flavours
* feat: [CDE-470]: set env variable for flavours
* feat: [CDE-470]: set env variable for flavours
* feat: [CDE-470]: format code
* feat: [CDE-470]: format code
* feat: [CDE-470]: format code
* feat: [CDE-470]: use stream logger
* feat: [CDE-470]: use stream logger
* feat: [CDE-470]: use stream logger
* feat: [CDE-470]: env variables for VS Code desktop
This commit is contained in:
Ansuman Satapathy 2024-11-18 04:55:01 +00:00 committed by Harness
parent fa05570220
commit 17dea68b57
16 changed files with 420 additions and 244 deletions

View File

@ -17,17 +17,24 @@ package common
import (
"context"
"fmt"
"strings"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/template"
"github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types/enum"
)
const templateSupportedOSDistribution = "supported_os_distribution.sh"
const templateVsCodeWebToolsInstallation = "install_tools_vs_code_web.sh"
const templateVsCodeToolsInstallation = "install_tools_vs_code.sh"
const templateSetEnv = "set_env.sh"
func ValidateSupportedOS(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
func ValidateSupportedOS(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
) error {
// TODO: Currently not supporting arch, freebsd and alpine.
// For alpine wee need to install multiple things from
// https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites
@ -36,79 +43,127 @@ func ValidateSupportedOS(ctx context.Context, exec *devcontainer.Exec) ([]byte,
OSInfoScript: osDetectScript,
})
if err != nil {
return nil, fmt.Errorf("failed to generate scipt to validate supported os distribution from template %s: %w",
return fmt.Errorf("failed to generate scipt to validate supported os distribution from template %s: %w",
templateSupportedOSDistribution, err)
}
output, err := exec.ExecuteCommandInHomeDirectory(ctx, script, true, false)
gitspaceLogger.Info("Validate supported OSes...")
err = ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("error while detecting os distribution: %w", err)
return fmt.Errorf("error while detecting os distribution: %w", err)
}
return output, nil
return nil
}
func InstallTools(ctx context.Context, exec *devcontainer.Exec, ideType enum.IDEType) ([]byte, error) {
func InstallTools(
ctx context.Context,
exec *devcontainer.Exec,
ideType enum.IDEType,
gitspaceLogger types.GitspaceLogger,
) error {
switch ideType {
case enum.IDETypeVSCodeWeb:
{
output, err := InstallToolsForVsCodeWeb(ctx, exec)
if err != nil {
return []byte(output), err
}
return []byte(output), nil
err := InstallToolsForVsCodeWeb(ctx, exec, gitspaceLogger)
if err != nil {
return err
}
return nil
case enum.IDETypeVSCode:
{
output, err := InstallToolsForVsCode(ctx, exec)
if err != nil {
return []byte(output), err
}
return []byte(output), nil
err := InstallToolsForVsCode(ctx, exec, gitspaceLogger)
if err != nil {
return err
}
return nil
}
return nil, nil
return nil
}
func InstallToolsForVsCodeWeb(ctx context.Context, exec *devcontainer.Exec) (string, error) {
func InstallToolsForVsCodeWeb(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
) error {
script, err := template.GenerateScriptFromTemplate(
templateVsCodeWebToolsInstallation, &template.InstallToolsPayload{
OSInfoScript: osDetectScript,
})
if err != nil {
return "", fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to install tools for vs code web from template %s: %w",
templateVsCodeWebToolsInstallation, err)
}
output := "Installing tools for vs code web inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, true, false)
gitspaceLogger.Info("Installing tools for vs code web inside container")
gitspaceLogger.Info("Tools installation output...")
err = ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return "", fmt.Errorf("failed to install tools for vs code web: %w", err)
return fmt.Errorf("failed to install tools for vs code web: %w", err)
}
output += "Successfully installed tools for vs code web\n"
return output, nil
gitspaceLogger.Info("Successfully installed tools for vs code web")
return nil
}
func InstallToolsForVsCode(ctx context.Context, exec *devcontainer.Exec) (string, error) {
func InstallToolsForVsCode(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
) error {
script, err := template.GenerateScriptFromTemplate(
templateVsCodeToolsInstallation, &template.InstallToolsPayload{
OSInfoScript: osDetectScript,
})
if err != nil {
return "", fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to install tools for vs code from template %s: %w",
templateVsCodeToolsInstallation, err)
}
output := "Installing tools for vs code inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, true, false)
gitspaceLogger.Info("Installing tools for vs code in container")
err = ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return "", fmt.Errorf("failed to install tools for vs code: %w", err)
return fmt.Errorf("failed to install tools for vs code: %w", err)
}
output += "Successfully installed tools for vs code\n"
return output, nil
gitspaceLogger.Info("Successfully installed tools for vs code")
return nil
}
func SetEnv(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
environment []string,
) error {
// Join the elements with a newline character
result := strings.Join(environment, "\n")
script, err := template.GenerateScriptFromTemplate(
templateSetEnv, &template.SetEnvPayload{
EnvVariables: result,
})
if err != nil {
return fmt.Errorf("failed to generate scipt to set env from template %s: %w",
templateSetEnv, err)
}
gitspaceLogger.Info("Setting env...")
err = ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return fmt.Errorf("error while setting env vars: %w", err)
}
return nil
}
func ExecuteCommandInHomeDirAndLog(
ctx context.Context,
exec *devcontainer.Exec,
script string,
root bool,
gitspaceLogger types.GitspaceLogger,
) error {
outputCh := make(chan []byte)
err := exec.ExecuteCommandInHomeDirectory(ctx, script, root, false, outputCh)
for output := range outputCh {
// Log output from the command as a string
if len(output) > 0 {
gitspaceLogger.Info(string(output))
}
}
return err
}

View File

@ -22,7 +22,7 @@ import (
"strconv"
"strings"
orchestratorTypes "github.com/harness/gitness/app/gitspace/orchestrator/types"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/docker/docker/api/types/container"
@ -47,7 +47,7 @@ var containerStateMapping = map[string]State{
}
// Helper function to log messages and handle error wrapping.
func logStreamWrapError(gitspaceLogger orchestratorTypes.GitspaceLogger, msg string, err error) error {
func logStreamWrapError(gitspaceLogger gitspaceTypes.GitspaceLogger, msg string, err error) error {
gitspaceLogger.Error(msg, err)
return fmt.Errorf("%s: %w", msg, err)
}
@ -58,7 +58,7 @@ func ManageContainer(
action Action,
containerName string,
dockerClient *client.Client,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
var err error
switch action {
@ -123,7 +123,7 @@ func CreateContainer(
dockerClient *client.Client,
imageName string,
containerName string,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
bindMountSource string,
bindMountTarget string,
mountType mount.Type,
@ -225,7 +225,7 @@ func PullImage(
ctx context.Context,
imageName string,
dockerClient *client.Client,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
gitspaceLogger.Info("Pulling image: " + imageName)

View File

@ -23,9 +23,9 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/git"
"github.com/harness/gitness/app/gitspace/orchestrator/ide"
orchestratorTypes "github.com/harness/gitness/app/gitspace/orchestrator/types"
"github.com/harness/gitness/app/gitspace/orchestrator/user"
"github.com/harness/gitness/app/gitspace/scm"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
@ -34,11 +34,12 @@ import (
func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
ideService ide.IDE,
gitspaceConfig types.GitspaceConfig,
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
environment []string,
) error {
homeDir := GetUserHomeDir(gitspaceConfig.GitspaceUser.Identifier)
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
@ -47,45 +48,49 @@ func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE(
// Register setup steps
e.RegisterStep("Validate Supported OS", ValidateSupportedOS, true)
e.RegisterStep("Manage User",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return ManageUser(ctx, exec, e.userService, gitspaceLogger)
}, true)
e.RegisterStep("Set environment",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return SetEnv(ctx, exec, gitspaceLogger, environment)
}, true)
e.RegisterStep("Install Tools",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return InstallTools(ctx, exec, gitspaceLogger, gitspaceConfig.IDE)
}, true)
e.RegisterStep("Setup IDE",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return SetupIDE(ctx, exec, ideService, gitspaceLogger)
}, true)
e.RegisterStep("Run IDE",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return RunIDE(ctx, exec, ideService, gitspaceLogger)
}, true)
e.RegisterStep("Install Git",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return InstallGit(ctx, exec, e.gitService, gitspaceLogger)
}, true)
e.RegisterStep("Setup Git Credentials",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
if resolvedRepoDetails.ResolvedCredentials.Credentials != nil {
return SetupGitCredentials(ctx, exec, resolvedRepoDetails, e.gitService, gitspaceLogger)
}
return nil
}, true)
e.RegisterStep("Clone Code",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
return CloneCode(ctx, exec, defaultBaseImage, resolvedRepoDetails, e.gitService, gitspaceLogger)
}, true)
// Register the Execute Command steps (PostCreate and PostStart)
e.RegisterStep("Execute PostCreate Command",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
command := ExtractCommand(PostCreateAction, devcontainerConfig)
return ExecuteCommand(ctx, exec, codeRepoDir, gitspaceLogger, command, PostCreateAction)
}, false)
e.RegisterStep("Execute PostStart Command",
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error {
func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error {
command := ExtractCommand(PostStartAction, devcontainerConfig)
return ExecuteCommand(ctx, exec, codeRepoDir, gitspaceLogger, command, PostStartAction)
}, false)
@ -100,27 +105,25 @@ func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE(
func InstallTools(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
ideType enum.IDEType,
) error {
output, err := common.InstallTools(ctx, exec, ideType)
err := common.InstallTools(ctx, exec, ideType, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while installing tools inside container", err)
}
gitspaceLogger.Info("Tools installation output...\n" + string(output))
return nil
}
func ValidateSupportedOS(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
output, err := common.ValidateSupportedOS(ctx, exec)
err := common.ValidateSupportedOS(ctx, exec, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while detecting OS inside container", err)
}
gitspaceLogger.Info("Validate supported OSes...\n" + string(output))
return nil
}
@ -128,7 +131,7 @@ func ExecuteCommand(
ctx context.Context,
exec *devcontainer.Exec,
codeRepoDir string,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
command string,
actionType PostAction,
) error {
@ -137,12 +140,17 @@ func ExecuteCommand(
return nil
}
gitspaceLogger.Info(fmt.Sprintf("Executing %s command: %s", actionType, command))
output, err := exec.ExecuteCommand(ctx, command, true, false, codeRepoDir)
gitspaceLogger.Info(fmt.Sprintf("%s command execution output...", actionType))
// Create a channel to stream command output
outputCh := make(chan []byte)
err := exec.ExecuteCommand(ctx, command, true, false, codeRepoDir, outputCh)
if err != nil {
return logStreamWrapError(
gitspaceLogger, fmt.Sprintf("Error while executing %s command", actionType), err)
}
gitspaceLogger.Info(fmt.Sprintf("%s command execution output...\n %s", actionType, string(output)))
for output := range outputCh {
gitspaceLogger.Info(string(output))
}
gitspaceLogger.Info(fmt.Sprintf("Successfully executed %s command", actionType))
return nil
}
@ -153,13 +161,12 @@ func CloneCode(
defaultBaseImage string,
resolvedRepoDetails scm.ResolvedDetails,
gitService git.Service,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
output, err := gitService.CloneCode(ctx, exec, resolvedRepoDetails, defaultBaseImage)
err := gitService.CloneCode(ctx, exec, resolvedRepoDetails, defaultBaseImage, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while cloning code inside container", err)
}
gitspaceLogger.Info("Clone output...\n" + string(output))
return nil
}
@ -167,14 +174,12 @@ func InstallGit(
ctx context.Context,
exec *devcontainer.Exec,
gitService git.Service,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
output, err := gitService.Install(ctx, exec)
err := gitService.Install(ctx, exec, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while installing git inside container", err)
}
gitspaceLogger.Info("Install git output...\n" + string(output))
return nil
}
@ -183,15 +188,13 @@ func SetupGitCredentials(
exec *devcontainer.Exec,
resolvedRepoDetails scm.ResolvedDetails,
gitService git.Service,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
output, err := gitService.SetupCredentials(ctx, exec, resolvedRepoDetails)
err := gitService.SetupCredentials(ctx, exec, resolvedRepoDetails, gitspaceLogger)
if err != nil {
return logStreamWrapError(
gitspaceLogger, "Error while setting up git credentials inside container", err)
}
gitspaceLogger.Info("Setting up git credentials output...\n" + string(output))
return nil
}
@ -199,13 +202,12 @@ func ManageUser(
ctx context.Context,
exec *devcontainer.Exec,
userService user.Service,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
output, err := userService.Manage(ctx, exec)
err := userService.Manage(ctx, exec, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while creating user inside container", err)
}
gitspaceLogger.Info("Managing user output...\n" + string(output))
return nil
}
@ -213,17 +215,13 @@ func SetupIDE(
ctx context.Context,
exec *devcontainer.Exec,
ideService ide.IDE,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
gitspaceLogger.Info("Setting up IDE inside container: " + string(ideService.Type()))
output, err := ideService.Setup(ctx, exec)
err := ideService.Setup(ctx, exec, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while setting up IDE inside container", err)
}
gitspaceLogger.Info("IDE setup output...\n" + string(output))
gitspaceLogger.Info("Successfully set up IDE inside container")
return nil
}
@ -231,15 +229,27 @@ func RunIDE(
ctx context.Context,
exec *devcontainer.Exec,
ideService ide.IDE,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
gitspaceLogger.Info("Running the IDE inside container: " + string(ideService.Type()))
output, err := ideService.Run(ctx, exec)
err := ideService.Run(ctx, exec, gitspaceLogger)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while running IDE inside container", err)
}
gitspaceLogger.Info("IDE run output...\n" + string(output))
gitspaceLogger.Info("Successfully run the IDE inside container")
return nil
}
func SetEnv(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger,
environment []string,
) error {
if len(environment) > 0 {
err := common.SetEnv(ctx, exec, gitspaceLogger, environment)
if err != nil {
return logStreamWrapError(gitspaceLogger, "Error while installing tools inside container", err)
}
}
return nil
}

View File

@ -23,9 +23,9 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/git"
"github.com/harness/gitness/app/gitspace/orchestrator/ide"
orchestratorTypes "github.com/harness/gitness/app/gitspace/orchestrator/types"
"github.com/harness/gitness/app/gitspace/orchestrator/user"
"github.com/harness/gitness/app/gitspace/scm"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/infraprovider"
"github.com/harness/gitness/types"
@ -41,7 +41,7 @@ const (
)
type EmbeddedDockerOrchestrator struct {
steps []orchestratorTypes.Step // Steps registry
steps []gitspaceTypes.Step // Steps registry
dockerClientFactory *infraprovider.DockerClientFactory
statefulLogger *logutil.StatefulLogger
gitService git.Service
@ -51,10 +51,10 @@ type EmbeddedDockerOrchestrator struct {
// RegisterStep registers a new setup step with an option to stop or continue on failure.
func (e *EmbeddedDockerOrchestrator) RegisterStep(
name string,
execute func(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger orchestratorTypes.GitspaceLogger) error,
execute func(ctx context.Context, exec *devcontainer.Exec, logger gitspaceTypes.GitspaceLogger) error,
stopOnFailure bool,
) {
step := orchestratorTypes.Step{
step := gitspaceTypes.Step{
Name: name,
Execute: execute,
StopOnFailure: stopOnFailure,
@ -66,7 +66,7 @@ func (e *EmbeddedDockerOrchestrator) RegisterStep(
func (e *EmbeddedDockerOrchestrator) ExecuteSteps(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
for _, step := range e.steps {
// Execute the step
@ -378,7 +378,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
infrastructure types.Infrastructure,
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
gitspaceLogger orchestratorTypes.GitspaceLogger,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
homeDir := GetUserHomeDir(gitspaceConfig.GitspaceUser.Identifier)
containerName := GetGitspaceContainerName(gitspaceConfig)
@ -450,6 +450,7 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
gitspaceConfig,
resolvedRepoDetails,
defaultBaseImage,
environment,
); err != nil {
return err
}

View File

@ -15,10 +15,13 @@
package devcontainer
import (
"bytes"
"bufio"
"context"
"fmt"
"io"
"log"
"strings"
"sync"
"github.com/harness/gitness/types/enum"
@ -28,6 +31,7 @@ import (
)
const RootUser = "root"
const ErrMsgTCP = "unable to upgrade to tcp, received 200"
type Exec struct {
ContainerName string
@ -39,8 +43,8 @@ type Exec struct {
}
type execResult struct {
StdOut []byte
StdErr []byte
StdOut io.Reader
StdErr io.Reader
ExitCode int
}
@ -50,49 +54,48 @@ func (e *Exec) ExecuteCommand(
root bool,
detach bool,
workingDir string,
) ([]byte, error) {
outputCh chan []byte, // channel to stream output as []byte
) error {
user := e.UserIdentifier
if root {
user = RootUser
}
cmd := []string{"/bin/sh", "-c", command}
execConfig := container.ExecOptions{
User: user,
AttachStdout: true,
AttachStderr: true,
AttachStdout: !detach,
AttachStderr: !detach,
Cmd: cmd,
Detach: detach,
WorkingDir: workingDir,
}
execID, err := e.DockerClient.ContainerExecCreate(ctx, e.ContainerName, execConfig)
// Create exec instance for the container
containerExecCreate, err := e.DockerClient.ContainerExecCreate(ctx, e.ContainerName, execConfig)
if err != nil {
return nil, fmt.Errorf("failed to create docker exec for container %s: %w", e.ContainerName, err)
return fmt.Errorf("failed to create docker exec for container %s: %w", e.ContainerName, err)
}
resp, err := e.attachAndInspectExec(ctx, execID.ID, detach)
if err != nil && err.Error() != "unable to upgrade to tcp, received 200" {
return nil, fmt.Errorf("failed to start docker exec for container %s: %w", e.ContainerName, err)
// Attach and inspect exec session to get the output
inspectExec, err := e.attachAndInspectExec(ctx, containerExecCreate.ID, detach)
if err != nil && !strings.Contains(err.Error(), ErrMsgTCP) {
return fmt.Errorf("failed to start docker exec for container %s: %w", e.ContainerName, err)
}
// If in detach mode, exit early as the command will run in the background
if detach {
close(outputCh)
return nil
}
if resp != nil && resp.ExitCode != 0 {
var errLog string
if resp.StdErr != nil {
errLog = string(resp.StdErr)
}
return nil, fmt.Errorf("error during command execution in container %s. exit code %d. log: %s",
e.ContainerName, resp.ExitCode, errLog)
// Wait for the exit code after the command completes
if inspectExec != nil && inspectExec.ExitCode != 0 {
return fmt.Errorf("error during command execution in container %s. exit code %d",
e.ContainerName, inspectExec.ExitCode)
}
var stdOutput []byte
if resp != nil {
stdOutput = resp.StdOut
}
return stdOutput, nil
e.streamResponse(inspectExec, outputCh)
return nil
}
func (e *Exec) ExecuteCommandInHomeDirectory(
@ -100,55 +103,91 @@ func (e *Exec) ExecuteCommandInHomeDirectory(
command string,
root bool,
detach bool,
) ([]byte, error) {
return e.ExecuteCommand(ctx, command, root, detach, e.HomeDir)
outputCh chan []byte, // channel to stream output as []byte
) error {
return e.ExecuteCommand(ctx, command, root, detach, e.HomeDir, outputCh)
}
func (e *Exec) attachAndInspectExec(ctx context.Context, id string, detach bool) (*execResult, error) {
resp, attachErr := e.DockerClient.ContainerExecAttach(ctx, id, container.ExecStartOptions{Detach: detach})
if attachErr != nil {
return nil, attachErr
}
defer resp.Close()
var outBuf, errBuf bytes.Buffer
copyErr := make(chan error)
go func() {
// StdCopy demultiplexes the stream into two buffers
_, err := stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
copyErr <- err
}()
select {
case err := <-copyErr:
if err != nil {
return nil, err
}
break
case <-ctx.Done():
return nil, ctx.Err()
return nil, fmt.Errorf("failed to attach to exec session: %w", attachErr)
}
stdout, err := io.ReadAll(&outBuf)
if err != nil {
return nil, fmt.Errorf("failed to read stdout of exec for container %s: %w", e.ContainerName, err)
// If in detach mode, we just need to close the connection, not process output
if detach {
// No need to process output in detach mode, so we simply close the connection
resp.Close()
return nil, nil //nolint:nilnil
}
stderr, err := io.ReadAll(&errBuf)
if err != nil {
return nil, fmt.Errorf("failed to read stderr of exec for container %s: %w", e.ContainerName, err)
}
// Create pipes for stdout and stderr
stdoutPipe, stdoutWriter := io.Pipe()
stderrPipe, stderrWriter := io.Pipe()
inspectRes, err := e.DockerClient.ContainerExecInspect(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to inspect exec for container %s: %w", e.ContainerName, err)
}
go e.copyOutput(resp.Reader, stdoutWriter, stderrWriter)
// Return the output streams and the response
return &execResult{
StdOut: stdout,
StdErr: stderr,
ExitCode: inspectRes.ExitCode,
StdOut: stdoutPipe, // Pipe for stdout
StdErr: stderrPipe, // Pipe for stderr
}, nil
}
func (e *Exec) streamResponse(resp *execResult, outputCh chan []byte) {
// Stream the output asynchronously if not in detach mode
go func() {
if resp != nil {
var wg sync.WaitGroup
// Handle stdout as a streaming reader
if resp.StdOut != nil {
wg.Add(1)
go e.streamStdOut(resp.StdOut, outputCh, &wg)
}
// Handle stderr as a streaming reader
if resp.StdErr != nil {
wg.Add(1)
go e.streamStdErr(resp.StdErr, outputCh, &wg)
}
// Wait for all readers to finish before closing the channel
wg.Wait()
// Close the output channel after all output has been processed
close(outputCh)
}
}()
}
// copyOutput copies the output from the exec response to the pipes, and is blocking.
func (e *Exec) copyOutput(reader io.Reader, stdoutWriter, stderrWriter io.WriteCloser) {
_, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, reader)
if err != nil {
log.Printf("Error copying output: %v", err)
}
stdoutWriter.Close()
stderrWriter.Close()
}
// streamStdOut reads from the stdout pipe and sends each line to the output channel.
func (e *Exec) streamStdOut(stdout io.Reader, outputCh chan []byte, wg *sync.WaitGroup) {
defer wg.Done()
stdoutReader := bufio.NewScanner(stdout)
for stdoutReader.Scan() {
outputCh <- stdoutReader.Bytes()
}
if err := stdoutReader.Err(); err != nil {
log.Println("Error reading stdout:", err)
}
}
// streamStdErr reads from the stderr pipe and sends each line to the output channel.
func (e *Exec) streamStdErr(stderr io.Reader, outputCh chan []byte, wg *sync.WaitGroup) {
defer wg.Done()
stderrReader := bufio.NewScanner(stderr)
for stderrReader.Scan() {
outputCh <- []byte("ERR> " + stderrReader.Text())
}
if err := stderrReader.Err(); err != nil {
log.Println("Error reading stderr:", err)
}
}

View File

@ -19,18 +19,23 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/gitspace/types"
)
type Service interface {
// Install ensures git is installed in the container.
Install(ctx context.Context, exec *devcontainer.Exec) ([]byte, error)
Install(ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
) error
// SetupCredentials sets the user's git credentials inside the container.
SetupCredentials(
ctx context.Context,
exec *devcontainer.Exec,
resolvedRepoDetails scm.ResolvedDetails,
) ([]byte, error)
gitspaceLogger types.GitspaceLogger,
) error
// CloneCode clones the code and ensures devcontainer file is present.
CloneCode(
@ -38,5 +43,6 @@ type Service interface {
exec *devcontainer.Exec,
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
) ([]byte, error)
gitspaceLogger types.GitspaceLogger,
) error
}

View File

@ -23,6 +23,7 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/template"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/gitspace/types"
)
var _ Service = (*ServiceImpl)(nil)
@ -38,50 +39,52 @@ func NewGitServiceImpl() Service {
return &ServiceImpl{}
}
func (g *ServiceImpl) Install(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
func (g *ServiceImpl) Install(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger types.GitspaceLogger,
) error {
script, err := template.GenerateScriptFromTemplate(
templateGitInstallScript, &template.SetupGitInstallPayload{
OSInfoScript: common.GetOSInfoScript(),
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to setup git install from template %s: %w", templateGitInstallScript, err)
}
output := "Setting up git inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, true, false)
gitspaceLogger.Info("Install git output...")
gitspaceLogger.Info("Setting up git inside container")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to setup git: %w", err)
return fmt.Errorf("failed to setup git: %w", err)
}
gitspaceLogger.Info("Successfully setup git")
output += "Successfully setup git\n"
return []byte(output), nil
return nil
}
func (g *ServiceImpl) SetupCredentials(
ctx context.Context,
exec *devcontainer.Exec,
resolvedRepoDetails scm.ResolvedDetails,
) ([]byte, error) {
gitspaceLogger types.GitspaceLogger,
) error {
script, err := template.GenerateScriptFromTemplate(
templateSetupGitCredentials, &template.SetupGitCredentialsPayload{
CloneURLWithCreds: resolvedRepoDetails.CloneURL,
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to setup git credentials from template %s: %w", templateSetupGitCredentials, err)
}
output := "Setting up git credentials inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, false, false)
gitspaceLogger.Info("Setting up git credentials output...")
gitspaceLogger.Info("Setting up git credentials inside container")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, script, false, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to setup git credentials: %w", err)
return fmt.Errorf("failed to setup git credentials: %w", err)
}
output += "Successfully setup git credentials\n"
return []byte(output), nil
gitspaceLogger.Info("Successfully setup git credentials")
return nil
}
func (g *ServiceImpl) CloneCode(
@ -89,10 +92,11 @@ func (g *ServiceImpl) CloneCode(
exec *devcontainer.Exec,
resolvedRepoDetails scm.ResolvedDetails,
defaultBaseImage string,
) ([]byte, error) {
gitspaceLogger types.GitspaceLogger,
) error {
cloneURL, err := url.Parse(resolvedRepoDetails.CloneURL)
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to parse clone url %s: %w", resolvedRepoDetails.CloneURL, err)
}
cloneURL.User = nil
@ -109,18 +113,16 @@ func (g *ServiceImpl) CloneCode(
script, err := template.GenerateScriptFromTemplate(
templateCloneCode, data)
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to clone code from template %s: %w", templateCloneCode, err)
}
output := "Cloning code inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, false, false)
gitspaceLogger.Info("Clone output...")
gitspaceLogger.Info("Cloning code inside container")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, script, false, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to clone code: %w", err)
return fmt.Errorf("failed to clone code: %w", err)
}
gitspaceLogger.Info("Successfully clone code")
output += "Successfully clone code\n"
return []byte(output), nil
return nil
}

View File

@ -18,6 +18,7 @@ import (
"context"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
@ -25,10 +26,10 @@ import (
type IDE interface {
// Setup is responsible for doing all the operations for setting up the IDE in the container e.g. installation,
// copying settings and configurations.
Setup(ctx context.Context, exec *devcontainer.Exec) ([]byte, error)
Setup(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error
// Run runs the IDE and supporting services.
Run(ctx context.Context, exec *devcontainer.Exec) ([]byte, error)
Run(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error
// Port provides the port which will be used by this IDE.
Port() *types.GitspacePort

View File

@ -22,6 +22,7 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/common"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/template"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
@ -47,7 +48,8 @@ func NewVsCodeService(config *VSCodeConfig) *VSCode {
func (v *VSCode) Setup(
ctx context.Context,
exec *devcontainer.Exec,
) ([]byte, error) {
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
osInfoScript := common.GetOSInfoScript()
sshServerScript, err := template.GenerateScriptFromTemplate(
templateSetupSSHServer, &template.SetupSSHServerPayload{
@ -56,43 +58,43 @@ func (v *VSCode) Setup(
OSInfoScript: osInfoScript,
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to setup ssh server from template %s: %w", templateSetupSSHServer, err)
}
output := "Installing ssh-server inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, sshServerScript, true, false)
gitspaceLogger.Info("Installing ssh-server inside container")
gitspaceLogger.Info("IDE setup output...")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, sshServerScript, true, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to setup SSH serverr: %w", err)
return fmt.Errorf("failed to setup SSH serverr: %w", err)
}
output += "Successfully installed ssh-server\n"
return []byte(output), nil
gitspaceLogger.Info("Successfully installed ssh-server")
gitspaceLogger.Info("Successfully set up IDE inside container")
return nil
}
// Run runs the SSH server inside the container.
func (v *VSCode) Run(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
var output = ""
func (v *VSCode) Run(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
runSSHScript, err := template.GenerateScriptFromTemplate(
templateRunSSHServer, &template.RunSSHServerPayload{
Port: strconv.Itoa(v.config.Port),
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to run ssh server from template %s: %w", templateRunSSHServer, err)
}
execOutput, err := exec.ExecuteCommandInHomeDirectory(ctx, runSSHScript, true, false)
gitspaceLogger.Info("SSH server run output...")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, runSSHScript, true, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to run SSH server: %w", err)
return fmt.Errorf("failed to run SSH server: %w", err)
}
gitspaceLogger.Info("Successfully run ssh-server")
output += "SSH server run output...\n" + string(execOutput) + "\nSuccessfully run ssh-server\n"
return []byte(output), nil
return nil
}
// Port returns the port on which the ssh-server is listening.

View File

@ -27,6 +27,7 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/template"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
@ -61,58 +62,75 @@ func NewVsCodeWebService(config *VSCodeWebConfig) *VSCodeWeb {
}
// Setup runs the installScript which downloads the required version of the code-server binary.
func (v *VSCodeWeb) Setup(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
output := "Installing VSCode Web inside container.\n"
_, err := exec.ExecuteCommandInHomeDirectory(ctx, installScript, true, false)
func (v *VSCodeWeb) Setup(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
gitspaceLogger.Info("Installing VSCode Web inside container.")
gitspaceLogger.Info("IDE setup output...")
outputCh := make(chan []byte)
err := exec.ExecuteCommandInHomeDirectory(ctx, installScript, true, false, outputCh)
if err != nil {
return nil, fmt.Errorf("failed to install VSCode Web: %w", err)
return fmt.Errorf("failed to install VSCode Web: %w", err)
}
for chunk := range outputCh {
_, err := io.Discard.Write(chunk)
if err != nil {
return err
}
}
findOutput, err := exec.ExecuteCommandInHomeDirectory(ctx, findPathScript, true, false)
if err != nil {
return nil, fmt.Errorf("failed to find VSCode Web install path: %w", err)
findCh := make(chan []byte)
err = exec.ExecuteCommandInHomeDirectory(ctx, findPathScript, true, false, findCh)
var findOutput []byte
for chunk := range findCh {
findOutput = append(findOutput, chunk...) // Concatenate each chunk of data
}
if err != nil {
return fmt.Errorf("failed to find VSCode Web install path: %w", err)
}
path := string(findOutput)
startIndex := strings.Index(path, startMarker)
endIndex := strings.Index(path, endMarker)
if startIndex == -1 || endIndex == -1 || startIndex >= endIndex {
return nil, fmt.Errorf("could not find media folder path from find output: %s", path)
return fmt.Errorf("could not find media folder path from find output: %s", path)
}
mediaFolderPath := path[startIndex+len(startMarker) : endIndex]
err = v.copyMediaToContainer(ctx, exec, mediaFolderPath)
if err != nil {
return nil, fmt.Errorf("failed to copy media folder to container at path %s: %w", mediaFolderPath, err)
return fmt.Errorf("failed to copy media folder to container at path %s: %w", mediaFolderPath, err)
}
output += "Successfully installed VSCode Web inside container.\n"
return []byte(output), nil
gitspaceLogger.Info("Successfully set up IDE inside container")
return nil
}
// Run runs the code-server binary.
func (v *VSCodeWeb) Run(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
var output []byte
func (v *VSCodeWeb) Run(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger) error {
runScript, err := template.GenerateScriptFromTemplate(
templateRunVSCodeWeb, &template.RunVSCodeWebPayload{
Port: strconv.Itoa(v.config.Port),
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to run VSCode Web from template %s: %w",
templateRunVSCodeWeb,
err,
)
}
_, err = exec.ExecuteCommandInHomeDirectory(ctx, runScript, false, true)
gitspaceLogger.Info("Starting IDE ...")
outputCh := make(chan []byte)
err = exec.ExecuteCommandInHomeDirectory(ctx, runScript, false, false, outputCh)
if err != nil {
return nil, fmt.Errorf("failed to run VSCode Web: %w", err)
return fmt.Errorf("failed to run VSCode Web: %w", err)
}
return output, nil
return nil
}
// PortAndProtocol returns the port on which the code-server is listening.

View File

@ -82,6 +82,10 @@ type SupportedOSDistributionPayload struct {
OSInfoScript string
}
type SetEnvPayload struct {
EnvVariables string
}
func init() {
err := LoadTemplates()
if err != nil {

View File

@ -0,0 +1,29 @@
#!/bin/sh
VARIABLES={{ .EnvVariables }}
# Check if the script is run as root, as modifying /etc/profile requires root privileges
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root."
exit 1
fi
# Path to /etc/profile
PROFILE_FILE="/etc/profile"
# Process each line in the VARIABLES string
echo "$VARIABLES" | while IFS= read -r line; do
# Skip empty lines
[[ -z "$line" ]] && continue
# Extract the variable name and value
var_name="${line%%=*}" # Part before '='
var_value="${line#*=}" # Part after '='
# Create the export statement
export_statement="export $var_name=$var_value"
# Check if the variable is already present in /etc/profile
if ! grep -q "^export $var_name=" "$PROFILE_FILE"; then
echo "$export_statement" >> "$PROFILE_FILE"
echo "Added $export_statement to $PROFILE_FILE"
else
echo "$var_name is already present in $PROFILE_FILE"
fi
done

View File

@ -18,9 +18,10 @@ import (
"context"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/types"
)
type Service interface {
// Manage manager the linux user in the container.
Manage(ctx context.Context, exec *devcontainer.Exec) ([]byte, error)
Manage(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger types.GitspaceLogger) error
}

View File

@ -21,6 +21,7 @@ import (
"github.com/harness/gitness/app/gitspace/orchestrator/common"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
"github.com/harness/gitness/app/gitspace/orchestrator/template"
gitspaceTypes "github.com/harness/gitness/app/gitspace/types"
)
var _ Service = (*ServiceImpl)(nil)
@ -34,7 +35,11 @@ func NewUserServiceImpl() Service {
return &ServiceImpl{}
}
func (u *ServiceImpl) Manage(ctx context.Context, exec *devcontainer.Exec) ([]byte, error) {
func (u *ServiceImpl) Manage(
ctx context.Context,
exec *devcontainer.Exec,
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
osInfoScript := common.GetOSInfoScript()
script, err := template.GenerateScriptFromTemplate(
templateManagerUser, &template.SetupUserPayload{
@ -45,17 +50,18 @@ func (u *ServiceImpl) Manage(ctx context.Context, exec *devcontainer.Exec) ([]by
OSInfoScript: osInfoScript,
})
if err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to generate scipt to manager user from template %s: %w", templateManagerUser, err)
}
output := "Setting up user inside container\n"
_, err = exec.ExecuteCommandInHomeDirectory(ctx, script, true, false)
gitspaceLogger.Info("Setting up user inside container")
gitspaceLogger.Info("Managing user output...")
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, script, true, gitspaceLogger)
if err != nil {
return nil, fmt.Errorf("failed to setup user: %w", err)
return fmt.Errorf("failed to setup user: %w", err)
}
output += "Successfully setup user\n"
gitspaceLogger.Info("Successfully setup user")
return []byte(output), nil
return nil
}

View File

@ -14,7 +14,9 @@
package types
import "github.com/rs/zerolog"
import (
"github.com/rs/zerolog"
)
// NewZerologAdapter creates a new adapter from a zerolog.Logger.
func NewZerologAdapter(logger *zerolog.Logger) *ZerologAdapter {