mirror of
https://github.com/harness/drone.git
synced 2025-05-05 15:32:56 +00:00
* Formatting. * fix: [CDE-150]: In logstream, adding locking and panic recovery around a subscriber's publish method. Adding check in the stream's publish method to not publish if the sub is closed. Closing the err channel in the log stream API handler.
811 lines
23 KiB
Go
811 lines
23 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 container
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/harness/gitness/app/gitspace/logutil"
|
|
"github.com/harness/gitness/infraprovider"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
dockerTypes "github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/api/types/strslice"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var _ Orchestrator = (*EmbeddedDockerOrchestrator)(nil)
|
|
|
|
const (
|
|
loggingKey = "gitspace.container"
|
|
sshPort = "22/tcp"
|
|
catchAllIP = "0.0.0.0"
|
|
catchAllPort = "0"
|
|
containerStateRunning = "running"
|
|
containerStateRemoved = "removed"
|
|
templateCloneGit = "clone_git.sh"
|
|
templateSetupSSHServer = "setup_ssh_server.sh"
|
|
gitspacesDir = "gitspaces"
|
|
)
|
|
|
|
type Config struct {
|
|
DefaultBaseImage string
|
|
DefaultBindMountTargetPath string
|
|
DefaultBindMountSourceBasePath string
|
|
DefaultBindMountSourceBasePathAbsolute string
|
|
}
|
|
|
|
type EmbeddedDockerOrchestrator struct {
|
|
dockerClientFactory *infraprovider.DockerClientFactory
|
|
vsCodeService *VSCode
|
|
vsCodeWebService *VSCodeWeb
|
|
config *Config
|
|
statefulLogger *logutil.StatefulLogger
|
|
}
|
|
|
|
func NewEmbeddedDockerOrchestrator(
|
|
dockerClientFactory *infraprovider.DockerClientFactory,
|
|
vsCodeService *VSCode,
|
|
vsCodeWebService *VSCodeWeb,
|
|
config *Config,
|
|
statefulLogger *logutil.StatefulLogger,
|
|
) Orchestrator {
|
|
return &EmbeddedDockerOrchestrator{
|
|
dockerClientFactory: dockerClientFactory,
|
|
vsCodeService: vsCodeService,
|
|
vsCodeWebService: vsCodeWebService,
|
|
config: config,
|
|
statefulLogger: statefulLogger,
|
|
}
|
|
}
|
|
|
|
// StartGitspace checks if the Gitspace is already running by checking its entry in a map. If is it running,
|
|
// it returns, else, it creates a new Gitspace container by using the provided image. If the provided image is
|
|
// nil, it uses a default image read from Gitness config. Post creation it runs the postCreate command and clones
|
|
// the code inside the container. It uses the IDE service to setup the relevant IDE and also installs SSH server
|
|
// inside the container.
|
|
func (e *EmbeddedDockerOrchestrator) StartGitspace(
|
|
ctx context.Context,
|
|
gitspaceConfig *types.GitspaceConfig,
|
|
devcontainerConfig *types.DevcontainerConfig,
|
|
infra *infraprovider.Infrastructure,
|
|
) (*StartResponse, error) {
|
|
containerName := getGitspaceContainerName(gitspaceConfig)
|
|
|
|
log := log.Ctx(ctx).With().Str(loggingKey, containerName).Logger()
|
|
|
|
dockerClient, err := e.dockerClientFactory.NewDockerClient(ctx, infra)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting docker client from docker client factory: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
closingErr := dockerClient.Close()
|
|
if closingErr != nil {
|
|
log.Warn().Err(closingErr).Msg("failed to close docker client")
|
|
}
|
|
}()
|
|
|
|
log.Debug().Msg("checking current state of gitspace")
|
|
state, err := e.containerState(ctx, containerName, dockerClient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var usedPorts map[enum.IDEType]string
|
|
var containerID string
|
|
switch state {
|
|
case containerStateRunning:
|
|
log.Debug().Msg("gitspace is already running")
|
|
|
|
ideService, startErr := e.getIDEService(gitspaceConfig)
|
|
if startErr != nil {
|
|
return nil, startErr
|
|
}
|
|
|
|
id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, ideService)
|
|
if startErr != nil {
|
|
return nil, startErr
|
|
}
|
|
|
|
usedPorts = ports
|
|
containerID = id
|
|
|
|
case containerStateRemoved:
|
|
log.Debug().Msg("gitspace is not running, starting it...")
|
|
|
|
ideService, startErr := e.getIDEService(gitspaceConfig)
|
|
if startErr != nil {
|
|
return nil, startErr
|
|
}
|
|
|
|
logStreamInstance, loggerErr := e.statefulLogger.CreateLogStream(ctx, gitspaceConfig.ID)
|
|
if loggerErr != nil {
|
|
return nil, fmt.Errorf("error getting log stream for gitspace ID %d: %w", gitspaceConfig.ID, loggerErr)
|
|
}
|
|
|
|
defer func() {
|
|
loggerErr = logStreamInstance.Flush()
|
|
if loggerErr != nil {
|
|
log.Warn().Err(loggerErr).Msgf("failed to flush log stream for gitspace ID %d", gitspaceConfig.ID)
|
|
}
|
|
}()
|
|
|
|
startErr = e.startGitspace(
|
|
ctx,
|
|
gitspaceConfig,
|
|
devcontainerConfig,
|
|
containerName,
|
|
dockerClient,
|
|
ideService,
|
|
logStreamInstance,
|
|
)
|
|
if startErr != nil {
|
|
return nil, fmt.Errorf("failed to start gitspace %s: %w", containerName, startErr)
|
|
}
|
|
id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, ideService)
|
|
if startErr != nil {
|
|
return nil, startErr
|
|
}
|
|
|
|
containerID = id
|
|
usedPorts = ports
|
|
|
|
// TODO: Add gitspace status reporting.
|
|
log.Debug().Msgf("started gitspace: %s", gitspaceConfig.Identifier)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("gitspace %s is in a bad state: %s", containerName, state)
|
|
}
|
|
|
|
return &StartResponse{
|
|
ContainerID: containerID,
|
|
ContainerName: containerName,
|
|
WorkingDirectory: strings.TrimPrefix(e.config.DefaultBindMountTargetPath, "/"),
|
|
PortsUsed: usedPorts,
|
|
}, nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) startGitspace(
|
|
ctx context.Context,
|
|
gitspaceConfig *types.GitspaceConfig,
|
|
devcontainerConfig *types.DevcontainerConfig,
|
|
containerName string,
|
|
dockerClient *client.Client,
|
|
ideService IDE,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
var imageName = devcontainerConfig.Image
|
|
if imageName == "" {
|
|
imageName = e.config.DefaultBaseImage
|
|
}
|
|
|
|
err := e.pullImage(ctx, imageName, dockerClient, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = e.createContainer(ctx, gitspaceConfig, dockerClient, imageName, containerName, ideService, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var devcontainer = &Devcontainer{
|
|
ContainerName: containerName,
|
|
DockerClient: dockerClient,
|
|
WorkingDir: e.config.DefaultBindMountTargetPath,
|
|
}
|
|
|
|
err = e.executePostCreateCommand(ctx, devcontainerConfig, devcontainer, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = e.cloneCode(ctx, gitspaceConfig, devcontainerConfig, devcontainer, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = e.setupSSHServer(ctx, gitspaceConfig.GitspaceInstance, devcontainer, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = e.setupIDE(ctx, devcontainer, ideService, logStreamInstance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) setupIDE(
|
|
ctx context.Context,
|
|
devcontainer *Devcontainer,
|
|
ideService IDE,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
loggingErr := logStreamInstance.Write("Setting up IDE inside container: " + string(ideService.Type()))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
output, err := ideService.Setup(ctx, devcontainer)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while setting up IDE inside container: " + err.Error())
|
|
|
|
err = fmt.Errorf("failed to setup IDE for gitspace %s: %w", devcontainer.ContainerName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("IDE setup output...\n" + string(output))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully set up IDE inside container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) getContainerInfo(
|
|
ctx context.Context,
|
|
containerName string,
|
|
dockerClient *client.Client,
|
|
ideService IDE,
|
|
) (string, map[enum.IDEType]string, error) {
|
|
inspectResp, err := dockerClient.ContainerInspect(ctx, containerName)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err)
|
|
}
|
|
|
|
usedPorts := map[enum.IDEType]string{}
|
|
for port, bindings := range inspectResp.NetworkSettings.Ports {
|
|
if port == sshPort {
|
|
usedPorts[enum.IDETypeVSCode] = bindings[0].HostPort
|
|
}
|
|
if port == nat.Port(ideService.PortAndProtocol()) {
|
|
usedPorts[ideService.Type()] = bindings[0].HostPort
|
|
}
|
|
}
|
|
|
|
return inspectResp.ID, usedPorts, nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) getIDEService(gitspaceConfig *types.GitspaceConfig) (IDE, error) {
|
|
var ideService IDE
|
|
|
|
switch gitspaceConfig.IDE {
|
|
case enum.IDETypeVSCode:
|
|
ideService = e.vsCodeService
|
|
case enum.IDETypeVSCodeWeb:
|
|
ideService = e.vsCodeWebService
|
|
default:
|
|
return nil, fmt.Errorf("unsupported IDE: %s", gitspaceConfig.IDE)
|
|
}
|
|
|
|
return ideService, nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) setupSSHServer(
|
|
ctx context.Context,
|
|
gitspaceInstance *types.GitspaceInstance,
|
|
devcontainer *Devcontainer,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
sshServerScript, err := GenerateScriptFromTemplate(
|
|
templateSetupSSHServer, &SetupSSHServerPayload{
|
|
Username: "harness",
|
|
Password: *gitspaceInstance.AccessKey,
|
|
WorkingDirectory: devcontainer.WorkingDir,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to generate scipt to setup ssh server from template %s: %w", templateSetupSSHServer, err)
|
|
}
|
|
|
|
loggingErr := logStreamInstance.Write("Installing ssh-server inside container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
output, err := devcontainer.ExecuteCommand(ctx, sshServerScript, false)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while installing ssh-server inside container: " + err.Error())
|
|
|
|
err = fmt.Errorf("failed to setup SSH server: %w", err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("SSH server installation output...\n" + string(output))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully installed ssh-server inside container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) cloneCode(
|
|
ctx context.Context,
|
|
gitspaceConfig *types.GitspaceConfig,
|
|
devcontainerConfig *types.DevcontainerConfig,
|
|
devcontainer *Devcontainer,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
var devcontainerPresent = "true"
|
|
if devcontainerConfig.Image == "" {
|
|
devcontainerPresent = "false"
|
|
}
|
|
|
|
gitCloneScript, err := GenerateScriptFromTemplate(
|
|
templateCloneGit, &CloneGitPayload{
|
|
RepoURL: gitspaceConfig.CodeRepoURL,
|
|
DevcontainerPresent: devcontainerPresent,
|
|
Image: e.config.DefaultBaseImage,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate scipt to clone git from template %s: %w", templateCloneGit, err)
|
|
}
|
|
|
|
loggingErr := logStreamInstance.Write("Cloning git repo inside container: " + gitspaceConfig.CodeRepoURL)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
output, err := devcontainer.ExecuteCommand(ctx, gitCloneScript, false)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while cloning git repo inside container: " + err.Error())
|
|
|
|
err = fmt.Errorf("failed to clone code: %w", err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Cloning git repo output...\n" + string(output))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully cloned git repo inside container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) executePostCreateCommand(
|
|
ctx context.Context,
|
|
devcontainerConfig *types.DevcontainerConfig,
|
|
devcontainer *Devcontainer,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
if devcontainerConfig.PostCreateCommand == "" {
|
|
loggingErr := logStreamInstance.Write("No post-create command provided, skipping execution")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
loggingErr := logStreamInstance.Write("Executing postCreate command: " + devcontainerConfig.PostCreateCommand)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
output, err := devcontainer.ExecuteCommand(ctx, devcontainerConfig.PostCreateCommand, false)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while executing postCreate command")
|
|
|
|
err = fmt.Errorf("failed to execute postCreate command %q: %w", devcontainerConfig.PostCreateCommand, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Post create command execution output...\n" + string(output))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully executed postCreate command")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) createContainer(
|
|
ctx context.Context,
|
|
gitspaceConfig *types.GitspaceConfig,
|
|
dockerClient *client.Client,
|
|
imageName string,
|
|
containerName string,
|
|
ideService IDE,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
portUsedByIDE := ideService.PortAndProtocol()
|
|
|
|
exposedPorts := nat.PortSet{
|
|
sshPort: struct{}{},
|
|
}
|
|
|
|
hostPortBindings := []nat.PortBinding{
|
|
{
|
|
HostIP: catchAllIP,
|
|
HostPort: catchAllPort,
|
|
},
|
|
}
|
|
|
|
portBindings := nat.PortMap{
|
|
sshPort: hostPortBindings,
|
|
}
|
|
|
|
if portUsedByIDE != "" {
|
|
natPort := nat.Port(portUsedByIDE)
|
|
exposedPorts[natPort] = struct{}{}
|
|
portBindings[natPort] = hostPortBindings
|
|
}
|
|
|
|
entryPoint := make(strslice.StrSlice, 0)
|
|
entryPoint = append(entryPoint, "sleep")
|
|
|
|
commands := make(strslice.StrSlice, 0)
|
|
commands = append(commands, "infinity")
|
|
|
|
bindMountSourcePath :=
|
|
filepath.Join(
|
|
e.config.DefaultBindMountSourceBasePath,
|
|
gitspacesDir,
|
|
gitspaceConfig.SpacePath,
|
|
gitspaceConfig.Identifier,
|
|
)
|
|
|
|
absoluteBindMountSourcePath :=
|
|
filepath.Join(
|
|
e.config.DefaultBindMountSourceBasePathAbsolute,
|
|
gitspacesDir,
|
|
gitspaceConfig.SpacePath,
|
|
gitspaceConfig.Identifier,
|
|
)
|
|
|
|
loggingErr := logStreamInstance.Write(
|
|
"Creating bind mount source directory: " + bindMountSourcePath + " (" + absoluteBindMountSourcePath + " )")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
err := os.MkdirAll(bindMountSourcePath, os.ModePerm)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while creating bind mount source directory: " + err.Error())
|
|
|
|
err = fmt.Errorf(
|
|
"could not create bind mount source path %s: %w", bindMountSourcePath, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully created bind mount source directory")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Creating container: " + containerName)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
resp2, err := dockerClient.ContainerCreate(ctx, &container.Config{
|
|
Image: imageName,
|
|
Entrypoint: entryPoint,
|
|
Cmd: commands,
|
|
ExposedPorts: exposedPorts,
|
|
}, &container.HostConfig{
|
|
PortBindings: portBindings,
|
|
Mounts: []mount.Mount{
|
|
{
|
|
Type: mount.TypeBind,
|
|
Source: absoluteBindMountSourcePath,
|
|
Target: e.config.DefaultBindMountTargetPath,
|
|
},
|
|
},
|
|
}, nil, containerName)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while creating container: " + err.Error())
|
|
|
|
err = fmt.Errorf("could not create container %s: %w", containerName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully created container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Starting container: " + containerName)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
err = dockerClient.ContainerStart(ctx, resp2.ID, dockerTypes.ContainerStartOptions{})
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while creating container: " + err.Error())
|
|
|
|
err = fmt.Errorf("could not start container %s: %w", containerName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully started container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) pullImage(
|
|
ctx context.Context,
|
|
imageName string,
|
|
dockerClient *client.Client,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
loggingErr := logStreamInstance.Write("Pulling image: " + imageName)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
pullResponse, err := dockerClient.ImagePull(ctx, imageName, dockerTypes.ImagePullOptions{})
|
|
|
|
defer func() {
|
|
closingErr := pullResponse.Close()
|
|
if closingErr != nil {
|
|
log.Warn().Err(closingErr).Msg("failed to close image pull response")
|
|
}
|
|
}()
|
|
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while pulling image: " + err.Error())
|
|
|
|
err = fmt.Errorf("could not pull image %s: %w", imageName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// NOTE: It is necessary to read all the data in pullResponse to ensure the image has been completely downloaded.
|
|
// If the execution proceeds before the response is completed, the container will not find the required image.
|
|
output, err := io.ReadAll(pullResponse)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while parsing image pull response: " + err.Error())
|
|
|
|
err = fmt.Errorf("error while parsing pull image output %s: %w", imageName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write(string(output))
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully pulled image")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StopGitspace checks if the Gitspace container is running. If yes, it stops and removes the container.
|
|
// Else it returns.
|
|
func (e EmbeddedDockerOrchestrator) StopGitspace(
|
|
ctx context.Context,
|
|
gitspaceConfig *types.GitspaceConfig,
|
|
infra *infraprovider.Infrastructure,
|
|
) error {
|
|
containerName := getGitspaceContainerName(gitspaceConfig)
|
|
|
|
log := log.Ctx(ctx).With().Str(loggingKey, containerName).Logger()
|
|
|
|
dockerClient, err := e.dockerClientFactory.NewDockerClient(ctx, infra)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting docker client from docker client factory: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
closingErr := dockerClient.Close()
|
|
if closingErr != nil {
|
|
log.Warn().Err(closingErr).Msg("failed to close docker client")
|
|
}
|
|
}()
|
|
|
|
log.Debug().Msg("checking current state of gitspace")
|
|
state, err := e.containerState(ctx, containerName, dockerClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if state == containerStateRemoved {
|
|
log.Debug().Msg("gitspace is already stopped")
|
|
return nil
|
|
}
|
|
|
|
log.Debug().Msg("stopping gitspace")
|
|
|
|
logStreamInstance, loggerErr := e.statefulLogger.CreateLogStream(ctx, gitspaceConfig.ID)
|
|
if loggerErr != nil {
|
|
return fmt.Errorf("error getting log stream for gitspace ID %d: %w", gitspaceConfig.ID, loggerErr)
|
|
}
|
|
|
|
defer func() {
|
|
loggerErr = logStreamInstance.Flush()
|
|
if loggerErr != nil {
|
|
log.Warn().Err(loggerErr).Msgf("failed to flush log stream for gitspace ID %d", gitspaceConfig.ID)
|
|
}
|
|
}()
|
|
|
|
err = e.stopGitspace(ctx, containerName, dockerClient, logStreamInstance)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stop gitspace %s: %w", containerName, err)
|
|
}
|
|
|
|
log.Debug().Msg("stopped gitspace")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e EmbeddedDockerOrchestrator) stopGitspace(
|
|
ctx context.Context,
|
|
containerName string,
|
|
dockerClient *client.Client,
|
|
logStreamInstance *logutil.LogStreamInstance,
|
|
) error {
|
|
loggingErr := logStreamInstance.Write("Stopping container: " + containerName)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
err := dockerClient.ContainerStop(ctx, containerName, nil)
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while stopping container: " + err.Error())
|
|
|
|
err = fmt.Errorf("could not stop container %s: %w", containerName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully stopped container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Removing container: " + containerName)
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
err = dockerClient.ContainerRemove(ctx, containerName, dockerTypes.ContainerRemoveOptions{Force: true})
|
|
if err != nil {
|
|
loggingErr = logStreamInstance.Write("Error while removing container: " + err.Error())
|
|
|
|
err = fmt.Errorf("could not remove container %s: %w", containerName, err)
|
|
|
|
if loggingErr != nil {
|
|
err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
loggingErr = logStreamInstance.Write("Successfully removed container")
|
|
if loggingErr != nil {
|
|
return fmt.Errorf("logging error: %w", loggingErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getGitspaceContainerName(config *types.GitspaceConfig) string {
|
|
return "gitspace-" + config.UserID + "-" + config.Identifier
|
|
}
|
|
|
|
// Status is NOOP for EmbeddedDockerOrchestrator as the docker host is verified by the infra provisioner.
|
|
func (e *EmbeddedDockerOrchestrator) Status(_ context.Context, _ *infraprovider.Infrastructure) error {
|
|
return nil
|
|
}
|
|
|
|
func (e *EmbeddedDockerOrchestrator) containerState(
|
|
ctx context.Context,
|
|
containerName string,
|
|
dockerClient *client.Client,
|
|
) (string, error) {
|
|
var args = filters.NewArgs()
|
|
args.Add("name", containerName)
|
|
|
|
containers, err := dockerClient.ContainerList(ctx, dockerTypes.ContainerListOptions{All: true, Filters: args})
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not list container %s: %w", containerName, err)
|
|
}
|
|
|
|
if len(containers) == 0 {
|
|
return containerStateRemoved, nil
|
|
}
|
|
|
|
return containers[0].State, nil
|
|
}
|