feat: [CDE-92]: Container orchestrator and IDE service

* feat: [CDE-92]: Removing Client method from infraprovider interface and using DockerClientFactory to generate docker clients. Using go templates to substitute values in gitspace scripts. Setting a default base path for gitspaces if not provided.
* Rebasing
* feat: [CDE-92]: Addressing review comments.
* Rebasing
* Rebasing
* feat: [CDE-92]: Addressing review comments
* Rebasing
* feat: [CDE-92]: Using port from config for code-server installation
* feat: [CDE-92]: Initial commit
* Rebasing
unified-ui
Dhruv Dhruv 2024-07-05 05:28:31 +00:00 committed by Harness
parent 4833ed67a5
commit 3acded8ed8
19 changed files with 1197 additions and 124 deletions

View File

@ -0,0 +1,40 @@
// 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"
"github.com/harness/gitness/infraprovider"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type Orchestrator interface {
// StartGitspace starts the gitspace container using the specified image or default image, clones the code,
// runs SSH server and installs the IDE inside the container. It returns a map of the ports used by the Gitspace.
StartGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
devcontainerConfig *types.DevcontainerConfig,
infra *infraprovider.Infrastructure,
) (map[enum.IDEType]string, error)
// StopGitspace stops and removes the gitspace container.
StopGitspace(ctx context.Context, gitspaceConfig *types.GitspaceConfig, infra *infraprovider.Infrastructure) error
// Status checks if the infra is reachable and ready to begin container creation.
Status(ctx context.Context, infra *infraprovider.Infrastructure) error
}

View File

