feat: [CDE-472]: add status code to the channel for gitspace exec (#3096)

* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
* feat: [CDE-472]: add status code to the channel
pull/3597/head
Ansuman Satapathy 2024-12-03 04:49:05 +00:00 committed by Harness
parent 384fb7a7d2
commit 74ff87178f
8 changed files with 207 additions and 109 deletions

View File

@ -17,6 +17,7 @@ package common
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/harness/gitness/app/gitspace/orchestrator/devcontainer"
@ -156,16 +157,50 @@ func ExecuteCommandInHomeDirAndLog(
gitspaceLogger types.GitspaceLogger,
verbose bool,
) error {
outputCh := make(chan []byte)
err := exec.ExecuteCommandInHomeDirectory(ctx, script, root, false, outputCh)
for output := range outputCh {
msg := string(output)
// Log output from the command as a string
if len(output) > 0 {
if verbose || strings.HasPrefix(msg, devcontainer.LoggerErrorPrefix) {
gitspaceLogger.Info(msg)
// Buffer upto a thousand messages
outputCh := make(chan []byte, 1000)
err := exec.ExecuteCmdInHomeDirectoryAsyncStream(ctx, script, root, false, outputCh)
if err != nil {
return err
}
// Use select to wait for the output and exit status
for {
select {
case output := <-outputCh:
done, chErr := handleOutputChannel(output, verbose, gitspaceLogger)
if done {
return chErr
}
case <-ctx.Done():
// Handle context cancellation or timeout
return ctx.Err()
}
}
return err
}
func handleOutputChannel(output []byte, verbose bool, gitspaceLogger types.GitspaceLogger) (bool, error) {
// Handle the exit status first
if strings.HasPrefix(string(output), devcontainer.ChannelExitStatus) {
// Extract the exit code from the message
exitCodeStr := strings.TrimPrefix(string(output), devcontainer.ChannelExitStatus)
exitCode, err := strconv.Atoi(exitCodeStr)
if err != nil {
return true, fmt.Errorf("invalid exit status format: %w", err)
}
if exitCode != 0 {
gitspaceLogger.Info("Process Exited with status " + exitCodeStr)
return true, fmt.Errorf("command exited with non-zero status: %d", exitCode)
}
// If exit status is zero, just continue processing
return true, nil
}
// Handle regular command output
msg := string(output)
if len(output) > 0 {
// Log output if verbose or if it's an error
if verbose || strings.HasPrefix(msg, devcontainer.LoggerErrorPrefix) {
gitspaceLogger.Info(msg)
}
}
return false, nil
}

View File

@ -53,9 +53,9 @@ func ValidateSupportedOS(
return nil
}
func ExecuteCommands(
func ExecuteLifecycleCommands(
ctx context.Context,
exec *devcontainer.Exec,
exec devcontainer.Exec,
codeRepoDir string,
gitspaceLogger gitspaceTypes.GitspaceLogger,
commands []string,
@ -69,18 +69,12 @@ func ExecuteCommands(
gitspaceLogger.Info(fmt.Sprintf("Executing %s command: %s", actionType, command))
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, false, false, codeRepoDir, outputCh)
exec.DefaultWorkingDir = codeRepoDir
err := common.ExecuteCommandInHomeDirAndLog(ctx, &exec, command, false, gitspaceLogger, true)
if err != nil {
return logStreamWrapError(
gitspaceLogger, fmt.Sprintf("Error while executing %s command: %s", actionType, command), err)
}
for output := range outputCh {
gitspaceLogger.Info(string(output))
}
gitspaceLogger.Info(fmt.Sprintf("Completed execution %s command: %s", actionType, command))
}

View File

@ -1,15 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container

View File

@ -197,12 +197,12 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
codeRepoDir := filepath.Join(homeDir, resolvedRepoDetails.RepoName)
exec := &devcontainer.Exec{
ContainerName: containerName,
DockerClient: dockerClient,
HomeDir: homeDir,
RemoteUser: remoteUser,
AccessKey: accessKey,
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
ContainerName: containerName,
DockerClient: dockerClient,
DefaultWorkingDir: homeDir,
RemoteUser: remoteUser,
AccessKey: accessKey,
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
}
// Set up git credentials if needed
@ -220,7 +220,7 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace(
// Execute post-start command
devcontainerConfig := resolvedRepoDetails.DevcontainerConfig
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
startErr = ExecuteCommands(ctx, exec, codeRepoDir, logStreamInstance, command, PostStartAction)
startErr = ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, logStreamInstance, command, PostStartAction)
if startErr != nil {
log.Warn().Msgf("Error is post-start command, continuing : %s", startErr.Error())
}
@ -454,12 +454,12 @@ func (e *EmbeddedDockerOrchestrator) runGitspaceSetupSteps(
// Setup and run commands
exec := &devcontainer.Exec{
ContainerName: containerName,
DockerClient: dockerClient,
HomeDir: homeDir,
RemoteUser: remoteUser,
AccessKey: *gitspaceConfig.GitspaceInstance.AccessKey,
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
ContainerName: containerName,
DockerClient: dockerClient,
DefaultWorkingDir: homeDir,
RemoteUser: remoteUser,
AccessKey: *gitspaceConfig.GitspaceInstance.AccessKey,
AccessType: gitspaceConfig.GitspaceInstance.AccessType,
}
if err := e.setupGitspaceAndIDE(
@ -598,7 +598,7 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
command := ExtractLifecycleCommands(PostCreateAction, devcontainerConfig)
return ExecuteCommands(ctx, exec, codeRepoDir, gitspaceLogger, command, PostCreateAction)
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger, command, PostCreateAction)
},
StopOnFailure: false,
},
@ -610,7 +610,7 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps(
gitspaceLogger gitspaceTypes.GitspaceLogger,
) error {
command := ExtractLifecycleCommands(PostStartAction, devcontainerConfig)
return ExecuteCommands(ctx, exec, codeRepoDir, gitspaceLogger, command, PostStartAction)
return ExecuteLifecycleCommands(ctx, *exec, codeRepoDir, gitspaceLogger, command, PostStartAction)
},
StopOnFailure: false,
},

View File

@ -16,47 +16,93 @@ package devcontainer
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"strings"
"sync"
"github.com/harness/gitness/types/enum"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/rs/zerolog/log"
)
const RootUser = "root"
const ErrMsgTCP = "unable to upgrade to tcp, received 200"
const LoggerErrorPrefix = "ERR>> "
const ChannelExitStatus = "DEVCONTAINER_EXIT_STATUS"
type Exec struct {
ContainerName string
DockerClient *client.Client
HomeDir string
RemoteUser string
AccessKey string
AccessType enum.GitspaceAccessType
ContainerName string
DockerClient *client.Client
DefaultWorkingDir string
RemoteUser string
AccessKey string
AccessType enum.GitspaceAccessType
}
type execResult struct {
StdOut io.Reader
StdErr io.Reader
ExitCode int
ExecID string
StdOut io.Reader
StdErr io.Reader
}
func (e *Exec) ExecuteCommand(
ctx context.Context,
command string,
root bool,
detach bool,
workingDir string,
outputCh chan []byte, // channel to stream output as []byte
) error {
) (string, error) {
containerExecCreate, err := e.createExecution(ctx, command, root, workingDir, false)
if err != nil {
return "", err
}
// Attach and inspect exec session to get the output
inspectExec, err := e.attachAndInspectExec(ctx, containerExecCreate.ID, false)
if err != nil {
return "", fmt.Errorf("failed to start docker exec for container %s: %w", e.ContainerName, err)
}
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
stdoutData, err := io.ReadAll(inspectExec.StdOut)
if err != nil {
return "", fmt.Errorf("error reading stdout: %w", err)
}
stdoutBuf.Write(stdoutData)
stderrData, err := io.ReadAll(inspectExec.StdErr)
if err != nil {
return "", fmt.Errorf("error reading stderr: %w", err)
}
stderrBuf.Write(stderrData)
inspect, err := e.DockerClient.ContainerExecInspect(ctx, containerExecCreate.ID)
if err != nil {
return "", fmt.Errorf("failed to inspect exec session: %w", err)
}
// If the exit code is non-zero, return both stdout and stderr
if inspect.ExitCode != 0 {
// Combine stdout and stderr
return fmt.Sprintf(
"STDOUT:\n%s\nSTDERR:\n%s", stdoutBuf.String(), stderrBuf.String()),
fmt.Errorf("command exited with non-zero status: %d", inspect.ExitCode)
}
// If the exit code is zero, only return stdout
return stdoutBuf.String(), nil
}
func (e *Exec) createExecution(
ctx context.Context,
command string,
root bool,
workingDir string,
detach bool,
) (*dockerTypes.IDResponse, error) {
user := e.RemoteUser
if root {
user = RootUser
@ -73,11 +119,26 @@ func (e *Exec) ExecuteCommand(
}
// Create exec instance for the container
log.Debug().Msgf("Creating execution for container %s", e.ContainerName)
containerExecCreate, err := e.DockerClient.ContainerExecCreate(ctx, e.ContainerName, execConfig)
if err != nil {
return fmt.Errorf("failed to create docker exec for container %s: %w", e.ContainerName, err)
return nil, fmt.Errorf("failed to create docker exec for container %s: %w", e.ContainerName, err)
}
return &containerExecCreate, nil
}
func (e *Exec) executeCmdAsyncStream(
ctx context.Context,
command string,
root bool,
detach bool,
workingDir string,
outputCh chan []byte, // channel to stream output as []byte
) error {
containerExecCreate, err := e.createExecution(ctx, command, root, workingDir, detach)
if err != nil {
return 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) {
@ -88,25 +149,18 @@ func (e *Exec) ExecuteCommand(
close(outputCh)
return nil
}
// 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)
}
e.streamResponse(inspectExec, outputCh)
return nil
}
func (e *Exec) ExecuteCommandInHomeDirectory(
func (e *Exec) ExecuteCmdInHomeDirectoryAsyncStream(
ctx context.Context,
command string,
root bool,
detach bool,
outputCh chan []byte, // channel to stream output as []byte
) error {
return e.ExecuteCommand(ctx, command, root, detach, e.HomeDir, outputCh)
return e.executeCmdAsyncStream(ctx, command, root, detach, e.DefaultWorkingDir, outputCh)
}
func (e *Exec) attachAndInspectExec(ctx context.Context, id string, detach bool) (*execResult, error) {
@ -117,7 +171,6 @@ func (e *Exec) attachAndInspectExec(ctx context.Context, id string, detach bool)
// 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
}
@ -130,6 +183,7 @@ func (e *Exec) attachAndInspectExec(ctx context.Context, id string, detach bool)
// Return the output streams and the response
return &execResult{
ExecID: id,
StdOut: stdoutPipe, // Pipe for stdout
StdErr: stderrPipe, // Pipe for stderr
}, nil
@ -138,6 +192,7 @@ func (e *Exec) attachAndInspectExec(ctx context.Context, id string, detach bool)
func (e *Exec) streamResponse(resp *execResult, outputCh chan []byte) {
// Stream the output asynchronously if not in detach mode
go func() {
defer close(outputCh)
if resp != nil {
var wg sync.WaitGroup
@ -153,20 +208,31 @@ func (e *Exec) streamResponse(resp *execResult, outputCh chan []byte) {
}
// Wait for all readers to finish before closing the channel
wg.Wait()
// Close the output channel after all output has been processed
close(outputCh)
// Now that streaming is finished, inspect the exit status
log.Debug().Msgf("Inspecting container for status: %s", resp.ExecID)
inspect, err := e.DockerClient.ContainerExecInspect(context.Background(), resp.ExecID)
if err != nil {
log.Error().Err(err).Msgf("Failed to inspect exec session: %s", err.Error())
return
}
// Send the exit status as a final message
exitStatusMsg := fmt.Sprintf(ChannelExitStatus+"%d", inspect.ExitCode)
outputCh <- []byte(exitStatusMsg)
}
}()
}
// 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) {
defer func() {
stdoutWriter.Close()
stderrWriter.Close()
}()
_, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, reader)
if err != nil {
log.Printf("Error copying output: %v", err)
log.Error().Err(err).Msg("Error in stdcopy.StdCopy " + err.Error())
}
stdoutWriter.Close()
stderrWriter.Close()
}
// streamStdOut reads from the stdout pipe and sends each line to the output channel.
@ -174,10 +240,16 @@ func (e *Exec) streamStdOut(stdout io.Reader, outputCh chan []byte, wg *sync.Wai
defer wg.Done()
stdoutReader := bufio.NewScanner(stdout)
for stdoutReader.Scan() {
outputCh <- stdoutReader.Bytes()
select {
case <-context.Background().Done():
log.Info().Msg("Context canceled, stopping stdout streaming")
return
default:
outputCh <- stdoutReader.Bytes()
}
}
if err := stdoutReader.Err(); err != nil {
log.Println("Error reading stdout:", err)
log.Error().Err(err).Msg("Error reading stdout " + err.Error())
}
}
@ -186,9 +258,15 @@ func (e *Exec) streamStdErr(stderr io.Reader, outputCh chan []byte, wg *sync.Wai
defer wg.Done()
stderrReader := bufio.NewScanner(stderr)
for stderrReader.Scan() {
outputCh <- []byte(LoggerErrorPrefix + stderrReader.Text())
select {
case <-context.Background().Done():
log.Info().Msg("Context canceled, stopping stderr streaming")
return
default:
outputCh <- []byte(LoggerErrorPrefix + stderrReader.Text())
}
}
if err := stderrReader.Err(); err != nil {
log.Println("Error reading stderr:", err)
log.Error().Err(err).Msg("Error reading stderr " + err.Error())
}
}

View File

@ -25,6 +25,7 @@ import (
"strconv"
"strings"
"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"
@ -81,18 +82,10 @@ func (v *VSCodeWeb) Setup(
err,
)
}
outputCh := make(chan []byte)
err = exec.ExecuteCommandInHomeDirectory(ctx, setupScript, false, false, outputCh)
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, setupScript, false, gitspaceLogger, false)
if err != nil {
return fmt.Errorf("failed to install VSCode Web: %w", err)
}
for chunk := range outputCh {
_, err := io.Discard.Write(chunk)
if err != nil {
return err
}
}
if err = v.updateMediaContent(ctx, exec); err != nil {
return err
}
@ -101,18 +94,11 @@ func (v *VSCodeWeb) Setup(
}
func (v *VSCodeWeb) updateMediaContent(ctx context.Context, exec *devcontainer.Exec) error {
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
}
findOutput, err := exec.ExecuteCommand(ctx, findPathScript, true, exec.DefaultWorkingDir)
if err != nil {
return fmt.Errorf("failed to find VSCode Web install path: %w", err)
}
path := string(findOutput)
path := findOutput
startIndex := strings.Index(path, startMarker)
endIndex := strings.Index(path, endMarker)
if startIndex == -1 || endIndex == -1 || startIndex >= endIndex {
@ -153,10 +139,8 @@ func (v *VSCodeWeb) Run(
)
}
gitspaceLogger.Info("Starting IDE ...")
outputCh := make(chan []byte)
// Execute the script in the home directory
err = exec.ExecuteCommandInHomeDirectory(ctx, runScript, false, false, outputCh)
err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, runScript, false, gitspaceLogger, true)
if err != nil {
return fmt.Errorf("failed to run VSCode Web: %w", err)
}

