From b6034a25aa5850cf05a3e6494d5829e7b938d1fb Mon Sep 17 00:00:00 2001 From: Ansuman Satapathy Date: Fri, 26 Jul 2024 12:44:36 +0000 Subject: [PATCH] feat: [CDE-195]: read devcontainer.json directly for gitness (#2294) * feat: [CDE-195]: Run VS Code Web as non-root user. * feat: [CDE-195]: read devcontainer.json directly for gitness --- app/api/controller/gitspace/action.go | 17 ++ app/api/controller/gitspace/controller.go | 3 + app/api/controller/gitspace/create.go | 40 ++- app/api/controller/gitspace/events.go | 30 +-- app/api/controller/gitspace/wire.go | 2 + app/bootstrap/bootstrap.go | 44 ++++ .../container/container_orchestrator.go | 4 +- .../orchestrator/container/embedded_docker.go | 157 +++++++++--- .../orchestrator/devcontainer/devcontainer.go | 16 +- app/gitspace/orchestrator/ide/ide.go | 4 +- .../ide/script/install_vscode_web.sh | 5 + .../orchestrator/ide/script/run_vscode_web.sh | 5 - app/gitspace/orchestrator/ide/vscode.go | 8 +- app/gitspace/orchestrator/ide/vscodeweb.go | 44 ++-- .../orchestrator/orchestrator_impl.go | 9 +- .../orchestrator/template/template.go | 7 +- .../template/templates/authenticate_git.sh | 15 ++ .../template/templates/clone_git.sh | 20 +- .../template/templates/manage_user.sh | 24 ++ ...nstall_vscode_web.sh => run_vscode_web.sh} | 12 +- .../template/templates/setup_ssh_server.sh | 18 -- app/gitspace/scm/scm.go | 227 ++++++++++++++---- app/gitspace/scm/types.go | 33 +++ app/gitspace/scm/wire.go | 17 +- cmd/gitness/wire_gen.go | 4 +- types/config.go | 8 + types/enum/gitspace_cope_repo_type.go | 4 +- types/gitspace.go | 2 +- 28 files changed, 588 insertions(+), 191 deletions(-) create mode 100644 app/gitspace/orchestrator/ide/script/install_vscode_web.sh delete mode 100644 app/gitspace/orchestrator/ide/script/run_vscode_web.sh create mode 100644 app/gitspace/orchestrator/template/templates/authenticate_git.sh create mode 100644 app/gitspace/orchestrator/template/templates/manage_user.sh rename app/gitspace/orchestrator/template/templates/{install_vscode_web.sh => run_vscode_web.sh} (51%) create mode 100644 app/gitspace/scm/types.go diff --git a/app/api/controller/gitspace/action.go b/app/api/controller/gitspace/action.go index 1eaa08943..31eb37512 100644 --- a/app/api/controller/gitspace/action.go +++ b/app/api/controller/gitspace/action.go @@ -58,12 +58,29 @@ func (c *Controller) Action( if err != nil { return nil, fmt.Errorf("failed to authorize: %w", err) } + gitspaceConfig, err := c.gitspaceConfigStore.FindByIdentifier(ctx, space.ID, in.Identifier) gitspaceConfig.SpacePath = space.Path gitspaceConfig.SpaceID = space.ID if err != nil { return nil, fmt.Errorf("failed to find gitspace config: %w", err) } + + // check if it's an internal repo + if gitspaceConfig.CodeRepoType == enum.CodeRepoTypeGitness && gitspaceConfig.CodeRepoURL != "" { + repo, err := c.repoStore.FindByRef(ctx, gitspaceConfig.CodeRepoURL) + if err != nil { + return nil, fmt.Errorf("couldn't fetch repo for the user: %w", err) + } + if err = apiauth.CheckRepo( + ctx, + c.authorizer, + session, + repo, + enum.PermissionRepoView); err != nil { + return nil, err + } + } // All the actions should be idempotent. switch in.Action { case enum.GitspaceActionTypeStart: diff --git a/app/api/controller/gitspace/controller.go b/app/api/controller/gitspace/controller.go index 46f68f539..b78b9f781 100644 --- a/app/api/controller/gitspace/controller.go +++ b/app/api/controller/gitspace/controller.go @@ -37,6 +37,7 @@ type Controller struct { tx dbtx.Transactor statefulLogger *logutil.StatefulLogger scm scm.SCM + repoStore store.RepoStore } func NewController( @@ -51,6 +52,7 @@ func NewController( gitspaceEventStore store.GitspaceEventStore, statefulLogger *logutil.StatefulLogger, scm scm.SCM, + repoStore store.RepoStore, ) *Controller { return &Controller{ tx: tx, @@ -64,5 +66,6 @@ func NewController( gitspaceEventStore: gitspaceEventStore, statefulLogger: statefulLogger, scm: scm, + repoStore: repoStore, } } diff --git a/app/api/controller/gitspace/create.go b/app/api/controller/gitspace/create.go index 1282189a6..062221cab 100644 --- a/app/api/controller/gitspace/create.go +++ b/app/api/controller/gitspace/create.go @@ -44,16 +44,17 @@ var ( // CreateInput is the input used for create operations. type CreateInput struct { - Identifier string `json:"identifier"` - Name string `json:"name"` - SpaceRef string `json:"space_ref"` // Ref of the parent space - IDE enum.IDEType `json:"ide"` - ResourceIdentifier string `json:"resource_identifier"` - ResourceSpaceRef string `json:"resource_space_ref"` - CodeRepoURL string `json:"code_repo_url"` - Branch string `json:"branch"` - DevcontainerPath *string `json:"devcontainer_path"` - Metadata map[string]string `json:"metadata"` + Identifier string `json:"identifier"` + Name string `json:"name"` + SpaceRef string `json:"space_ref"` // Ref of the parent space + IDE enum.IDEType `json:"ide"` + ResourceIdentifier string `json:"resource_identifier"` + ResourceSpaceRef string `json:"resource_space_ref"` + CodeRepoURL string `json:"code_repo_url"` + CodeRepoType enum.GitspaceCodeRepoType `json:"code_repo_type"` + Branch string `json:"branch"` + DevcontainerPath *string `json:"devcontainer_path"` + Metadata map[string]string `json:"metadata"` } // Create creates a new gitspace. @@ -75,6 +76,21 @@ func (c *Controller) Create( enum.PermissionGitspaceEdit); err != nil { return nil, err } + // check if it's an internal repo + if in.CodeRepoType == enum.CodeRepoTypeGitness && in.CodeRepoURL != "" { + repo, err := c.repoStore.FindByRef(ctx, in.CodeRepoURL) + if err != nil { + return nil, fmt.Errorf("couldn't fetch repo for the user: %w", err) + } + if err = apiauth.CheckRepo( + ctx, + c.authorizer, + session, + repo, + enum.PermissionRepoView); err != nil { + return nil, err + } + } suffixUID, err := gonanoid.Generate(allowedUIDAlphabet, 6) if err != nil { return nil, fmt.Errorf("could not generate UID for gitspace config : %q %w", in.Identifier, err) @@ -86,7 +102,7 @@ func (c *Controller) Create( now := time.Now().UnixMilli() var gitspaceConfig *types.GitspaceConfig resourceIdentifier := in.ResourceIdentifier - // assume resource to be in same space if its not explicitly specified. + // assume resource to be in same space if it's not explicitly specified. if in.ResourceSpaceRef == "" { in.ResourceSpaceRef = in.SpaceRef } @@ -121,7 +137,7 @@ func (c *Controller) Create( IDE: in.IDE, InfraProviderResourceID: infraProviderResource.ID, InfraProviderResourceIdentifier: infraProviderResource.Identifier, - CodeRepoType: enum.CodeRepoTypeUnknown, + CodeRepoType: in.CodeRepoType, State: enum.GitspaceStateUninitialized, CodeRepoURL: in.CodeRepoURL, Branch: in.Branch, diff --git a/app/api/controller/gitspace/events.go b/app/api/controller/gitspace/events.go index 7e188418c..1f30f376b 100644 --- a/app/api/controller/gitspace/events.go +++ b/app/api/controller/gitspace/events.go @@ -73,29 +73,29 @@ func (c *Controller) Events( func eventsMessageMapping() map[enum.GitspaceEventType]string { var gitspaceConfigsMap = make(map[enum.GitspaceEventType]string) - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStart] = "Starting Gitspace..." - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStartCompleted] = "Started Gitspace" - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStartFailed] = "Starting Gitspace Failed" + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStart] = "Starting gitspace..." + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStartCompleted] = "Started gitspace" + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStartFailed] = "Starting gitspace failed" - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStop] = "Stopping Gitspace" - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStopCompleted] = "Stopped Gitspace" - gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStopFailed] = "Stopping Gitspace Failed" + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStop] = "Stopping gitspace" + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStopCompleted] = "Stopped gitspace" + gitspaceConfigsMap[enum.GitspaceEventTypeGitspaceActionStopFailed] = "Stopping gitspace failed" gitspaceConfigsMap[enum.GitspaceEventTypeFetchDevcontainerStart] = "Fetching devcontainer config..." gitspaceConfigsMap[enum.GitspaceEventTypeFetchDevcontainerCompleted] = "Fetched devcontainer config" gitspaceConfigsMap[enum.GitspaceEventTypeFetchDevcontainerFailed] = "Fetching devcontainer config failed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningStart] = "Provisioning Infrastructure..." - gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningCompleted] = "Provisioning Infrastructure Completed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningFailed] = "Provisioning Infrastructure Failed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningStart] = "Provisioning infrastructure..." + gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningCompleted] = "Provisioning infrastructure completed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraProvisioningFailed] = "Provisioning infrastructure failed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopStart] = "Stopping Infrastructure..." - gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopCompleted] = "Stopping Infrastructure Completed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopFailed] = "Stopping Infrastructure Failed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopStart] = "Stopping infrastructure..." + gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopCompleted] = "Stopping infrastructure completed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraStopFailed] = "Stopping infrastructure failed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningStart] = "Deprovisioning Infrastructure..." - gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningCompleted] = "Deprovisioning Infrastructure Completed" - gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningFailed] = "Deprovisioning Infrastructure Failed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningStart] = "Deprovisioning infrastructure..." + gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningCompleted] = "Deprovisioning infrastructure completed" + gitspaceConfigsMap[enum.GitspaceEventTypeInfraDeprovisioningFailed] = "Deprovisioning infrastructure failed" gitspaceConfigsMap[enum.GitspaceEventTypeAgentConnectStart] = "Connecting to the gitspace agent..." gitspaceConfigsMap[enum.GitspaceEventTypeAgentConnectCompleted] = "Connected to the gitspace agent" diff --git a/app/api/controller/gitspace/wire.go b/app/api/controller/gitspace/wire.go index cef13e244..ee0d2e5fa 100644 --- a/app/api/controller/gitspace/wire.go +++ b/app/api/controller/gitspace/wire.go @@ -44,6 +44,7 @@ func ProvideController( eventStore store.GitspaceEventStore, statefulLogger *logutil.StatefulLogger, scm scm.SCM, + repoStore store.RepoStore, ) *Controller { return NewController( tx, @@ -57,5 +58,6 @@ func ProvideController( eventStore, statefulLogger, scm, + repoStore, ) } diff --git a/app/bootstrap/bootstrap.go b/app/bootstrap/bootstrap.go index 6886b4adb..061f6e543 100644 --- a/app/bootstrap/bootstrap.go +++ b/app/bootstrap/bootstrap.go @@ -52,6 +52,17 @@ func NewPipelineServiceSession() *auth.Session { } } +// gitspaceServicePrincipal is the principal that is used during +// gitspace token injection for calling gitness APIs. +var gitspaceServicePrincipal *types.Principal + +func NewGitspaceServiceSession() *auth.Session { + return &auth.Session{ + Principal: *gitspaceServicePrincipal, + Metadata: &auth.EmptyMetadata{}, + } +} + // Bootstrap is an abstraction of a function that bootstraps a system. type Bootstrap func(context.Context) error @@ -65,6 +76,9 @@ func System(config *types.Config, userCtrl *user.Controller, if err := PipelineService(ctx, config, serviceCtrl); err != nil { return fmt.Errorf("failed to setup pipeline service: %w", err) } + if err := GitspaceService(ctx, config, serviceCtrl); err != nil { + return fmt.Errorf("failed to setup gitspace service: %w", err) + } if err := AdminUser(ctx, config, userCtrl); err != nil { return fmt.Errorf("failed to setup admin user: %w", err) @@ -196,6 +210,36 @@ func PipelineService( return nil } +// GitspaceService sets up the gitspace service principal that is used during +// gitspace credential injection for calling gitness APIs. +func GitspaceService( + ctx context.Context, + config *types.Config, + serviceCtrl *service.Controller, +) error { + svc, err := serviceCtrl.FindNoAuth(ctx, config.Principal.Gitspace.UID) + if errors.Is(err, store.ErrResourceNotFound) { + svc, err = createServicePrincipal( + ctx, + serviceCtrl, + config.Principal.Gitspace.UID, + config.Principal.Gitspace.Email, + config.Principal.Gitspace.DisplayName, + false, + ) + } + + if err != nil { + return fmt.Errorf("failed to setup gitspace service: %w", err) + } + + gitspaceServicePrincipal = svc.ToPrincipal() + + log.Ctx(ctx).Info().Msgf("Completed setup of gitspace service '%s' (id: %d).", svc.UID, svc.ID) + + return nil +} + func createServicePrincipal( ctx context.Context, serviceCtrl *service.Controller, diff --git a/app/gitspace/orchestrator/container/container_orchestrator.go b/app/gitspace/orchestrator/container/container_orchestrator.go index 4b2798ebb..f51ad9a19 100644 --- a/app/gitspace/orchestrator/container/container_orchestrator.go +++ b/app/gitspace/orchestrator/container/container_orchestrator.go @@ -18,6 +18,7 @@ import ( "context" "github.com/harness/gitness/app/gitspace/orchestrator/ide" + "github.com/harness/gitness/app/gitspace/scm" "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/types" ) @@ -29,9 +30,8 @@ type Orchestrator interface { CreateAndStartGitspace( ctx context.Context, gitspaceConfig *types.GitspaceConfig, - devcontainerConfig *types.DevcontainerConfig, infra *infraprovider.Infrastructure, - repoName string, + resolvedDetails *scm.ResolvedDetails, defaultBaseImage string, ideService ide.IDE, ) (*StartResponse, error) diff --git a/app/gitspace/orchestrator/container/embedded_docker.go b/app/gitspace/orchestrator/container/embedded_docker.go index a9727c91d..b1b73b9b1 100644 --- a/app/gitspace/orchestrator/container/embedded_docker.go +++ b/app/gitspace/orchestrator/container/embedded_docker.go @@ -25,6 +25,7 @@ import ( "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" @@ -40,12 +41,15 @@ import ( var _ Orchestrator = (*EmbeddedDockerOrchestrator)(nil) const ( - loggingKey = "gitspace.container" - catchAllIP = "0.0.0.0" - containerStateRunning = "running" - containerStateRemoved = "removed" - containerStateStopped = "exited" - templateCloneGit = "clone_git.sh" + 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 { @@ -70,9 +74,8 @@ func NewEmbeddedDockerOrchestrator( func (e *EmbeddedDockerOrchestrator) CreateAndStartGitspace( ctx context.Context, gitspaceConfig *types.GitspaceConfig, - devcontainerConfig *types.DevcontainerConfig, infra *infraprovider.Infrastructure, - repoName string, + resolvedRepoDetails *scm.ResolvedDetails, defaultBaseImage string, ideService ide.IDE, ) (*StartResponse, error) { @@ -122,12 +125,18 @@ func (e *EmbeddedDockerOrchestrator) CreateAndStartGitspace( return nil, startErr } - devcontainer := &devcontainer.Devcontainer{ + devcontainer := &devcontainer.Exec{ ContainerName: containerName, - WorkingDir: e.getWorkingDir(repoName), + 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 @@ -154,13 +163,13 @@ func (e *EmbeddedDockerOrchestrator) CreateAndStartGitspace( startErr := e.startGitspace( ctx, gitspaceConfig, - devcontainerConfig, containerName, dockerClient, ideService, logStreamInstance, infra.Storage, - e.getWorkingDir(repoName), + e.getWorkingDir(resolvedRepoDetails.RepoName), + resolvedRepoDetails, infra.PortMappings, defaultBaseImage, ) @@ -194,17 +203,17 @@ func (e *EmbeddedDockerOrchestrator) getWorkingDir(repoName string) string { func (e *EmbeddedDockerOrchestrator) startGitspace( ctx context.Context, gitspaceConfig *types.GitspaceConfig, - devcontainerConfig *types.DevcontainerConfig, containerName string, dockerClient *client.Client, ideService ide.IDE, logStreamInstance *logutil.LogStreamInstance, volumeName string, workingDirectory string, + resolvedRepoDetails *scm.ResolvedDetails, portMappings map[int]*infraprovider.PortMapping, defaultBaseImage string, ) error { - var imageName = devcontainerConfig.Image + var imageName = resolvedRepoDetails.DevcontainerConfig.Image if imageName == "" { imageName = defaultBaseImage } @@ -233,12 +242,17 @@ func (e *EmbeddedDockerOrchestrator) startGitspace( return err } - var devcontainer = &devcontainer.Devcontainer{ + 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 @@ -249,12 +263,12 @@ func (e *EmbeddedDockerOrchestrator) startGitspace( return err } - err = e.cloneCode(ctx, gitspaceConfig, devcontainer, defaultBaseImage, logStreamInstance) + err = e.cloneCode(ctx, devcontainer, defaultBaseImage, logStreamInstance, resolvedRepoDetails) if err != nil { return err } - err = e.executePostCreateCommand(ctx, devcontainerConfig, devcontainer, logStreamInstance) + err = e.executePostCreateCommand(ctx, resolvedRepoDetails.DevcontainerConfig, devcontainer, logStreamInstance) if err != nil { return err } @@ -266,7 +280,7 @@ func (e *EmbeddedDockerOrchestrator) startGitspace( func (e *EmbeddedDockerOrchestrator) runIDE( ctx context.Context, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, ideService ide.IDE, logStreamInstance *logutil.LogStreamInstance, ) error { @@ -304,7 +318,7 @@ func (e *EmbeddedDockerOrchestrator) runIDE( func (e *EmbeddedDockerOrchestrator) setupIDE( ctx context.Context, gitspaceInstance *types.GitspaceInstance, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, ideService ide.IDE, logStreamInstance *logutil.LogStreamInstance, ) error { @@ -366,30 +380,103 @@ func (e *EmbeddedDockerOrchestrator) getContainerInfo( return inspectResp.ID, usedPorts, nil } -func (e *EmbeddedDockerOrchestrator) cloneCode( +func (e *EmbeddedDockerOrchestrator) authenticateGit( ctx context.Context, - gitspaceConfig *types.GitspaceConfig, - devcontainer *devcontainer.Devcontainer, - defaultBaseImage string, - logStreamInstance *logutil.LogStreamInstance, + devcontainer *devcontainer.Exec, + resolvedRepoDetails *scm.ResolvedDetails, ) error { - gitCloneScript, err := template.GenerateScriptFromTemplate( - templateCloneGit, &template.CloneGitPayload{ - RepoURL: gitspaceConfig.CodeRepoURL, - Image: defaultBaseImage, - Branch: gitspaceConfig.Branch, - }) + data := &template.AuthenticateGitPayload{ + Password: resolvedRepoDetails.Credentials.Password, + } + gitAuthenticateScript, err := template.GenerateScriptFromTemplate( + templateAuthenticateGit, data) if err != nil { - return fmt.Errorf("failed to generate scipt to clone git from template %s: %w", templateCloneGit, err) + 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( - "Cloning git repo inside container: " + gitspaceConfig.CodeRepoURL + " branch: " + gitspaceConfig.Branch) + "creating user inside container: " + data.Username) if loggingErr != nil { return fmt.Errorf("logging error: %w", loggingErr) } - output, err := devcontainer.ExecuteCommand(ctx, gitCloneScript, false) + 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.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()) @@ -418,7 +505,7 @@ func (e *EmbeddedDockerOrchestrator) cloneCode( func (e *EmbeddedDockerOrchestrator) executePostCreateCommand( ctx context.Context, devcontainerConfig *types.DevcontainerConfig, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, logStreamInstance *logutil.LogStreamInstance, ) error { if devcontainerConfig.PostCreateCommand == "" { @@ -435,7 +522,7 @@ func (e *EmbeddedDockerOrchestrator) executePostCreateCommand( return fmt.Errorf("logging error: %w", loggingErr) } - output, err := devcontainer.ExecuteCommand(ctx, devcontainerConfig.PostCreateCommand, false) + output, err := devcontainer.ExecuteCommand(ctx, devcontainerConfig.PostCreateCommand, false, harnessUser) if err != nil { loggingErr = logStreamInstance.Write("Error while executing postCreate command") diff --git a/app/gitspace/orchestrator/devcontainer/devcontainer.go b/app/gitspace/orchestrator/devcontainer/devcontainer.go index 6482d8a07..bd2104bb9 100644 --- a/app/gitspace/orchestrator/devcontainer/devcontainer.go +++ b/app/gitspace/orchestrator/devcontainer/devcontainer.go @@ -23,32 +23,32 @@ import ( "github.com/docker/docker/client" ) -type Devcontainer struct { +type Exec struct { ContainerName string WorkingDir string DockerClient *client.Client } -func (d *Devcontainer) ExecuteCommand(ctx context.Context, command string, detach bool) ([]byte, error) { +func (e *Exec) ExecuteCommand(ctx context.Context, command string, detach bool, userName string) ([]byte, error) { cmd := []string{"/bin/sh", "-c", command} execConfig := dockerTypes.ExecConfig{ - User: "root", + User: userName, AttachStdout: true, AttachStderr: true, Cmd: cmd, Detach: detach, - WorkingDir: d.WorkingDir, + WorkingDir: e.WorkingDir, } - execID, err := d.DockerClient.ContainerExecCreate(ctx, d.ContainerName, execConfig) + execID, err := e.DockerClient.ContainerExecCreate(ctx, e.ContainerName, execConfig) if err != nil { - return nil, fmt.Errorf("failed to create docker exec for container %s: %w", d.ContainerName, err) + return nil, fmt.Errorf("failed to create docker exec for container %s: %w", e.ContainerName, err) } - execResponse, err := d.DockerClient.ContainerExecAttach(ctx, execID.ID, dockerTypes.ExecStartCheck{Detach: detach}) + execResponse, err := e.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) + return nil, fmt.Errorf("failed to start docker exec for container %s: %w", e.ContainerName, err) } if execResponse.Conn != nil { diff --git a/app/gitspace/orchestrator/ide/ide.go b/app/gitspace/orchestrator/ide/ide.go index 51e4af447..51389e709 100644 --- a/app/gitspace/orchestrator/ide/ide.go +++ b/app/gitspace/orchestrator/ide/ide.go @@ -27,12 +27,12 @@ type IDE interface { // copying settings and configurations. Setup( ctx context.Context, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, gitspaceInstance *types.GitspaceInstance, ) ([]byte, error) // Run runs the IDE and supporting services. - Run(ctx context.Context, devcontainer *devcontainer.Devcontainer) ([]byte, error) + Run(ctx context.Context, devcontainer *devcontainer.Exec) ([]byte, error) // Port provides the port which will be used by this IDE. Port() int diff --git a/app/gitspace/orchestrator/ide/script/install_vscode_web.sh b/app/gitspace/orchestrator/ide/script/install_vscode_web.sh new file mode 100644 index 000000000..2a06f2d18 --- /dev/null +++ b/app/gitspace/orchestrator/ide/script/install_vscode_web.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "Installing VSCode Web" + +curl -fsSL https://code-server.dev/install.sh | sh \ No newline at end of file diff --git a/app/gitspace/orchestrator/ide/script/run_vscode_web.sh b/app/gitspace/orchestrator/ide/script/run_vscode_web.sh deleted file mode 100644 index 3b9305d65..000000000 --- a/app/gitspace/orchestrator/ide/script/run_vscode_web.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -echo "Running VSCode Web" - -code-server \ No newline at end of file diff --git a/app/gitspace/orchestrator/ide/vscode.go b/app/gitspace/orchestrator/ide/vscode.go index f5d281ab0..1e8d49d1a 100644 --- a/app/gitspace/orchestrator/ide/vscode.go +++ b/app/gitspace/orchestrator/ide/vscode.go @@ -43,7 +43,7 @@ func NewVsCodeService() *VSCode { // Setup installs the SSH server inside the container. func (v *VSCode) Setup( ctx context.Context, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, gitspaceInstance *types.GitspaceInstance, ) ([]byte, error) { sshServerScript, err := template.GenerateScriptFromTemplate( @@ -59,7 +59,7 @@ func (v *VSCode) Setup( output := "Installing ssh-server inside container\n" - _, err = devcontainer.ExecuteCommand(ctx, sshServerScript, false) + _, err = devcontainer.ExecuteCommand(ctx, sshServerScript, false, rootUser) if err != nil { return nil, fmt.Errorf("failed to setup SSH serverr: %w", err) } @@ -70,10 +70,10 @@ func (v *VSCode) Setup( } // Run runs the SSH server inside the container. -func (v *VSCode) Run(ctx context.Context, devcontainer *devcontainer.Devcontainer) ([]byte, error) { +func (v *VSCode) Run(ctx context.Context, devcontainer *devcontainer.Exec) ([]byte, error) { var output = "" - execOutput, err := devcontainer.ExecuteCommand(ctx, runSSHScript, false) + execOutput, err := devcontainer.ExecuteCommand(ctx, runSSHScript, false, rootUser) if err != nil { return nil, fmt.Errorf("failed to run SSH serverr: %w", err) } diff --git a/app/gitspace/orchestrator/ide/vscodeweb.go b/app/gitspace/orchestrator/ide/vscodeweb.go index a35d1db56..808d3dc67 100644 --- a/app/gitspace/orchestrator/ide/vscodeweb.go +++ b/app/gitspace/orchestrator/ide/vscodeweb.go @@ -35,8 +35,8 @@ import ( var _ IDE = (*VSCodeWeb)(nil) -//go:embed script/run_vscode_web.sh -var runScript string +//go:embed script/install_vscode_web.sh +var installScript string //go:embed script/find_vscode_web_path.sh var findPathScript string @@ -44,9 +44,11 @@ var findPathScript string //go:embed media/vscodeweb/* var mediaFiles embed.FS -const templateInstallVSCodeWeb = "install_vscode_web.sh" +const templateRunVSCodeWeb = "run_vscode_web.sh" const startMarker = "START_MARKER" const endMarker = "END_MARKER" +const rootUser = "root" +const harnessUser = "harness" type VSCodeWebConfig struct { Port int @@ -63,29 +65,17 @@ func NewVsCodeWebService(config *VSCodeWebConfig) *VSCodeWeb { // Setup runs the installScript which downloads the required version of the code-server binary. func (v *VSCodeWeb) Setup( ctx context.Context, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, _ *types.GitspaceInstance, ) ([]byte, error) { - installScript, err := template.GenerateScriptFromTemplate( - templateInstallVSCodeWeb, &template.InstallVSCodeWebPayload{ - Port: strconv.Itoa(v.config.Port), - }) - if err != nil { - return nil, fmt.Errorf( - "failed to generate scipt to install VSCode Web from template %s: %w", - templateInstallVSCodeWeb, - err, - ) - } - output := "Installing VSCode Web inside container.\n" - _, err = devcontainer.ExecuteCommand(ctx, installScript, false) + _, err := devcontainer.ExecuteCommand(ctx, installScript, false, rootUser) if err != nil { return nil, fmt.Errorf("failed to install VSCode Web: %w", err) } - findOutput, err := devcontainer.ExecuteCommand(ctx, findPathScript, false) + findOutput, err := devcontainer.ExecuteCommand(ctx, findPathScript, false, rootUser) if err != nil { return nil, fmt.Errorf("failed to find VSCode Web install path: %w", err) } @@ -109,10 +99,22 @@ func (v *VSCodeWeb) Setup( } // Run runs the code-server binary. -func (v *VSCodeWeb) Run(ctx context.Context, devcontainer *devcontainer.Devcontainer) ([]byte, error) { +func (v *VSCodeWeb) Run(ctx context.Context, devcontainer *devcontainer.Exec) ([]byte, error) { var output []byte - _, err := devcontainer.ExecuteCommand(ctx, runScript, true) + runScript, err := template.GenerateScriptFromTemplate( + templateRunVSCodeWeb, &template.RunVSCodeWebPayload{ + Port: strconv.Itoa(v.config.Port), + }) + if err != nil { + return nil, fmt.Errorf( + "failed to generate scipt to run VSCode Web from template %s: %w", + templateRunVSCodeWeb, + err, + ) + } + + _, err = devcontainer.ExecuteCommand(ctx, runScript, true, harnessUser) if err != nil { return nil, fmt.Errorf("failed to run VSCode Web: %w", err) } @@ -130,7 +132,7 @@ func (v *VSCodeWeb) Type() enum.IDEType { func (v *VSCodeWeb) copyMediaToContainer( ctx context.Context, - devcontainer *devcontainer.Devcontainer, + devcontainer *devcontainer.Exec, path string, ) error { // Create a buffer to hold the tar data diff --git a/app/gitspace/orchestrator/orchestrator_impl.go b/app/gitspace/orchestrator/orchestrator_impl.go index 020f9268b..67882e127 100644 --- a/app/gitspace/orchestrator/orchestrator_impl.go +++ b/app/gitspace/orchestrator/orchestrator_impl.go @@ -83,16 +83,17 @@ func (o orchestrator) StartGitspace( o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerStart) - repoName, devcontainerConfig, err := o.scm.RepoNameAndDevcontainerConfig(ctx, gitspaceConfig) + scmResolvedDetails, err := o.scm.Resolve(ctx, gitspaceConfig) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerFailed) - return fmt.Errorf("failed to fetch code repo details for gitspace config ID %d", gitspaceConfig.ID) + return fmt.Errorf("failed to fetch code repo details for gitspace config ID %w %d", err, gitspaceConfig.ID) } + devcontainerConfig := scmResolvedDetails.DevcontainerConfig + repoName := scmResolvedDetails.RepoName if devcontainerConfig == nil { log.Warn().Err(err).Msg("devcontainer config is nil, using empty config") - devcontainerConfig = &types.DevcontainerConfig{} } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerCompleted) @@ -137,7 +138,7 @@ func (o orchestrator) StartGitspace( o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationStart) startResponse, err := o.containerOrchestrator.CreateAndStartGitspace( - ctx, gitspaceConfig, devcontainerConfig, infra, repoName, o.config.DefaultBaseImage, ideSvc) + ctx, gitspaceConfig, infra, scmResolvedDetails, o.config.DefaultBaseImage, ideSvc) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationFailed) diff --git a/app/gitspace/orchestrator/template/template.go b/app/gitspace/orchestrator/template/template.go index b7531b6d1..35db1e988 100644 --- a/app/gitspace/orchestrator/template/template.go +++ b/app/gitspace/orchestrator/template/template.go @@ -37,9 +37,14 @@ type CloneGitPayload struct { RepoURL string Image string Branch string + AuthenticateGitPayload } -type InstallVSCodeWebPayload struct { +type AuthenticateGitPayload struct { + Password string +} + +type RunVSCodeWebPayload struct { Port string } diff --git a/app/gitspace/orchestrator/template/templates/authenticate_git.sh b/app/gitspace/orchestrator/template/templates/authenticate_git.sh new file mode 100644 index 000000000..8a0d12e7b --- /dev/null +++ b/app/gitspace/orchestrator/template/templates/authenticate_git.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +password={{ .Password }} + +# Create or overwrite the config file with new settings +touch $HOME/.git-askpass +cat > $HOME/.git-askpass < $HOME/.git-askpass </dev/null 2>&1; then echo "Git is not installed. Installing Git..." @@ -18,20 +28,20 @@ if ! command -v git >/dev/null 2>&1; then echo "Git is not installed. Exiting..." exit 1 fi - +git config --global --add safe.directory /$repo_name # Clone the repository inside the working directory if it doesn't exist if [ ! -d ".git" ]; then echo "Cloning the repository..." - git clone "$repo_url" --branch "$branch" . +git clone "$repo_url" --branch "$branch" /$repo_name else echo "Repository already exists. Skipping clone." fi - +rm $HOME/.git-askpass # Check if .devcontainer/devcontainer.json exists if [ ! -f ".devcontainer/devcontainer.json" ]; then echo "Creating .devcontainer directory and devcontainer.json..." - mkdir -p ".devcontainer" - cat < ".devcontainer/devcontainer.json" + mkdir -p /$repo_name/.devcontainer + cat < /$repo_name/.devcontainer/devcontainer.json { "image": "$image" } diff --git a/app/gitspace/orchestrator/template/templates/manage_user.sh b/app/gitspace/orchestrator/template/templates/manage_user.sh new file mode 100644 index 000000000..e35f65aa7 --- /dev/null +++ b/app/gitspace/orchestrator/template/templates/manage_user.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +username={{ .Username }} +password={{ .Password }} +workingDir={{ .WorkingDirectory }} + +# Check if the user already exists +if id "$username" >/dev/null 2>&1; then + echo "User $username already exists." +else + # Create a new user + adduser --disabled-password --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 + +# Changing ownership of everything inside user home to the newly created user +chown -R $username $workingDir +echo "Changing ownership of dir $workingDir to user $username." \ No newline at end of file diff --git a/app/gitspace/orchestrator/template/templates/install_vscode_web.sh b/app/gitspace/orchestrator/template/templates/run_vscode_web.sh similarity index 51% rename from app/gitspace/orchestrator/template/templates/install_vscode_web.sh rename to app/gitspace/orchestrator/template/templates/run_vscode_web.sh index a73dceb0d..e7a25da42 100644 --- a/app/gitspace/orchestrator/template/templates/install_vscode_web.sh +++ b/app/gitspace/orchestrator/template/templates/run_vscode_web.sh @@ -1,17 +1,17 @@ #!/bin/sh -echo "Installing VSCode Web" - -curl -fsSL https://code-server.dev/install.sh | sh +echo "Running VSCode Web" port={{ .Port }} # Ensure the configuration directory exists -mkdir -p /root/.config/code-server +mkdir -p $HOME/.config/code-server # Create or overwrite the config file with new settings -cat > /root/.config/code-server/config.yaml < $HOME/.config/code-server/config.yaml </dev/null 2>&1; 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 @@ -45,7 +30,4 @@ 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 \ No newline at end of file diff --git a/app/gitspace/scm/scm.go b/app/gitspace/scm/scm.go index 245a0f6d4..440e44d1a 100644 --- a/app/gitspace/scm/scm.go +++ b/app/gitspace/scm/scm.go @@ -26,9 +26,18 @@ import ( "path" "regexp" "strings" + "time" + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/bootstrap" + "github.com/harness/gitness/app/jwt" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/app/token" + urlprovider "github.com/harness/gitness/app/url" + "github.com/harness/gitness/git" "github.com/harness/gitness/git/command" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" "github.com/google/uuid" "github.com/rs/zerolog/log" @@ -38,23 +47,49 @@ var ( ErrNoDefaultBranch = errors.New("no default branch") ) -var _ SCM = (*scm)(nil) +var gitspaceJWTLifetime = 720 * 24 * time.Hour + +const defaultGitspacePATIdentifier = "Gitspace_Default" + +var _ SCM = (*genericSCM)(nil) type SCM interface { // RepoNameAndDevcontainerConfig fetches repository name & devcontainer config file from the given repo and branch. - RepoNameAndDevcontainerConfig( + Resolve( ctx context.Context, gitspaceConfig *types.GitspaceConfig, - ) (string, *types.DevcontainerConfig, error) + ) (*ResolvedDetails, error) // CheckValidCodeRepo checks if the current URL is a valid and accessible code repo, // input can be connector info, user token etc. CheckValidCodeRepo(ctx context.Context, request CodeRepositoryRequest) (*CodeRepositoryResponse, error) } -type scm struct{} +type genericSCM struct { + git git.Interface + repoStore store.RepoStore + tokenStore store.TokenStore + principalStore store.PrincipalStore + urlProvider urlprovider.Provider +} -func (s scm) CheckValidCodeRepo(ctx context.Context, request CodeRepositoryRequest) (*CodeRepositoryResponse, error) { +func NewSCM(repoStore store.RepoStore, git git.Interface, + tokenStore store.TokenStore, + principalStore store.PrincipalStore, + urlProvider urlprovider.Provider) SCM { + return &genericSCM{ + repoStore: repoStore, + git: git, + tokenStore: tokenStore, + principalStore: principalStore, + urlProvider: urlProvider, + } +} + +func (s genericSCM) CheckValidCodeRepo( + ctx context.Context, + request CodeRepositoryRequest, +) (*CodeRepositoryResponse, error) { err := validateURL(request) if err != nil { return nil, fmt.Errorf("invalid URL, %w", err) @@ -78,27 +113,24 @@ func (s scm) CheckValidCodeRepo(ctx context.Context, request CodeRepositoryReque return codeRepositoryResponse, nil } -func NewSCM() SCM { - return &scm{} -} - -func (s scm) RepoNameAndDevcontainerConfig( +func (s genericSCM) Resolve( ctx context.Context, gitspaceConfig *types.GitspaceConfig, -) (string, *types.DevcontainerConfig, error) { +) (*ResolvedDetails, error) { + resolvedDetails := &ResolvedDetails{Branch: gitspaceConfig.Branch, CloneURL: gitspaceConfig.CodeRepoURL} repoURL, err := url.Parse(gitspaceConfig.CodeRepoURL) if err != nil { - return "", nil, fmt.Errorf("failed to parse repository URL %s: %w", gitspaceConfig.CodeRepoURL, err) + return nil, fmt.Errorf("failed to parse repository URL %s: %w", gitspaceConfig.CodeRepoURL, err) } repoName := strings.TrimSuffix(path.Base(repoURL.Path), ".git") - + resolvedDetails.RepoName = repoName gitWorkingDirectory := "/tmp/git/" cloneDir := gitWorkingDirectory + uuid.New().String() err = os.MkdirAll(cloneDir, os.ModePerm) if err != nil { - return "", nil, fmt.Errorf("error creating directory %s: %w", cloneDir, err) + return nil, fmt.Errorf("error creating directory %s: %w", cloneDir, err) } defer func() { @@ -109,11 +141,90 @@ func (s scm) RepoNameAndDevcontainerConfig( }() filePath := ".devcontainer/devcontainer.json" - err = validateArgs(gitspaceConfig) + var catFileOutputBytes []byte + switch gitspaceConfig.CodeRepoType { //nolint:exhaustive + case enum.CodeRepoTypeGitness: + repo, err := s.repoStore.FindByRef(ctx, gitspaceConfig.CodeRepoURL) + + // Backfill clone URL + repo.GitURL = s.urlProvider.GenerateContainerGITCloneURL(ctx, repo.Path) + if err != nil { + return nil, fmt.Errorf("failed to find repository: %w", err) + } + resolvedDetails.CloneURL = repo.GitURL + catFileOutputBytes, err = s.getDevContainerConfigInternal(ctx, gitspaceConfig, filePath, repo) + if err != nil { + return nil, fmt.Errorf("failed to read devcontainer file : %w", err) + } + netrc, err := s.gitnessCredentials(ctx, repo, gitspaceConfig.UserID) + if err != nil { + return nil, fmt.Errorf("failed to resolve repo credentials: %w", err) + } + resolvedDetails.Credentials = netrc + default: + catFileOutputBytes, err = s.getDevContainerConfigPublic(ctx, gitspaceConfig, cloneDir, filePath) + if err != nil { + return nil, err + } + } + if len(catFileOutputBytes) == 0 { + resolvedDetails.DevcontainerConfig = &types.DevcontainerConfig{} + return resolvedDetails, nil + } + sanitizedJSON := removeComments(catFileOutputBytes) + var config *types.DevcontainerConfig + err = json.Unmarshal(sanitizedJSON, &config) if err != nil { - return "", nil, fmt.Errorf("invalid branch or url: %w", err) + return nil, fmt.Errorf("failed to parse devcontainer json: %w", err) + } + resolvedDetails.DevcontainerConfig = config + return resolvedDetails, nil +} + +func (s genericSCM) getDevContainerConfigInternal(ctx context.Context, + gitspaceConfig *types.GitspaceConfig, + filePath string, + repo *types.Repository, +) ([]byte, error) { + // create read params once + readParams := git.CreateReadParams(repo) + treeNodeOutput, err := s.git.GetTreeNode(ctx, &git.GetTreeNodeParams{ + ReadParams: readParams, + GitREF: gitspaceConfig.Branch, + Path: filePath, + IncludeLatestCommit: false, + }) + if err != nil { + return nil, fmt.Errorf("failed to read tree node: %w", err) } + // viewing Raw content is only supported for blob content + if treeNodeOutput.Node.Type != git.TreeNodeTypeBlob { + return nil, usererror.BadRequestf( + "Object in '%s' at '/%s' is of type '%s'. Only objects of type %s support raw viewing.", + gitspaceConfig.Branch, filePath, treeNodeOutput.Node.Type, git.TreeNodeTypeBlob) + } + + blobReader, err := s.git.GetBlob(ctx, &git.GetBlobParams{ + ReadParams: readParams, + SHA: treeNodeOutput.Node.SHA, + SizeLimit: 0, // no size limit, we stream whatever data there is + }) + if err != nil { + return nil, fmt.Errorf("failed to read blob: %w", err) + } + catFileOutput, err := io.ReadAll(blobReader.Content) + if err != nil { + return nil, fmt.Errorf("failed to read blob content: %w", err) + } + return catFileOutput, nil +} + +func (s genericSCM) getDevContainerConfigPublic(ctx context.Context, + gitspaceConfig *types.GitspaceConfig, + cloneDir string, + filePath string, +) ([]byte, error) { log.Info().Msg("Cloning the repository...") cmd := command.New("clone", command.WithFlag("--branch", gitspaceConfig.Branch), @@ -122,12 +233,8 @@ func (s scm) RepoNameAndDevcontainerConfig( command.WithArg(gitspaceConfig.CodeRepoURL), command.WithArg(cloneDir), ) - err = cmd.Run( - ctx, - command.WithDir(cloneDir), - ) - if err != nil { - return "", nil, fmt.Errorf("failed to clone repository %s: %w", gitspaceConfig.CodeRepoURL, err) + if err := cmd.Run(ctx, command.WithDir(cloneDir)); err != nil { + return nil, fmt.Errorf("failed to clone repository %s: %w", gitspaceConfig.CodeRepoURL, err) } var lsTreeOutput bytes.Buffer @@ -135,19 +242,14 @@ func (s scm) RepoNameAndDevcontainerConfig( command.WithArg("HEAD"), command.WithArg(filePath), ) - err = lsTreeCmd.Run( - ctx, - command.WithDir(cloneDir), - command.WithStdout(&lsTreeOutput), - ) - if err != nil { - return "", nil, fmt.Errorf("failed to list files in repository %s: %w", cloneDir, err) + + if err := lsTreeCmd.Run(ctx, command.WithDir(cloneDir), command.WithStdout(&lsTreeOutput)); err != nil { + return nil, fmt.Errorf("failed to list files in repository %s: %w", cloneDir, err) } if lsTreeOutput.Len() == 0 { log.Info().Msg("File not found, returning empty devcontainerConfig") - emptyConfig := &types.DevcontainerConfig{} - return repoName, emptyConfig, nil + return nil, nil } fields := strings.Fields(lsTreeOutput.String()) @@ -155,25 +257,16 @@ func (s scm) RepoNameAndDevcontainerConfig( var catFileOutput bytes.Buffer catFileCmd := command.New("cat-file", command.WithFlag("-p"), command.WithArg(blobSHA)) - err = catFileCmd.Run( + err := catFileCmd.Run( ctx, command.WithDir(cloneDir), command.WithStderr(io.Discard), command.WithStdout(&catFileOutput), ) if err != nil { - return "", nil, fmt.Errorf("failed to read devcontainer file from path %s: %w", filePath, err) + return nil, fmt.Errorf("failed to read devcontainer file from path %s: %w", filePath, err) } - - sanitizedJSON := removeComments(catFileOutput.Bytes()) - - var config types.DevcontainerConfig - err = json.Unmarshal(sanitizedJSON, &config) - if err != nil { - return "", nil, fmt.Errorf("failed to parse devcontainer json: %w", err) - } - - return repoName, &config, nil + return catFileOutput.Bytes(), nil } func removeComments(input []byte) []byte { @@ -209,7 +302,49 @@ func validateURL(request CodeRepositoryRequest) error { return nil } -func validateArgs(_ *types.GitspaceConfig) error { - // TODO Validate the args - return nil +func findUserFromUID(ctx context.Context, + principalStore store.PrincipalStore, userUID string, +) (*types.User, error) { + return principalStore.FindUserByUID(ctx, userUID) +} + +func (s genericSCM) gitnessCredentials( + ctx context.Context, + repo *types.Repository, + userUID string, +) (*Credentials, error) { + gitspacePrincipal := bootstrap.NewGitspaceServiceSession().Principal + user, err := findUserFromUID(ctx, s.principalStore, userUID) + if err != nil { + return nil, err + } + var jwtToken string + existingToken, _ := s.tokenStore.FindByIdentifier(ctx, user.ID, defaultGitspacePATIdentifier) + if existingToken != nil { + // create jwt token. + jwtToken, err = jwt.GenerateForToken(existingToken, user.ToPrincipal().Salt) + if err != nil { + return nil, fmt.Errorf("failed to create JWT token: %w", err) + } + } else { + _, jwtToken, err = token.CreatePAT( + ctx, + s.tokenStore, + &gitspacePrincipal, + user, + defaultGitspacePATIdentifier, + &gitspaceJWTLifetime) + } + if err != nil { + return nil, fmt.Errorf("failed to create JWT: %w", err) + } + + cloneURL, err := url.Parse(repo.GitURL) + if err != nil { + return nil, fmt.Errorf("failed to parse clone url '%s': %w", cloneURL, err) + } + + return &Credentials{ + Password: jwtToken, + }, nil } diff --git a/app/gitspace/scm/types.go b/app/gitspace/scm/types.go new file mode 100644 index 000000000..2f0c31679 --- /dev/null +++ b/app/gitspace/scm/types.go @@ -0,0 +1,33 @@ +// 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 scm + +import "github.com/harness/gitness/types" + +type ( + ResolvedDetails struct { + RepoName string + DevcontainerConfig *types.DevcontainerConfig + Credentials *Credentials + Branch string + CloneURL string + } + + // Credentials contains login and initialization information used + // by an automated login process. + Credentials struct { + Password string + } +) diff --git a/app/gitspace/scm/wire.go b/app/gitspace/scm/wire.go index dd71f5172..799d60a64 100644 --- a/app/gitspace/scm/wire.go +++ b/app/gitspace/scm/wire.go @@ -14,13 +14,24 @@ package scm -import "github.com/google/wire" +import ( + "github.com/harness/gitness/app/store" + urlprovider "github.com/harness/gitness/app/url" + "github.com/harness/gitness/git" + + "github.com/google/wire" +) // WireSet provides a wire set for this package. var WireSet = wire.NewSet( ProvideSCM, ) -func ProvideSCM() SCM { - return NewSCM() +func ProvideSCM(repoStore store.RepoStore, + rpcClient git.Interface, + tokenStore store.TokenStore, + principalStore store.PrincipalStore, + urlProvider urlprovider.Provider, +) SCM { + return NewSCM(repoStore, rpcClient, tokenStore, principalStore, urlProvider) } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 5477816e6..4da3c9cfd 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -343,7 +343,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } - scmSCM := scm.ProvideSCM() + scmSCM := scm.ProvideSCM(repoStore, gitInterface, tokenStore, principalStore, provider) infraProvisioner := infrastructure.ProvideInfraProvisionerService(infraProviderConfigStore, infraProviderResourceStore, factory) statefulLogger := logutil.ProvideStatefulLogger(logStream) containerOrchestrator := container.ProvideEmbeddedDockerOrchestrator(dockerClientFactory, statefulLogger) @@ -353,7 +353,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro vsCodeWeb := ide.ProvideVSCodeWebService(vsCodeWebConfig) orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, infraProviderResourceStore, infraProvisioner, containerOrchestrator, reporter3, orchestratorConfig, vsCode, vsCodeWeb) gitspaceEventStore := database.ProvideGitspaceEventStore(db) - gitspaceController := gitspace2.ProvideController(transactor, authorizer, infraproviderService, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, reporter3, orchestratorOrchestrator, gitspaceEventStore, statefulLogger, scmSCM) + gitspaceController := gitspace2.ProvideController(transactor, authorizer, infraproviderService, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, reporter3, orchestratorOrchestrator, gitspaceEventStore, statefulLogger, scmSCM, repoStore) migrateController := migrate.ProvideController(authorizer, principalStore) openapiService := openapi.ProvideOpenAPIService() routerRouter := router.ProvideRouter(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController, infraproviderController, gitspaceController, migrateController, provider, openapiService) diff --git a/types/config.go b/types/config.go index 94582b155..eb607879f 100644 --- a/types/config.go +++ b/types/config.go @@ -253,6 +253,14 @@ type Config struct { DisplayName string `envconfig:"GITNESS_PRINCIPAL_PIPELINE_DISPLAY_NAME" default:"Gitness Pipeline"` Email string `envconfig:"GITNESS_PRINCIPAL_PIPELINE_EMAIL" default:"pipeline@gitness.io"` } + + // Pipeline defines the principal information used to create the pipeline service. + Gitspace struct { + UID string `envconfig:"GITNESS_PRINCIPAL_GITSPACE_UID" default:"gitspace"` + DisplayName string `envconfig:"GITNESS_PRINCIPAL_GITSPACE_DISPLAY_NAME" default:"Gitness Gitspace"` + Email string `envconfig:"GITNESS_PRINCIPAL_GITSPACE_EMAIL" default:"gitspace@gitness.io"` + } + // Admin defines the principal information used to create the admin user. // NOTE: The admin user is only auto-created in case a password and an email is provided. Admin struct { diff --git a/types/enum/gitspace_cope_repo_type.go b/types/enum/gitspace_cope_repo_type.go index e5534faa3..aa5f1a878 100644 --- a/types/enum/gitspace_cope_repo_type.go +++ b/types/enum/gitspace_cope_repo_type.go @@ -19,12 +19,14 @@ type GitspaceCodeRepoType string func (GitspaceCodeRepoType) Enum() []interface{} { return toInterfaceSlice(codeRepoTypes) } var codeRepoTypes = []GitspaceCodeRepoType{ - CodeRepoTypeGithub, CodeRepoTypeGitlab, CodeRepoTypeHarnessCode, CodeRepoTypeBitbucket, CodeRepoTypeUnknown, + CodeRepoTypeGithub, CodeRepoTypeGitlab, CodeRepoTypeHarnessCode, + CodeRepoTypeBitbucket, CodeRepoTypeUnknown, CodeRepoTypeGitness, } const ( CodeRepoTypeGithub GitspaceCodeRepoType = "github" CodeRepoTypeGitlab GitspaceCodeRepoType = "gitlab" + CodeRepoTypeGitness GitspaceCodeRepoType = "gitness" CodeRepoTypeHarnessCode GitspaceCodeRepoType = "harness_code" CodeRepoTypeBitbucket GitspaceCodeRepoType = "bitbucket" CodeRepoTypeUnknown GitspaceCodeRepoType = "unknown" diff --git a/types/gitspace.go b/types/gitspace.go index 8312d0153..055127eb8 100644 --- a/types/gitspace.go +++ b/types/gitspace.go @@ -27,7 +27,7 @@ type GitspaceConfig struct { InfraProviderResourceID int64 `json:"-"` InfraProviderResourceIdentifier string `json:"resource_identifier"` CodeRepoURL string `json:"code_repo_url"` - CodeRepoType enum.GitspaceCodeRepoType `json:"-"` + CodeRepoType enum.GitspaceCodeRepoType `json:"code_repo_type"` Branch string `json:"branch"` DevcontainerPath *string `json:"devcontainer_path,omitempty"` UserID string `json:"user_id"`