@ -0,0 +1,67 @@
// 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"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type Devcontainer struct {
ContainerName string
WorkingDir string
DockerClient *client.Client
}
func (d *Devcontainer) ExecuteCommand(ctx context.Context, command string, detach bool) (*[]byte, error) {
cmd := []string{"/bin/bash", "-c", command}
execConfig := dockerTypes.ExecConfig{
User: "root",
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
Detach: detach,
WorkingDir: d.WorkingDir,
}
execID, err := d.DockerClient.ContainerExecCreate(ctx, d.ContainerName, execConfig)
if err != nil {
return nil, fmt.Errorf("failed to create docker exec for container %s: %w", d.ContainerName, err)
}
execResponse, err := d.DockerClient.ContainerExecAttach(ctx, execID.ID, dockerTypes.ExecStartCheck{Detach: 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", d.ContainerName, err)
}
if execResponse.Conn != nil {
defer execResponse.Close()
}
var output []byte
if execResponse.Reader != nil {
output, err = io.ReadAll(execResponse.Reader)
if err != nil {
return nil, err
}
}
return &output, nil
}

View File

@ -0,0 +1,508 @@
// 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"
"os"
"path/filepath"
"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"
)
type Config struct {
DefaultBaseImage string
DefaultBindMountTargetPath string
DefaultBindMountSourceBasePath string
}
type EmbeddedDockerOrchestrator struct {
dockerClientFactory *infraprovider.DockerClientFactory
vsCodeService *VSCode
vsCodeWebService *VSCodeWeb
config *Config
}
func NewEmbeddedDockerOrchestrator(
dockerClientFactory *infraprovider.DockerClientFactory,
vsCodeService *VSCode,
vsCodeWebService *VSCodeWeb,
config *Config,
) Orchestrator {
return &EmbeddedDockerOrchestrator{
dockerClientFactory: dockerClientFactory,
vsCodeService: vsCodeService,
vsCodeWebService: vsCodeWebService,
config: config,
}
}
// 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,
) (map[enum.IDEType]string, 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
switch state {
case containerStateRunning:
log.Debug().Msg("gitspace is already running")
ideService, startErr := e.getIDEService(gitspaceConfig)
if startErr != nil {
return nil, startErr
}
ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService)
if startErr != nil {
return nil, startErr
}
usedPorts = ports
case containerStateRemoved:
log.Debug().Msg("gitspace is not running, starting it...")
ideService, startErr := e.getIDEService(gitspaceConfig)
if startErr != nil {
return nil, startErr
}
startErr = e.startGitspace(
ctx,
gitspaceConfig,
devcontainerConfig,
containerName,
dockerClient,
ideService,
)
if startErr != nil {
return nil, fmt.Errorf("failed to start gitspace %s: %w", containerName, startErr)
}
ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService)
if startErr != nil {
return nil, startErr
}
usedPorts = ports
// 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)
}
return usedPorts, nil
}
func (e *EmbeddedDockerOrchestrator) startGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
devcontainerConfig *types.DevcontainerConfig,
containerName string,
dockerClient *client.Client,
ideService IDE,
) error {
var imageName = devcontainerConfig.Image
if imageName == "" {
imageName = e.config.DefaultBaseImage
}
err := e.pullImage(ctx, imageName, dockerClient)
if err != nil {
return err
}
err = e.createContainer(ctx, gitspaceConfig, dockerClient, imageName, containerName, ideService)
if err != nil {
return err
}
var devcontainer = &Devcontainer{
ContainerName: containerName,
DockerClient: dockerClient,
WorkingDir: e.config.DefaultBindMountTargetPath,
}
err = e.executePostCreateCommand(ctx, devcontainerConfig, devcontainer)
if err != nil {
return err
}
err = e.cloneCode(ctx, gitspaceConfig, devcontainerConfig, devcontainer)
if err != nil {
return err
}
err = e.setupSSHServer(ctx, gitspaceConfig.GitspaceInstance, devcontainer)
if err != nil {
return err
}
err = ideService.Setup(ctx, devcontainer, gitspaceConfig.GitspaceInstance)
if err != nil {
return fmt.Errorf("failed to setup IDE for gitspace %s: %w", containerName, err)
}
return nil
}
func (e *EmbeddedDockerOrchestrator) getUsedPorts(
ctx context.Context,
containerName string,
dockerClient *client.Client,
ideService IDE,
) (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 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,
) error {
sshServerScript, err := GenerateScriptFromTemplate(
templateSetupSSHServer, &SetupSSHServerPayload{
Username: "harness",
Password: gitspaceInstance.AccessKey.String,
WorkingDirectory: devcontainer.WorkingDir,
})
if err != nil {
return fmt.Errorf(
"failed to generate scipt to setup ssh server from template %s: %w", templateSetupSSHServer, err)
}
_, err = devcontainer.ExecuteCommand(ctx, sshServerScript, false)
if err != nil {
return fmt.Errorf("failed to setup SSH server: %w", err)
}
return nil
}
func (e *EmbeddedDockerOrchestrator) cloneCode(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
devcontainerConfig *types.DevcontainerConfig,
devcontainer *Devcontainer,
) 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)
}
_, err = devcontainer.ExecuteCommand(ctx, gitCloneScript, false)
if err != nil {
return fmt.Errorf("failed to clone code: %w", err)
}
return nil
}
func (e *EmbeddedDockerOrchestrator) executePostCreateCommand(
ctx context.Context,
devcontainerConfig *types.DevcontainerConfig,
devcontainer *Devcontainer,
) error {
if devcontainerConfig.PostCreateCommand == "" {
return nil
}
_, err := devcontainer.ExecuteCommand(ctx, devcontainerConfig.PostCreateCommand, false)
if err != nil {
return fmt.Errorf("post create command failed %q: %w", devcontainerConfig.PostCreateCommand, err)
}
return nil
}
func (e *EmbeddedDockerOrchestrator) createContainer(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
dockerClient *client.Client,
imageName string,
containerName string,
ideService IDE,
) 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,
gitspaceConfig.SpacePath,
gitspaceConfig.Identifier,
)
err := os.MkdirAll(bindMountSourcePath, 0600)
if err != nil {
return fmt.Errorf(
"could not create bind mount source path %s: %w", bindMountSourcePath, err)
}
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: bindMountSourcePath,
Target: e.config.DefaultBindMountTargetPath,
},
},
}, nil, containerName)
if err != nil {
return fmt.Errorf("could not create container %s: %w", containerName, err)
}
err = dockerClient.ContainerStart(ctx, resp2.ID, dockerTypes.ContainerStartOptions{})
if err != nil {
return fmt.Errorf("could not start container %s: %w", containerName, err)
}
return nil
}
func (e *EmbeddedDockerOrchestrator) pullImage(
ctx context.Context,
imageName string,
dockerClient *client.Client,
) error {
resp, err := dockerClient.ImagePull(ctx, imageName, dockerTypes.ImagePullOptions{})
defer func() {
closingErr := resp.Close()
if closingErr != nil {
log.Warn().Err(closingErr).Msg("failed to close image pull response")
}
}()
if err != nil {
return fmt.Errorf("could not pull image %s: %w", imageName, err)
}
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")
err = e.stopGitspace(ctx, containerName, dockerClient)
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,
) error {
err := dockerClient.ContainerStop(ctx, containerName, nil)
if err != nil {
return fmt.Errorf("could not stop container %s: %w", containerName, err)
}
err = dockerClient.ContainerRemove(ctx, containerName, dockerTypes.ContainerRemoveOptions{Force: true})
if err != nil {
return fmt.Errorf("could not remove container %s: %w", containerName, err)
}
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
}

