// 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" "strconv" "strings" "github.com/harness/gitness/app/gitspace/logutil" "github.com/harness/gitness/app/gitspace/orchestrator/devcontainer" "github.com/harness/gitness/app/gitspace/orchestrator/ide" "github.com/harness/gitness/app/gitspace/orchestrator/template" "github.com/harness/gitness/app/gitspace/scm" "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/rs/zerolog/log" ) var _ Orchestrator = (*EmbeddedDockerOrchestrator)(nil) const ( loggingKey = "gitspace.container" catchAllIP = "0.0.0.0" containerStateRunning = "running" containerStateRemoved = "removed" containerStateStopped = "exited" templateCloneGit = "clone_git.sh" templateAuthenticateGit = "authenticate_git.sh" templateManageUser = "manage_user.sh" harnessUser = "harness" ) type EmbeddedDockerOrchestrator struct { dockerClientFactory *infraprovider.DockerClientFactory statefulLogger *logutil.StatefulLogger } func NewEmbeddedDockerOrchestrator( dockerClientFactory *infraprovider.DockerClientFactory, statefulLogger *logutil.StatefulLogger, ) Orchestrator { return &EmbeddedDockerOrchestrator{ dockerClientFactory: dockerClientFactory, statefulLogger: statefulLogger, } } // CreateAndStartGitspace starts an exited container and starts a new container if the container is removed. // If the container is newly created, it clones the code, sets up the IDE and executes the postCreateCommand. // It returns the container ID, name and ports used. // It returns an error if the container is not running, exited or removed. func (e *EmbeddedDockerOrchestrator) CreateAndStartGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, infra *types.Infrastructure, resolvedRepoDetails *scm.ResolvedDetails, defaultBaseImage string, ideService ide.IDE, ) (*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 } switch state { case containerStateRunning: log.Debug().Msg("gitspace is already running") case containerStateStopped: log.Debug().Msg("gitspace is stopped, starting it") 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.startContainer(ctx, dockerClient, containerName, logStreamInstance) if startErr != nil { return nil, startErr } devcontainer := &devcontainer.Exec{ ContainerName: containerName, WorkingDir: e.getWorkingDir(resolvedRepoDetails.RepoName), DockerClient: dockerClient, } if resolvedRepoDetails.Credentials != nil { if err := e.authenticateGit(ctx, devcontainer, resolvedRepoDetails); err != nil { return nil, err } } err = e.runIDE(ctx, devcontainer, ideService, logStreamInstance) if err != nil { return nil, err } // TODO: Add gitspace status reporting. log.Debug().Msg("started gitspace") case containerStateRemoved: log.Debug().Msg("gitspace is removed, creating it...") 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, containerName, dockerClient, ideService, logStreamInstance, infra.Storage, e.getWorkingDir(resolvedRepoDetails.RepoName), resolvedRepoDetails, infra.PortMappings, defaultBaseImage, ) if startErr != nil { return nil, fmt.Errorf("failed to start gitspace %s: %w", containerName, startErr) } // TODO: Add gitspace status reporting. log.Debug().Msg("started gitspace") default: return nil, fmt.Errorf("gitspace %s is in a bad state: %s", containerName, state) } id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, infra.PortMappings) if startErr != nil { return nil, startErr } return &StartResponse{ ContainerID: id, ContainerName: containerName, PublishedPorts: ports, }, nil } func (e *EmbeddedDockerOrchestrator) getWorkingDir(repoName string) string { return "/" + repoName } func (e *EmbeddedDockerOrchestrator) startGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, containerName string, dockerClient *client.Client, ideService ide.IDE, logStreamInstance *logutil.LogStreamInstance, volumeName string, workingDirectory string, resolvedRepoDetails *scm.ResolvedDetails, portMappings map[int]*types.PortMapping, defaultBaseImage string, ) error { var imageName = resolvedRepoDetails.DevcontainerConfig.Image if imageName == "" { imageName = defaultBaseImage } err := e.pullImage(ctx, imageName, dockerClient, logStreamInstance) if err != nil { return err } err = e.createContainer( ctx, dockerClient, imageName, containerName, logStreamInstance, volumeName, workingDirectory, portMappings, ) if err != nil { return err } err = e.startContainer(ctx, dockerClient, containerName, logStreamInstance) if err != nil { return err } var devcontainer = &devcontainer.Exec{ ContainerName: containerName, DockerClient: dockerClient, WorkingDir: workingDirectory, } err = e.manageUser(ctx, devcontainer, logStreamInstance, gitspaceConfig.GitspaceInstance.AccessKey) if err != nil { return err } err = e.setupIDE(ctx, gitspaceConfig.GitspaceInstance, devcontainer, ideService, logStreamInstance) if err != nil { return err } err = e.runIDE(ctx, devcontainer, ideService, logStreamInstance) if err != nil { return err } err = e.cloneCode(ctx, devcontainer, defaultBaseImage, logStreamInstance, resolvedRepoDetails) if err != nil { return err } err = e.executePostCreateCommand(ctx, resolvedRepoDetails.DevcontainerConfig, devcontainer, logStreamInstance) if err != nil { return err } return nil } // TODO: Instead of explicitly running IDE related processes, we can explore service to run the service on boot. func (e *EmbeddedDockerOrchestrator) runIDE( ctx context.Context, devcontainer *devcontainer.Exec, ideService ide.IDE, logStreamInstance *logutil.LogStreamInstance, ) error { loggingErr := logStreamInstance.Write("Running the IDE inside container: " + string(ideService.Type())) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } output, err := ideService.Run(ctx, devcontainer) if err != nil { loggingErr = logStreamInstance.Write("Error while running IDE inside container: " + err.Error()) err = fmt.Errorf("failed to run the 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 run output...\n" + string(output)) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } loggingErr = logStreamInstance.Write("Successfully run the IDE inside container") if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } return nil } func (e *EmbeddedDockerOrchestrator) setupIDE( ctx context.Context, gitspaceInstance *types.GitspaceInstance, devcontainer *devcontainer.Exec, ideService ide.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, gitspaceInstance) 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, portMappings map[int]*types.PortMapping, ) (string, map[int]string, error) { inspectResp, err := dockerClient.ContainerInspect(ctx, containerName) if err != nil { return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err) } var usedPorts = make(map[int]string) for portAndProtocol, bindings := range inspectResp.NetworkSettings.Ports { portRaw := strings.Split(string(portAndProtocol), "/")[0] port, conversionErr := strconv.Atoi(portRaw) if conversionErr != nil { return "", nil, fmt.Errorf("could not convert port %s to int: %w", portRaw, err) } if portMappings[port] != nil { usedPorts[port] = bindings[0].HostPort } } return inspectResp.ID, usedPorts, nil } func (e *EmbeddedDockerOrchestrator) authenticateGit( ctx context.Context, devcontainer *devcontainer.Exec, resolvedRepoDetails *scm.ResolvedDetails, ) error { data := &template.AuthenticateGitPayload{ Email: resolvedRepoDetails.Credentials.Email, Name: resolvedRepoDetails.Credentials.Name, Password: resolvedRepoDetails.Credentials.Password, } gitAuthenticateScript, err := template.GenerateScriptFromTemplate( templateAuthenticateGit, data) if err != nil { return fmt.Errorf("failed to generate scipt to authenticate git from template %s: %w", templateAuthenticateGit, err) } _, err = devcontainer.ExecuteCommand(ctx, gitAuthenticateScript, false, harnessUser) if err != nil { err = fmt.Errorf("failed to authenticate git in container: %w", err) return err } return nil } func (e *EmbeddedDockerOrchestrator) manageUser( ctx context.Context, devcontainer *devcontainer.Exec, logStreamInstance *logutil.LogStreamInstance, accessKey *string, ) error { data := template.SetupSSHServerPayload{ Username: "harness", Password: *accessKey, WorkingDirectory: devcontainer.WorkingDir, } manageUserScript, err := template.GenerateScriptFromTemplate( templateManageUser, data) if err != nil { return fmt.Errorf("failed to generate scipt to manage user from template %s: %w", templateManageUser, err) } loggingErr := logStreamInstance.Write( "creating user inside container: " + data.Username) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } output, err := devcontainer.ExecuteCommand(ctx, manageUserScript, false, "root") if err != nil { loggingErr = logStreamInstance.Write("Error while creating user inside container : " + err.Error()) err = fmt.Errorf("failed to create user: %w", err) if loggingErr != nil { err = fmt.Errorf("original error: %w; logging error: %w", err, loggingErr) } return err } loggingErr = logStreamInstance.Write("Managing user output...\n" + string(output)) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } loggingErr = logStreamInstance.Write("Successfully created user inside container") if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } return nil } func (e *EmbeddedDockerOrchestrator) cloneCode( ctx context.Context, devcontainer *devcontainer.Exec, defaultBaseImage string, logStreamInstance *logutil.LogStreamInstance, resolvedRepoDetails *scm.ResolvedDetails, ) error { data := &template.CloneGitPayload{ RepoURL: resolvedRepoDetails.CloneURL, Image: defaultBaseImage, Branch: resolvedRepoDetails.Branch, } if resolvedRepoDetails.Credentials != nil { data.Email = resolvedRepoDetails.Credentials.Email data.Name = resolvedRepoDetails.Credentials.Name data.Password = resolvedRepoDetails.Credentials.Password } gitCloneScript, err := template.GenerateScriptFromTemplate( templateCloneGit, data) 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: " + resolvedRepoDetails.CloneURL + " branch: " + resolvedRepoDetails.Branch) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } output, err := devcontainer.ExecuteCommand(ctx, gitCloneScript, false, harnessUser) 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.Exec, 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, harnessUser) 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) startContainer( ctx context.Context, dockerClient *client.Client, containerName string, logStreamInstance *logutil.LogStreamInstance, ) error { loggingErr := logStreamInstance.Write("Starting container: " + containerName) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } err := dockerClient.ContainerStart(ctx, containerName, container.StartOptions{}) 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) createContainer( ctx context.Context, dockerClient *client.Client, imageName string, containerName string, logStreamInstance *logutil.LogStreamInstance, volumeName string, workingDirectory string, portMappings map[int]*types.PortMapping, ) error { exposedPorts := nat.PortSet{} portBindings := nat.PortMap{} for port, mapping := range portMappings { natPort := nat.Port(strconv.Itoa(port) + "/tcp") hostPortBindings := []nat.PortBinding{ { HostIP: catchAllIP, HostPort: strconv.Itoa(mapping.PublishedPort), }, } exposedPorts[natPort] = struct{}{} portBindings[natPort] = hostPortBindings } loggingErr := logStreamInstance.Write("Creating container: " + containerName) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } _, err := dockerClient.ContainerCreate(ctx, &container.Config{ Image: imageName, Entrypoint: []string{"/bin/sh"}, Cmd: []string{"-c", "trap 'exit 0' 15; sleep infinity & wait $!"}, ExposedPorts: exposedPorts, }, &container.HostConfig{ PortBindings: portBindings, Mounts: []mount.Mount{ { Type: mount.TypeVolume, Source: volumeName, Target: workingDirectory, }, }, }, nil, 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 } 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, image.PullOptions{}) 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 stops a container. If it is removed, it returns an error. func (e EmbeddedDockerOrchestrator) StopGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, infra *types.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 { return fmt.Errorf("gitspace %s is removed", containerName) } if state == containerStateStopped { 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.stopContainer(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) stopContainer( 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, container.StopOptions{}) 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) } 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, _ *types.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, container.ListOptions{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 } // StopAndRemoveGitspace stops the container if not stopped and removes it. // If the container is already removed, it returns. func (e *EmbeddedDockerOrchestrator) StopAndRemoveGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, infra *types.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 removed") return nil } 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) } }() if state != containerStateStopped { log.Debug().Msg("stopping gitspace") err = e.stopContainer(ctx, containerName, dockerClient, logStreamInstance) if err != nil { return fmt.Errorf("failed to stop gitspace %s: %w", containerName, err) } log.Debug().Msg("stopped gitspace") } log.Debug().Msg("removing gitspace") err = e.removeContainer(ctx, containerName, dockerClient, logStreamInstance) if err != nil { return fmt.Errorf("failed to remove gitspace %s: %w", containerName, err) } log.Debug().Msg("removed gitspace") return nil } func (e EmbeddedDockerOrchestrator) removeContainer( ctx context.Context, containerName string, dockerClient *client.Client, logStreamInstance *logutil.LogStreamInstance, ) error { loggingErr := logStreamInstance.Write("Removing container: " + containerName) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } err := dockerClient.ContainerRemove(ctx, containerName, container.RemoveOptions{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 }