feat: [CDE-94]: Adding orchestration to start, stop and delete Gitspaces. (#2159)

* Addressing review comments.
* feat: [CDE-94]: Rebasing PR. Adding StartResponse to store container orchestration start response. Refactoring provisioner to clean the method params.
* feat: [CDE-94]: Using ports returned by the container orchestrator
* feat: [CDE-94]: Adding orchestrtion to start, stop and delete Gitspaces.
unified-ui
Dhruv Dhruv 2024-07-08 20:10:44 +00:00 committed by Harness
parent b10fa3ea76
commit 34df7cf137
10 changed files with 372 additions and 60 deletions

View File

@ -28,35 +28,28 @@ type InfraProvisioner interface {
// stores the details in the db depending on the provisioning type.
Provision(
ctx context.Context,
spaceID int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
gitspaceInstanceID int64,
gitspaceInstanceIdentifier string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error)
// Stop unprovisions those resources which can be stopped without losing the gitspace data.
Stop(
ctx context.Context,
spaceID int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
gitspaceInstanceID int64,
gitspaceInstanceIdentifier string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error)
// Unprovision unprovisions all the resources created for the gitspace.
Unprovision(
ctx context.Context,
spaceID int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
gitspaceInstanceID int64,
gitspaceInstanceIdentifier string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error)
// Find finds the provisioned infra resources for the gitspace instance.
Find(
ctx context.Context,
spaceID int64,
infraProviderResource *types.InfraProviderResource,
gitspaceInstanceID int64,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error)
}

View File

@ -46,11 +46,8 @@ func NewInfraProvisionerService(
func (i infraProvisioner) Provision(
ctx context.Context,
_ int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
_ int64,
_ string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error) {
infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource)
if err != nil {
@ -83,11 +80,11 @@ func (i infraProvisioner) Provision(
return nil, fmt.Errorf("invalid provisioning params %v: %w", infraProviderResource.Metadata, err)
}
provisionedInfra, err := infraProvider.Provision(ctx, gitspaceConfigIdentifier, allParams)
provisionedInfra, err := infraProvider.Provision(ctx, gitspaceConfig.Identifier, allParams)
if err != nil {
return nil, fmt.Errorf(
"unable to provision infrastructure for gitspaceConfigIdentifier %v: %w",
gitspaceConfigIdentifier,
gitspaceConfig.Identifier,
err,
)
}
@ -101,11 +98,8 @@ func (i infraProvisioner) Provision(
func (i infraProvisioner) Stop(
ctx context.Context,
_ int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
_ int64,
_ string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error) {
infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource)
if err != nil {
@ -144,7 +138,7 @@ func (i infraProvisioner) Stop(
// TODO: Fetch and check existing infraProvisioned record
} else {
provisionedInfra = infraprovider.Infrastructure{
ResourceKey: gitspaceConfigIdentifier,
ResourceKey: gitspaceConfig.Identifier,
ProviderType: infraProviderEntity.Type,
Parameters: allParams,
}
@ -164,11 +158,8 @@ func (i infraProvisioner) Stop(
func (i infraProvisioner) Unprovision(
ctx context.Context,
_ int64,
infraProviderResource *types.InfraProviderResource,
gitspaceConfigIdentifier string,
_ int64,
_ string,
gitspaceConfig *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error) {
infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource)
if err != nil {
@ -207,7 +198,7 @@ func (i infraProvisioner) Unprovision(
// TODO: Fetch and check existing infraProvisioned record
} else {
provisionedInfra = infraprovider.Infrastructure{
ResourceKey: gitspaceConfigIdentifier,
ResourceKey: gitspaceConfig.Identifier,
ProviderType: infraProviderEntity.Type,
Parameters: allParams,
}
@ -227,9 +218,8 @@ func (i infraProvisioner) Unprovision(
func (i infraProvisioner) Find(
ctx context.Context,
_ int64,
infraProviderResource *types.InfraProviderResource,
_ int64,
_ *types.GitspaceConfig,
) (*infraprovider.Infrastructure, error) {
infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource)
if err != nil {

View File

@ -19,7 +19,6 @@ import (
"github.com/harness/gitness/infraprovider"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type Orchestrator interface {
@ -30,7 +29,7 @@ type Orchestrator interface {
gitspaceConfig *types.GitspaceConfig,
devcontainerConfig *types.DevcontainerConfig,
infra *infraprovider.Infrastructure,
) (map[enum.IDEType]string, error)
) (*StartResponse, error)
// StopGitspace stops and removes the gitspace container.
StopGitspace(ctx context.Context, gitspaceConfig *types.GitspaceConfig, infra *infraprovider.Infrastructure) error

View File

@ -84,7 +84,7 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace(
gitspaceConfig *types.GitspaceConfig,
devcontainerConfig *types.DevcontainerConfig,
infra *infraprovider.Infrastructure,
) (map[enum.IDEType]string, error) {
) (*StartResponse, error) {
containerName := getGitspaceContainerName(gitspaceConfig)
log := log.Ctx(ctx).With().Str(loggingKey, containerName).Logger()
@ -108,7 +108,7 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace(
}
var usedPorts map[enum.IDEType]string
var containerID string
switch state {
case containerStateRunning:
log.Debug().Msg("gitspace is already running")
@ -118,11 +118,13 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace(
return nil, startErr
}
ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService)
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...")
@ -143,10 +145,12 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace(
if startErr != nil {
return nil, fmt.Errorf("failed to start gitspace %s: %w", containerName, startErr)
}
ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService)
id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, ideService)
if startErr != nil {
return nil, startErr
}
containerID = id
usedPorts = ports
// TODO: Add gitspace status reporting.
@ -156,7 +160,11 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace(
return nil, fmt.Errorf("gitspace %s is in a bad state: %s", containerName, state)
}
return usedPorts, nil
return &StartResponse{
ContainerID: containerID,
ContainerName: containerName,
PortsUsed: usedPorts,
}, nil
}
func (e *EmbeddedDockerOrchestrator) startGitspace(
@ -211,15 +219,15 @@ func (e *EmbeddedDockerOrchestrator) startGitspace(
return nil
}
func (e *EmbeddedDockerOrchestrator) getUsedPorts(
func (e *EmbeddedDockerOrchestrator) getContainerInfo(
ctx context.Context,
containerName string,
dockerClient *client.Client,
ideService IDE,
) (map[enum.IDEType]string, error) {
) (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)
return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err)
}
usedPorts := map[enum.IDEType]string{}
@ -232,7 +240,7 @@ func (e *EmbeddedDockerOrchestrator) getUsedPorts(
}
}
return usedPorts, nil
return inspectResp.ID, usedPorts, nil
}
func (e *EmbeddedDockerOrchestrator) getIDEService(gitspaceConfig *types.GitspaceConfig) (IDE, error) {

View File

@ -0,0 +1,23 @@
// 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/types/enum"
type StartResponse struct {
ContainerID string
ContainerName string
PortsUsed map[enum.IDEType]string
}

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 orchestrator
import (
"context"
"github.com/harness/gitness/types"
)
type Orchestrator interface {
// StartGitspace is responsible for all the operations necessary to create the Gitspace container. It fetches the
// devcontainer.json from the code repo, provisions infra using the infra provisioner and setting up the Gitspace
// through the container orchestrator.
StartGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error)
// StopGitspace is responsible for stopping a running Gitspace. It stops the Gitspace container and unprovisions
// all the infra resources which are not required to restart the Gitspace.
StopGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error)
// DeleteGitspace is responsible for deleting a Gitspace. It stops the Gitspace container and unprovisions
// all the infra resources.
DeleteGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error)
}

View File

@ -0,0 +1,197 @@
// 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 orchestrator
import (
"context"
"fmt"
"net/url"
"time"
"github.com/harness/gitness/app/gitspace/infrastructure"
"github.com/harness/gitness/app/gitspace/orchestrator/container"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/guregu/null"
"github.com/rs/zerolog/log"
)
type orchestrator struct {
scm scm.SCM
infraProviderResourceStore store.InfraProviderResourceStore
infraProvisioner infrastructure.InfraProvisioner
containerOrchestrator container.Orchestrator
}
var _ Orchestrator = (*orchestrator)(nil)
func NewOrchestrator(
scm scm.SCM,
infraProviderResourceStore store.InfraProviderResourceStore,
infraProvisioner infrastructure.InfraProvisioner,
containerOrchestrator container.Orchestrator,
) Orchestrator {
return orchestrator{
scm: scm,
infraProviderResourceStore: infraProviderResourceStore,
infraProvisioner: infraProvisioner,
containerOrchestrator: containerOrchestrator,
}
}
func (o orchestrator) StartGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error) {
devcontainerConfig, err := o.scm.DevcontainerConfig(ctx, gitspaceConfig)
if err != nil {
log.Warn().Err(err).Msg("devcontainerConfig fetch failed.")
}
if devcontainerConfig == nil {
log.Warn().Err(err).Msg("devcontainerConfig is nil, using empty config")
devcontainerConfig = &types.DevcontainerConfig{}
}
infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID)
if err != nil {
return nil, fmt.Errorf("cannot get the infraProviderResource for ID %d: %w",
gitspaceConfig.InfraProviderResourceID, err)
}
infra, err := o.infraProvisioner.Provision(ctx, infraProviderResource, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf("cannot provision infrastructure for ID %d: %w",
gitspaceConfig.InfraProviderResourceID, err)
}
gitspaceInstance := gitspaceConfig.GitspaceInstance
err = o.containerOrchestrator.Status(ctx, infra)
gitspaceInstance.State = enum.GitspaceInstanceStateError
if err != nil {
return gitspaceInstance, fmt.Errorf("couldn't call the agent health API: %w", err)
}
startResponse, err := o.containerOrchestrator.StartGitspace(ctx, gitspaceConfig, devcontainerConfig, infra)
if err != nil {
return gitspaceInstance, fmt.Errorf("couldn't call the agent start API: %w", err)
}
repoName, err := o.scm.RepositoryName(ctx, gitspaceConfig)
if err != nil {
log.Warn().Err(err).Msg("failed to fetch repository name.")
}
port := startResponse.PortsUsed[gitspaceConfig.IDE]
var ideURL url.URL
if gitspaceConfig.IDE == enum.IDETypeVSCodeWeb {
ideURL = url.URL{
Scheme: "http",
Host: infra.Host + ":" + port,
RawQuery: "folder=/gitspace/" + repoName,
}
} else if gitspaceConfig.IDE == enum.IDETypeVSCode {
// TODO: the following user ID is hard coded and should be changed.
ideURL = url.URL{
Scheme: "vscode-remote",
Host: "", // Empty since we include the host and port in the path
Path: fmt.Sprintf("ssh-remote+%s@%s:%s/gitspace/%s", "harness", infra.Host, port, repoName),
}
}
gitspaceInstance.URL = null.NewString(ideURL.String(), true)
gitspaceInstance.LastUsed = time.Now().UnixMilli()
gitspaceInstance.State = enum.GitspaceInstanceStateRunning
return gitspaceInstance, nil
}
func (o orchestrator) StopGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error) {
infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID)
if err != nil {
return nil, fmt.Errorf(
"cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err)
}
infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf("cannot find the provisioned infra: %w", err)
}
err = o.containerOrchestrator.StopGitspace(ctx, gitspaceConfig, infra)
if err != nil {
return nil, fmt.Errorf("error stopping the Gitspace container: %w", err)
}
_, err = o.infraProvisioner.Stop(ctx, infraProviderResource, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf(
"cannot stop provisioned infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err)
}
gitspaceInstance := gitspaceConfig.GitspaceInstance
gitspaceInstance.State = enum.GitspaceInstanceStateDeleted
gitspaceInstance.URL = null.NewString("", false)
return gitspaceInstance, err
}
func (o orchestrator) DeleteGitspace(
ctx context.Context,
gitspaceConfig *types.GitspaceConfig,
) (*types.GitspaceInstance, error) {
var updatedGitspaceInstance *types.GitspaceInstance
gitspaceInstance := gitspaceConfig.GitspaceInstance
if gitspaceInstance.State == enum.GitspaceInstanceStateRunning ||
gitspaceInstance.State == enum.GitspaceInstanceStateUnknown {
infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID)
if err != nil {
return nil, fmt.Errorf(
"cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err)
}
infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf("cannot find the provisioned infra: %w", err)
}
err = o.containerOrchestrator.StopGitspace(ctx, gitspaceConfig, infra)
if err != nil {
return nil, fmt.Errorf("error stopping the Gitspace container: %w", err)
}
_, err = o.infraProvisioner.Unprovision(ctx, infraProviderResource, gitspaceConfig)
if err != nil {
return nil, fmt.Errorf(
"cannot stop provisioned infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err)
}
gitspaceInstance.State = enum.GitspaceInstanceStateDeleted
updatedGitspaceInstance = gitspaceInstance
}
return updatedGitspaceInstance, nil
}

View File

@ -0,0 +1,38 @@
// 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 orchestrator
import (
"github.com/harness/gitness/app/gitspace/infrastructure"
"github.com/harness/gitness/app/gitspace/orchestrator/container"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/store"
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideOrchestrator,
)
func ProvideOrchestrator(
scm scm.SCM,
infraProviderResourceStore store.InfraProviderResourceStore,
infraProvisioner infrastructure.InfraProvisioner,
containerOrchestrator container.Orchestrator,
) Orchestrator {
return NewOrchestrator(scm, infraProviderResourceStore, infraProvisioner, containerOrchestrator)
}

View File

@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"regexp"
"strings"
@ -36,6 +37,8 @@ var _ SCM = (*scm)(nil)
type SCM interface {
// DevcontainerConfig fetches devcontainer config file from the given repo and branch.
DevcontainerConfig(ctx context.Context, gitspaceConfig *types.GitspaceConfig) (*types.DevcontainerConfig, error)
// RepositoryName finds the repository name for the code repo URL from its provider.
RepositoryName(ctx context.Context, gitspaceConfig *types.GitspaceConfig) (string, error)
}
type scm struct{}
@ -128,6 +131,22 @@ func (s scm) DevcontainerConfig(
return &config, nil
}
// TODO: Make RepositoryName compatible with all SCM providers
func (s scm) RepositoryName(_ context.Context, gitspaceConfig *types.GitspaceConfig) (string, error) {
parsedURL, err := url.Parse(gitspaceConfig.CodeRepoURL)
if err != nil {
return "", fmt.Errorf("failed to parse url: %w", err)
}
pathSegments := strings.Split(parsedURL.Path, "/")
if len(pathSegments) < 3 || pathSegments[1] == "" || pathSegments[2] == "" {
return "", fmt.Errorf("invalid repository name URL: %s", parsedURL.String())
}
repoName := pathSegments[2]
return strings.ReplaceAll(repoName, ".git", ""), nil
}
func removeComments(input []byte) []byte {
blockCommentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`)
input = blockCommentRegex.ReplaceAll(input, nil)

View File

@ -45,23 +45,23 @@ type GitspaceConfig struct {
}
type GitspaceInstance struct {
ID int64 `json:"-"`
GitSpaceConfigID int64 `json:"-"`
Identifier string `json:"identifier"`
URL null.String `json:"url,omitempty"`
State enum.GitspaceStateType `json:"state"`
UserID string `json:"-"`
ResourceUsage null.String `json:"resource_usage"`
LastUsed int64 `json:"last_used,omitempty"`
TotalTimeUsed int64 `json:"total_time_used"`
TrackedChanges string `json:"tracked_changes"`
AccessKey null.String `json:"access_key,omitempty"`
AccessType enum.GitspaceAccessType `json:"access_type"`
MachineUser null.String `json:"machine_user,omitempty"`
SpacePath string `json:"space_path"`
SpaceID int64 `json:"-"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
ID int64 `json:"-"`
GitSpaceConfigID int64 `json:"-"`
Identifier string `json:"identifier"`
URL null.String `json:"url,omitempty"`
State enum.GitspaceInstanceStateType `json:"state"`
UserID string `json:"-"`
ResourceUsage null.String `json:"resource_usage"`
LastUsed int64 `json:"last_used,omitempty"`
TotalTimeUsed int64 `json:"total_time_used"`
TrackedChanges string `json:"tracked_changes"`
AccessKey null.String `json:"access_key,omitempty"`
AccessType enum.GitspaceAccessType `json:"access_type"`
MachineUser null.String `json:"machine_user,omitempty"`
SpacePath string `json:"space_path"`
SpaceID int64 `json:"-"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
}
type GitspaceFilter struct {