View File

@ -12,21 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package infraprovider
package container
import (
"context"
"github.com/docker/docker/client"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
var _ Client = (*DockerClient)(nil)
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, ensuring SSH server is running etc.
Setup(ctx context.Context, containerParams *Devcontainer, gitspaceInstance *types.GitspaceInstance) error
type DockerClient struct {
dockerClient *client.Client
closeFunc func(ctx context.Context)
}
// PortAndProtocol provides the port with protocol which will be used by this IDE.
PortAndProtocol() string
func (d DockerClient) Close(ctx context.Context) {
d.closeFunc(ctx)
// Type provides the IDE type to which the service belongs.
Type() enum.IDEType
}

View File

@ -0,0 +1,96 @@
// 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 (
"bytes"
"embed"
"fmt"
"io/fs"
"path"
"text/template"
)
const (
templatesDir = "template"
)
var (
//go:embed template/*
files embed.FS
scriptTemplates map[string]*template.Template
)
type CloneGitPayload struct {
RepoURL string
DevcontainerPresent string
Image string
}
type InstallVSCodeWebPayload struct {
Password string
Port string
}
type SetupSSHServerPayload struct {
Username string
Password string
WorkingDirectory string
}
func init() {
err := LoadTemplates()
if err != nil {
panic(fmt.Sprintf("error loading script templates: %v", err))
}
}
func LoadTemplates() error {
scriptTemplates = make(map[string]*template.Template)
tmplFiles, err := fs.ReadDir(files, templatesDir)
if err != nil {
return fmt.Errorf("error reading script templates: %w", err)
}
for _, tmpl := range tmplFiles {
if tmpl.IsDir() {
continue
}
textTemplate, parsingErr := template.ParseFS(files, path.Join(templatesDir, tmpl.Name()))
if parsingErr != nil {
return fmt.Errorf("error parsing template %s: %w", tmpl.Name(), parsingErr)
}
scriptTemplates[tmpl.Name()] = textTemplate
}
return nil
}
func GenerateScriptFromTemplate(name string, data interface{}) (string, error) {
if scriptTemplates[name] == nil {
return "", fmt.Errorf("no script template found for %s", name)
}
tmplOutput := bytes.Buffer{}
err := scriptTemplates[name].Execute(&tmplOutput, data)
if err != nil {
return "", fmt.Errorf("error executing template %s with data %+v: %w", name, data, err)
}
return tmplOutput.String(), nil
}

View File

@ -0,0 +1,49 @@
#!/bin/bash
repo_url={{ .RepoURL }}
devcontainer_present={{ .DevcontainerPresent }}
image={{ .Image }}
# Extract the repository name from the URL
repo_name=$(basename -s .git "$repo_url")
# Check if Git is installed
if ! command -v git &>/dev/null; then
echo "Git is not installed. Installing Git..."
apt-get update
apt-get install -y git
fi
if ! command -v git &>/dev/null; then
echo "Git is not installed. Exiting..."
exit 1
fi
# Clone the repository only if it doesn't exist
if [ ! -d "$repo_name" ]; then
echo "Cloning the repository..."
git clone "$repo_url"
else
echo "Repository already exists. Skipping clone."
fi
# Check if devcontainer_present is set to false
if [ "$devcontainer_present" = "false" ]; then
# Ensure the repository is cloned
if [ -d "$repo_name" ]; then
echo "Creating .devcontainer directory and devcontainer.json..."
mkdir -p "$repo_name/.devcontainer"
cat <<EOL > "$repo_name/.devcontainer/devcontainer.json"
{
"image": "$image"
}
EOL
echo "devcontainer.json created."
else
echo "Repository directory not found. Cannot create .devcontainer."
fi
else
echo "devcontainer_present is set to true. Skipping .devcontainer creation."
fi
echo "Script completed."

