drone/app/gitspace/orchestrator/devcontainer/exec.go

155 lines
3.7 KiB
Go

// 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 devcontainer
import (
"bytes"
"context"
"fmt"
"io"
"github.com/harness/gitness/types/enum"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
)
const RootUser = "root"
type Exec struct {
ContainerName string
DockerClient *client.Client
HomeDir string
UserIdentifier string
AccessKey string
AccessType enum.GitspaceAccessType
}
type execResult struct {
StdOut []byte
StdErr []byte
ExitCode int
}
func (e *Exec) ExecuteCommand(
ctx context.Context,
command string,
root bool,
detach bool,
workingDir string,
) ([]byte, error) {
user := e.UserIdentifier
if root {
user = RootUser
}
cmd := []string{"/bin/sh", "-c", command}
execConfig := container.ExecOptions{
User: user,
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
Detach: detach,
WorkingDir: workingDir,
}
execID, 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)
}
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)
}
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)
}
var stdOutput []byte
if resp != nil {
stdOutput = resp.StdOut
}
return stdOutput, nil
}
func (e *Exec) ExecuteCommandInHomeDirectory(
ctx context.Context,
command string,
root bool,
detach bool,
) ([]byte, error) {
return e.ExecuteCommand(ctx, command, root, detach, e.HomeDir)
}
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()
}
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)
}
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)
}
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)
}
return &execResult{
StdOut: stdout,
StdErr: stderr,
ExitCode: inspectRes.ExitCode,
}, nil
}