View File

@ -7,10 +7,11 @@ port={{ .Port }}
proxyuri="{{ .ProxyURI }}"
# Ensure the configuration directory exists
mkdir -p $HOME/.config/code-server
config_dir="$HOME/.config/code-server"
mkdir -p "$config_dir"
# Create or overwrite the config file with new settings
cat > $HOME/.config/code-server/config.yaml <<EOF
cat > "$config_dir/config.yaml" <<EOF
bind-addr: 0.0.0.0:$port
auth: none
cert: false
@ -22,5 +23,26 @@ if [ -n "$proxyuri" ]; then
echo "Exported VSCODE_PROXY_URI: $proxyuri"
fi
# Run code-server with templated arguments
eval "code-server --disable-workspace-trust"
# Start code-server in the background
nohup code-server --disable-workspace-trust > "$HOME/code-server.log" 2>&1 &
code_server_pid=$!
# Wait for the code-server IPC socket file to exist
echo "Waiting for vscode web to start..."
while true; do
# Check if the process is still running
if ! kill -0 "$code_server_pid" 2>/dev/null; then
echo "Error: code-server process has stopped unexpectedly."
exit 1
fi
# Check for the IPC socket
ipc_socket=$(find "$HOME/.local/" -type s -name "code-server-ipc.sock" 2>/dev/null)
if [ -n "$ipc_socket" ]; then
echo "vscode web is now running and ready."
break
fi
sleep 3
done
exit 0

View File

@ -45,7 +45,7 @@ func (u *ServiceImpl) Manage(
Username: exec.RemoteUser,
AccessKey: exec.AccessKey,
AccessType: exec.AccessType,
HomeDir: exec.HomeDir,
HomeDir: exec.DefaultWorkingDir,
})
if err != nil {
return fmt.Errorf(