View File

@ -0,0 +1,21 @@
#!/bin/bash
echo "Installing code-server"
password={{ .Password }}
port={{ .Port }}
curl -fsSL https://code-server.dev/install.sh | sh
# Ensure the configuration directory exists
mkdir -p /root/.config/code-server
# Create or overwrite the config file with new settings
cat > /root/.config/code-server/config.yaml <<EOF
bind-addr: 0.0.0.0:$port
auth: password
password: $password
cert: false
EOF
code-server

View File

@ -0,0 +1,52 @@
#!/bin/bash
# Install SSH if it's not already installed
if ! command -v sshd &> /dev/null; then
echo "OpenSSH server is not installed. Installing..."
apt-get update
apt-get install -y openssh-server
else
echo "OpenSSH server is already installed."
fi
username={{ .Username }}
password={{ .Password }}
workingDir={{ .WorkingDirectory }}
# Check if the user already exists
if id "$username" &> /dev/null; then
echo "User $username already exists."
else
# Create a new user
adduser --disabled-password --home "$workingDir" --gecos "" "$username"
if [ $? -ne 0 ]; then
echo "Failed to create user $username."
exit 1
fi
fi
# Set or update the user's password using chpasswd
echo "$username:$password" | chpasswd
# Configure SSH to allow this user
config_file='/etc/ssh/sshd_config'
grep -q "^AllowUsers" $config_file
if [ $? -eq 0 ]; then
# If AllowUsers exists, add the user to it
sed -i "/^AllowUsers/ s/$/ $username/" $config_file
else
# Otherwise, add a new AllowUsers line
echo "AllowUsers $username" >> $config_file
fi
# Ensure password authentication is enabled
sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' $config_file
if ! grep -q "^PasswordAuthentication yes" $config_file; then
echo "PasswordAuthentication yes" >> $config_file
fi
# Changing ownership of everything inside user home to the newly created user
chown -R $username .
mkdir /var/run/sshd
/usr/sbin/sshd

View File

@ -0,0 +1,45 @@
// 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"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
var _ IDE = (*VSCode)(nil)
type VSCode struct{}
func NewVsCodeService() *VSCode {
return &VSCode{}
}
// Setup is a NOOP since VS Code doesn't require any installation.
// TODO Check if the SSH server is accessible on the required port.
func (v *VSCode) Setup(_ context.Context, _ *Devcontainer, _ *types.GitspaceInstance) error {
return nil
}
// PortAndProtocol return nil since VS Code doesn't require any additional port to be exposed.
func (v *VSCode) PortAndProtocol() string {
return ""
}
func (v *VSCode) Type() enum.IDEType {
return enum.IDETypeVSCode
}

View File

@ -0,0 +1,76 @@
// 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"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
var _ IDE = (*VSCodeWeb)(nil)
const templateInstallVSCodeWeb = "install_vscode_web.sh"
type VSCodeWebConfig struct {
Port string
}
type VSCodeWeb struct {
config *VSCodeWebConfig
}
func NewVsCodeWebService(config *VSCodeWebConfig) *VSCodeWeb {
return &VSCodeWeb{config: config}
}
// Setup runs the installScript which downloads the required version of the code-server binary and runs it
// with the given password.
func (v *VSCodeWeb) Setup(
ctx context.Context,
devcontainer *Devcontainer,
gitspaceInstance *types.GitspaceInstance,
) error {
installScript, err := GenerateScriptFromTemplate(
templateInstallVSCodeWeb, &InstallVSCodeWebPayload{
Password: gitspaceInstance.AccessKey.String,
Port: v.config.Port,
})
if err != nil {
return fmt.Errorf(
"failed to generate scipt to install code server from template %s: %w",
templateInstallVSCodeWeb,
err,
)
}
_, err = devcontainer.ExecuteCommand(ctx, installScript, true)
if err != nil {
return fmt.Errorf("failed to install code-server: %w", err)
}
return nil
}
// PortAndProtocol returns the port on which the code-server is listening.
func (v *VSCodeWeb) PortAndProtocol() string {
return v.config.Port + "/tcp"
}
func (v *VSCodeWeb) Type() enum.IDEType {
return enum.IDETypeVSCodeWeb
}

View File

@ -0,0 +1,49 @@
// 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 (
"github.com/harness/gitness/infraprovider"
"github.com/google/wire"
)
var WireSet = wire.NewSet(
ProvideEmbeddedDockerOrchestrator,
ProvideVSCodeWebService,
ProvideVSCodeService,
)
func ProvideEmbeddedDockerOrchestrator(
dockerClientFactory *infraprovider.DockerClientFactory,
vsCodeService *VSCode,
vsCodeWebService *VSCodeWeb,
config *Config,
) Orchestrator {
return NewEmbeddedDockerOrchestrator(
dockerClientFactory,
vsCodeService,
vsCodeWebService,
config,
)
}
func ProvideVSCodeWebService(config *VSCodeWebConfig) *VSCodeWeb {
return NewVsCodeWebService(config)
}
func ProvideVSCodeService() *VSCode {
return NewVsCodeService()
}

View File

@ -22,6 +22,7 @@ import (
"strings"
"unicode"
"github.com/harness/gitness/app/gitspace/orchestrator/container"
"github.com/harness/gitness/app/services/cleanup"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/keywordsearch"
@ -31,6 +32,7 @@ import (
"github.com/harness/gitness/blob"
"github.com/harness/gitness/events"
gittypes "github.com/harness/gitness/git/types"
"github.com/harness/gitness/infraprovider"
"github.com/harness/gitness/job"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/pubsub"
@ -48,6 +50,7 @@ const (
schemeHTTPS = "https"
gitnessHomeDir = ".gitness"
blobDir = "blob"
gitspacesDir = "gitspaces"
)
// LoadConfig returns the system configuration from the
@ -371,3 +374,44 @@ func ProvideJobsConfig(config *types.Config) job.Config {
BackgroundJobsRetentionTime: config.BackgroundJobs.RetentionTime,
}
}
// ProvideDockerConfig loads config for Docker.
func ProvideDockerConfig(config *types.Config) *infraprovider.DockerConfig {
return &infraprovider.DockerConfig{
DockerHost: config.Docker.Host,
DockerAPIVersion: config.Docker.APIVersion,
DockerCertPath: config.Docker.CertPath,
DockerTLSVerify: config.Docker.TLSVerify,
}
}
// ProvideIDEVSCodeWebConfig loads the VSCode Web IDE config from the main config.
func ProvideIDEVSCodeWebConfig(config *types.Config) *container.VSCodeWebConfig {
return &container.VSCodeWebConfig{
Port: config.IDE.VSCodeWeb.Port,
}
}
// ProvideGitspaceContainerOrchestratorConfig loads the Gitspace container orchestrator config from the main config.
func ProvideGitspaceContainerOrchestratorConfig(config *types.Config) (*container.Config, error) {
var bindMountSourceBasePath string
if config.Gitspace.DefaultBindMountSourceBasePath == "" {
var homedir string
homedir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("unable to determine home directory: %w", err)
}
bindMountSourceBasePath = filepath.Join(homedir, gitnessHomeDir, gitspacesDir)
} else {
bindMountSourceBasePath = filepath.Join(config.Gitspace.DefaultBindMountSourceBasePath, gitspacesDir)
}
return &container.Config{
DefaultBaseImage: config.Gitspace.DefaultBaseImage,
DefaultBindMountTargetPath: config.Gitspace.DefaultBindMountTargetPath,
DefaultBindMountSourceBasePath: bindMountSourceBasePath,
}, nil
}

View File

@ -1,22 +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 infraprovider
import "context"
type Client interface {
// Close closes the underlying client.
Close(ctx context.Context)
}

View File

@ -0,0 +1,97 @@
// 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 infraprovider
import (
"context"
"fmt"
"net/http"
"path/filepath"
"github.com/harness/gitness/infraprovider/enum"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
)
type DockerConfig struct {
DockerHost string
DockerAPIVersion string
DockerCertPath string
DockerTLSVerify string
}
type DockerClientFactory struct {
config *DockerConfig
}
func NewDockerClientFactory(config *DockerConfig) *DockerClientFactory {
return &DockerClientFactory{config: config}
}
// NewDockerClient returns a new docker client created using the docker config and infra.
func (d *DockerClientFactory) NewDockerClient(
_ context.Context,
infra *Infrastructure,
) (*client.Client, error) {
if infra.ProviderType != enum.InfraProviderTypeDocker {
return nil, fmt.Errorf("infra provider type %s not supported", infra.ProviderType)
}
dockerClient, err := d.getClient(infra.Parameters)
if err != nil {
return nil, fmt.Errorf("error creating docker client using infra %+v: %w", infra, err)
}
return dockerClient, nil
}
func (d *DockerClientFactory) getClient(_ []Parameter) (*client.Client, error) {
var opts []client.Opt
opts = append(opts, client.WithHost(d.config.DockerHost))
opts = append(opts, client.WithVersion(d.config.DockerAPIVersion))
if d.config.DockerCertPath != "" {
httpsClient, err := d.getHTTPSClient()
if err != nil {
return nil, fmt.Errorf("unable to create https client for docker client: %w", err)
}
opts = append(opts, client.WithHTTPClient(httpsClient))
}
dockerClient, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, fmt.Errorf("unable to create docker client: %w", err)
}
return dockerClient, nil
}
func (d *DockerClientFactory) getHTTPSClient() (*http.Client, error) {
options := tlsconfig.Options{
CAFile: filepath.Join(d.config.DockerCertPath, "ca.pem"),
CertFile: filepath.Join(d.config.DockerCertPath, "cert.pem"),
KeyFile: filepath.Join(d.config.DockerCertPath, "key.pem"),
InsecureSkipVerify: d.config.DockerTLSVerify == "",
}
tlsc, err := tlsconfig.Client(options)
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsc},
CheckRedirect: client.CheckRedirect,
}, nil
}

View File

@ -18,43 +18,42 @@ import (
"context"
"fmt"
"io"
"net/http"
"path/filepath"
"github.com/harness/gitness/infraprovider/enum"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/rs/zerolog/log"
)
var _ InfraProvider = (*DockerProvider)(nil)
type Config struct {
DockerHost string
DockerAPIVersion string
DockerCertPath string
DockerTLSVerify string
}
type DockerProvider struct {
config *Config
dockerClientFactory *DockerClientFactory
}
func NewDockerProvider(config *Config) *DockerProvider {
func NewDockerProvider(dockerClientFactory *DockerClientFactory) *DockerProvider {
return &DockerProvider{
config: config,
dockerClientFactory: dockerClientFactory,
}
}
// Provision assumes a docker engine is already running on the Gitness host machine and re-uses that as infra.
// It does not start docker engine.
func (d DockerProvider) Provision(ctx context.Context, _ string, params []Parameter) (Infrastructure, error) {
dockerClient, closeFunc, err := d.getClient(params)
dockerClient, err := d.dockerClientFactory.NewDockerClient(ctx, &Infrastructure{
ProviderType: enum.InfraProviderTypeDocker,
Parameters: params,
})
if err != nil {
return Infrastructure{}, err
return Infrastructure{}, fmt.Errorf("error getting docker client from docker client factory: %w", err)
}
defer closeFunc(ctx)
defer func() {
closingErr := dockerClient.Close()
if closingErr != nil {
log.Ctx(ctx).Warn().Err(closingErr).Msg("failed to close docker client")
}
}()
info, err := dockerClient.Info(ctx)
if err != nil {
return Infrastructure{}, fmt.Errorf("unable to connect to docker engine: %w", err)
@ -110,63 +109,3 @@ func (d DockerProvider) Exec(_ context.Context, _ Infrastructure, _ []string) (i
// TODO implement me
panic("implement me")
}
// Client returns a new docker client created using params.
func (d DockerProvider) Client(_ context.Context, infra Infrastructure) (Client, error) {
dockerClient, closeFunc, err := d.getClient(infra.Parameters)
if err != nil {
return nil, err
}
return &DockerClient{
dockerClient: dockerClient,
closeFunc: closeFunc,
}, nil
}
// getClient returns a new docker client created using values from gitness docker config.
func (d DockerProvider) getClient(_ []Parameter) (*client.Client, func(context.Context), error) {
var opts []client.Opt
opts = append(opts, client.WithHost(d.config.DockerHost))
opts = append(opts, client.WithVersion(d.config.DockerAPIVersion))
if d.config.DockerCertPath != "" {
httpsClient, err := d.getHTTPSClient()
if err != nil {
return nil, nil, fmt.Errorf("unable to create https client for docker client: %w", err)
}
opts = append(opts, client.WithHTTPClient(httpsClient))
}
dockerClient, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, nil, fmt.Errorf("unable to create docker client: %w", err)
}
closeFunc := func(ctx context.Context) {
closingErr := dockerClient.Close()
if closingErr != nil {
log.Ctx(ctx).Warn().Err(closingErr).Msg("failed to close docker client")
}
}
return dockerClient, closeFunc, nil
}
func (d DockerProvider) getHTTPSClient() (*http.Client, error) {
options := tlsconfig.Options{
CAFile: filepath.Join(d.config.DockerCertPath, "ca.pem"),
CertFile: filepath.Join(d.config.DockerCertPath, "cert.pem"),
KeyFile: filepath.Join(d.config.DockerCertPath, "key.pem"),
InsecureSkipVerify: d.config.DockerTLSVerify == "",
}
tlsc, err := tlsconfig.Client(options)
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsc},
CheckRedirect: client.CheckRedirect,
}, nil
}

View File

@ -42,7 +42,4 @@ type InfraProvider interface {
ProvisioningType() enum.InfraProvisioningType
// Exec executes a shell command in the infrastructure.
Exec(ctx context.Context, infra Infrastructure, cmd []string) (io.Reader, io.Reader, error)
// Client returns a client which can be used to connect the provided infra.
// The responsibility of calling the close func lies with the user.
Client(ctx context.Context, infra Infrastructure) (Client, error)
}

View File

@ -15,8 +15,6 @@
package infraprovider
import (
"github.com/harness/gitness/types"
"github.com/google/wire"
)
@ -24,18 +22,17 @@ import (
var WireSet = wire.NewSet(
ProvideDockerProvider,
ProvideFactory,
ProvideDockerClientFactory,
)
func ProvideDockerProvider(config *types.Config) *DockerProvider {
dockerConfig := Config{
DockerHost: config.Docker.Host,
DockerAPIVersion: config.Docker.APIVersion,
DockerCertPath: config.Docker.CertPath,
DockerTLSVerify: config.Docker.TLSVerify,
}
return NewDockerProvider(&dockerConfig)
func ProvideDockerProvider(dockerClientFactory *DockerClientFactory) *DockerProvider {
return NewDockerProvider(dockerClientFactory)
}
func ProvideFactory(dockerProvider *DockerProvider) Factory {
return NewFactory(dockerProvider)
}
func ProvideDockerClientFactory(config *DockerConfig) *DockerClientFactory {
return NewDockerClientFactory(config)
}

View File

@ -386,4 +386,22 @@ type Config struct {
// TLSVerify enables or disables TLS verification, off by default.
TLSVerify string `envconfig:"GITNESS_DOCKER_TLS_VERIFY"`
}
IDE struct {
VSCodeWeb struct {
// PortAndProtocol is the port on which the VS Code Web will be accessible.
Port string `envconfig:"GITNESS_IDE_VSCODEWEB_PORT" default:"8089"`
}
}
Gitspace struct {
// DefaultBaseImage is used to create the Gitspace when no devcontainer.json is absent or doesn't have image.
DefaultBaseImage string `envconfig:"GITNESS_GITSPACE_DEFAULT_BASE_IMAGE" default:"mcr.microsoft.com/devcontainers/base:dev-ubuntu-24.04"` //nolint:lll
// DefaultBindMountTargetPath is the target for bind mount in the Gitspace container.
DefaultBindMountTargetPath string `envconfig:"GITNESS_GITSPACE_DEFAULT_BIND_MOUNT_TARGET_PATH" default:"/gitspace"` //nolint:lll
// DefaultBindMountTargetPath is the source for bind mount in the Gitspace container.
// Sub-directories will be created from this eg <DefaultBindMountSourceBasePath>/gitspace/space1/space2/config1
// If left blank, it will be set to $HOME/.gitness
DefaultBindMountSourceBasePath string `envconfig:"GITNESS_GITSPACE_DEFAULT_BIND_MOUNT_SOURCE_BASE_PATH"`
}
}

View File

@ -18,9 +18,7 @@ type IDEType string
func (IDEType) Enum() []interface{} { return toInterfaceSlice(ideTypes) }
var ideTypes = ([]IDEType{
IDETypeVSCode, IDETypeVSCodeWeb,
})
var ideTypes = []IDEType{IDETypeVSCode, IDETypeVSCodeWeb}
const (
IDETypeVSCode IDEType = "vs_code"