From 4f739d5127ed47b5c0fe803b907b11f916c79a9d Mon Sep 17 00:00:00 2001 From: Deepak Bhatt Date: Fri, 20 Dec 2024 04:52:13 +0000 Subject: [PATCH] feat: [CDE-563]: add support for intellij IDE (#3165) * feat: [CDE-552}: add ide url scheme consts * feat: [CDE-552}: define shell interpreter for script * feat: [CDE-552}: add intellij port * feat: [CDE-552}: fix lint * feat: [CDE-552}: add intellij integration * feat: [CDE-552}: add intellij type --- app/gitspace/infrastructure/find.go | 14 ++-- app/gitspace/orchestrator/common/utils.go | 3 + app/gitspace/orchestrator/ide/factory.go | 45 +++++++++++ app/gitspace/orchestrator/ide/vscode.go | 78 +++++++++++++++---- app/gitspace/orchestrator/ide/wire.go | 5 ++ app/gitspace/orchestrator/orchestrator.go | 5 ++ .../orchestrator/orchestrator_resume.go | 26 ++++++- .../orchestrator/orchestrator_trigger.go | 26 +------ .../orchestrator/template/template.go | 8 +- .../template/templates/setup_ssh_server.sh | 26 ------- .../templates/setup_vscode_extensions.sh | 30 +++++++ app/gitspace/orchestrator/wire.go | 6 +- cmd/gitness/wire_gen.go | 3 +- types/config.go | 5 ++ types/enum/ide.go | 3 +- 15 files changed, 201 insertions(+), 82 deletions(-) create mode 100644 app/gitspace/orchestrator/ide/factory.go create mode 100644 app/gitspace/orchestrator/template/templates/setup_vscode_extensions.sh diff --git a/app/gitspace/infrastructure/find.go b/app/gitspace/infrastructure/find.go index bd84fe94b..cfa30b151 100644 --- a/app/gitspace/infrastructure/find.go +++ b/app/gitspace/infrastructure/find.go @@ -117,17 +117,13 @@ func (i infraProvisioner) paramsForProvisioningTypeExisting( func (i infraProvisioner) getGitspaceScheme(ideType enum.IDEType, gitspaceSchemeFromMetadata string) (string, error) { switch ideType { case enum.IDETypeVSCodeWeb: - { - return gitspaceSchemeFromMetadata, nil - } + return gitspaceSchemeFromMetadata, nil case enum.IDETypeVSCode: - { - return "ssh", nil - } + return "ssh", nil + case enum.IDETypeIntellij: + return gitspaceSchemeFromMetadata, nil default: - { - return "", fmt.Errorf("unknown ideType %s", ideType) - } + return "", fmt.Errorf("unknown ideType %s", ideType) } } diff --git a/app/gitspace/orchestrator/common/utils.go b/app/gitspace/orchestrator/common/utils.go index c6c123aaa..467adbfd5 100644 --- a/app/gitspace/orchestrator/common/utils.go +++ b/app/gitspace/orchestrator/common/utils.go @@ -74,6 +74,9 @@ func InstallTools( return err } return nil + case enum.IDETypeIntellij: + // not installing any tools for intellij + return nil } return nil } diff --git a/app/gitspace/orchestrator/ide/factory.go b/app/gitspace/orchestrator/ide/factory.go new file mode 100644 index 000000000..5cb082614 --- /dev/null +++ b/app/gitspace/orchestrator/ide/factory.go @@ -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 ide + +import ( + "fmt" + + "github.com/harness/gitness/types/enum" +) + +type Factory struct { + ides map[enum.IDEType]IDE +} + +func NewFactory(vscode *VSCode, vscodeWeb *VSCodeWeb) Factory { + ides := make(map[enum.IDEType]IDE) + ides[enum.IDETypeVSCode] = vscode + ides[enum.IDETypeVSCodeWeb] = vscodeWeb + return Factory{ides: ides} +} + +func NewFactoryWithIDEs(ides map[enum.IDEType]IDE) Factory { + return Factory{ides: ides} +} + +func (f *Factory) GetIDE(ideType enum.IDEType) (IDE, error) { + val, exist := f.ides[ideType] + if !exist { + return nil, fmt.Errorf("unsupported IDE type: %s", ideType) + } + + return val, nil +} diff --git a/app/gitspace/orchestrator/ide/vscode.go b/app/gitspace/orchestrator/ide/vscode.go index 436980e27..a119e3c69 100644 --- a/app/gitspace/orchestrator/ide/vscode.go +++ b/app/gitspace/orchestrator/ide/vscode.go @@ -32,8 +32,11 @@ import ( var _ IDE = (*VSCode)(nil) -const templateSetupSSHServer string = "setup_ssh_server.sh" -const templateRunSSHServer string = "run_ssh_server.sh" +const ( + templateSetupSSHServer string = "setup_ssh_server.sh" + templateRunSSHServer string = "run_ssh_server.sh" + templateSetupVSCodeExtensions string = "setup_vscode_extensions.sh" +) type VSCodeConfig struct { Port int @@ -53,6 +56,30 @@ func (v *VSCode) Setup( exec *devcontainer.Exec, args map[gitspaceTypes.IDEArg]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, +) error { + gitspaceLogger.Info("Installing ssh-server inside container") + err := v.setupSSHServer(ctx, exec, gitspaceLogger) + if err != nil { + return fmt.Errorf("failed to setup SSH server: %w", err) + } + gitspaceLogger.Info("Successfully installed ssh-server") + + gitspaceLogger.Info("Installing vs-code extensions inside container") + gitspaceLogger.Info("IDE setup output...") + err = v.setupVSCodeExtensions(ctx, exec, args, gitspaceLogger) + if err != nil { + return fmt.Errorf("failed to setup vs code extensions: %w", err) + } + gitspaceLogger.Info("Successfully installed vs-code extensions") + gitspaceLogger.Info("Successfully set up IDE inside container") + + return nil +} + +func (v *VSCode) setupSSHServer( + ctx context.Context, + exec *devcontainer.Exec, + gitspaceLogger gitspaceTypes.GitspaceLogger, ) error { osInfoScript := common.GetOSInfoScript() payload := template.SetupSSHServerPayload{ @@ -60,24 +87,49 @@ func (v *VSCode) Setup( AccessType: exec.AccessType, OSInfoScript: osInfoScript, } - if err := v.updateVSCodeSetupPayload(args, gitspaceLogger, &payload); err != nil { - return err - } sshServerScript, err := template.GenerateScriptFromTemplate( templateSetupSSHServer, &payload) if err != nil { return fmt.Errorf( "failed to generate scipt to setup ssh server from template %s: %w", templateSetupSSHServer, err) } - - gitspaceLogger.Info("Installing ssh-server inside container") - gitspaceLogger.Info("IDE setup output...") err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, sshServerScript, true, gitspaceLogger, false) if err != nil { return fmt.Errorf("failed to setup SSH serverr: %w", err) } - gitspaceLogger.Info("Successfully installed ssh-server") - gitspaceLogger.Info("Successfully set up IDE inside container") + + return nil +} + +func (v *VSCode) setupVSCodeExtensions( + ctx context.Context, + exec *devcontainer.Exec, + args map[gitspaceTypes.IDEArg]interface{}, + gitspaceLogger gitspaceTypes.GitspaceLogger, +) error { + payload := template.SetupVSCodeExtensionsPayload{ + Username: exec.RemoteUser, + } + if err := v.updateVSCodeSetupPayload(args, gitspaceLogger, &payload); err != nil { + return err + } + + vscodeExtensionsScript, err := template.GenerateScriptFromTemplate( + templateSetupVSCodeExtensions, &payload) + if err != nil { + return fmt.Errorf( + "failed to generate scipt to setup vscode extensions from template %s: %w", + templateSetupVSCodeExtensions, + err, + ) + } + + err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, vscodeExtensionsScript, + true, gitspaceLogger, false) + if err != nil { + return fmt.Errorf("failed to setup vs-code extensions: %w", err) + } + return nil } @@ -122,7 +174,7 @@ func (v *VSCode) Type() enum.IDEType { func (v *VSCode) updateVSCodeSetupPayload( args map[gitspaceTypes.IDEArg]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, - payload *template.SetupSSHServerPayload, + payload *template.SetupVSCodeExtensionsPayload, ) error { if args == nil { return nil @@ -141,7 +193,7 @@ func (v *VSCode) updateVSCodeSetupPayload( func (v *VSCode) handleVSCodeCustomization( args map[gitspaceTypes.IDEArg]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, - payload *template.SetupSSHServerPayload, + payload *template.SetupVSCodeExtensionsPayload, ) error { customization, exists := args[gitspaceTypes.VSCodeCustomizationArg] if !exists { @@ -171,7 +223,7 @@ func (v *VSCode) handleVSCodeCustomization( func (v *VSCode) handleRepoName( args map[gitspaceTypes.IDEArg]interface{}, - payload *template.SetupSSHServerPayload, + payload *template.SetupVSCodeExtensionsPayload, ) error { repoName, exists := args[gitspaceTypes.IDERepoNameArg] if !exists { diff --git a/app/gitspace/orchestrator/ide/wire.go b/app/gitspace/orchestrator/ide/wire.go index d5a1a7da4..5fb784ceb 100644 --- a/app/gitspace/orchestrator/ide/wire.go +++ b/app/gitspace/orchestrator/ide/wire.go @@ -21,6 +21,7 @@ import ( var WireSet = wire.NewSet( ProvideVSCodeWebService, ProvideVSCodeService, + ProvideIDEFactory, ) func ProvideVSCodeWebService(config *VSCodeWebConfig) *VSCodeWeb { @@ -30,3 +31,7 @@ func ProvideVSCodeWebService(config *VSCodeWebConfig) *VSCodeWeb { func ProvideVSCodeService(config *VSCodeConfig) *VSCode { return NewVsCodeService(config) } + +func ProvideIDEFactory(vscode *VSCode, vscodeWeb *VSCodeWeb) Factory { + return NewFactory(vscode, vscodeWeb) +} diff --git a/app/gitspace/orchestrator/orchestrator.go b/app/gitspace/orchestrator/orchestrator.go index 75638da87..a3d1d507e 100644 --- a/app/gitspace/orchestrator/orchestrator.go +++ b/app/gitspace/orchestrator/orchestrator.go @@ -21,6 +21,11 @@ import ( "github.com/harness/gitness/types/enum" ) +const ( + vscodeURLScheme = "vscode-remote" + intellijURLScheme = "jetbrains-gateway" +) + type Orchestrator interface { // TriggerStartGitspace fetches the infra resources configured for the gitspace and triggers the infra provisioning. TriggerStartGitspace(ctx context.Context, gitspaceConfig types.GitspaceConfig) *types.GitspaceError diff --git a/app/gitspace/orchestrator/orchestrator_resume.go b/app/gitspace/orchestrator/orchestrator_resume.go index 69a448851..13ff012d6 100644 --- a/app/gitspace/orchestrator/orchestrator_resume.go +++ b/app/gitspace/orchestrator/orchestrator_resume.go @@ -74,7 +74,7 @@ func (o orchestrator) ResumeStartGitspace( } gitspaceInstance.AccessKey = &resolvedSecret.SecretValue - ideSvc, err := o.getIDEService(gitspaceConfig) + ideSvc, err := o.ideFactory.GetIDE(gitspaceConfig.IDE) if err != nil { return *gitspaceInstance, &types.GitspaceError{ Error: err, @@ -207,16 +207,17 @@ func generateIDEURL( relativeRepoPath := strings.TrimPrefix(startResponse.AbsoluteRepoPath, "/") - if gitspaceConfig.IDE == enum.IDETypeVSCodeWeb { + switch gitspaceConfig.IDE { + case enum.IDETypeVSCodeWeb: ideURL = url.URL{ Scheme: scheme, Host: host + ":" + forwardedPort, RawQuery: filepath.Join("folder=", relativeRepoPath), } - } else if gitspaceConfig.IDE == enum.IDETypeVSCode { + case enum.IDETypeVSCode: // TODO: the following userID is hard coded and should be changed. ideURL = url.URL{ - Scheme: "vscode-remote", + Scheme: vscodeURLScheme, Host: "", // Empty since we include the host and port in the path Path: fmt.Sprintf( "ssh-remote+%s@%s:%s", @@ -225,7 +226,24 @@ func generateIDEURL( filepath.Join(forwardedPort, relativeRepoPath), ), } + case enum.IDETypeIntellij: + idePath := relativeRepoPath + "/.cache" + ideURL = url.URL{ + Scheme: intellijURLScheme, + Host: "", // Empty since we include the host and port in the path + Path: "connect", + Fragment: fmt.Sprintf("idePath=%s&projectPath=%s&host=%s&port=%s&user=%s&type=%s&deploy=%s", + idePath, + relativeRepoPath, + host, + forwardedPort, + startResponse.RemoteUser, + "ssh", + "false", + ), + } } + ideURLString := ideURL.String() return ideURLString } diff --git a/app/gitspace/orchestrator/orchestrator_trigger.go b/app/gitspace/orchestrator/orchestrator_trigger.go index c80cc34ca..d6759aaf0 100644 --- a/app/gitspace/orchestrator/orchestrator_trigger.go +++ b/app/gitspace/orchestrator/orchestrator_trigger.go @@ -49,8 +49,7 @@ type orchestrator struct { containerOrchestrator container.Orchestrator eventReporter *events.Reporter config *Config - vsCodeService *ide.VSCode - vsCodeWebService *ide.VSCodeWeb + ideFactory ide.Factory secretResolverFactory *secret.ResolverFactory } @@ -64,8 +63,7 @@ func NewOrchestrator( containerOrchestrator container.Orchestrator, eventReporter *events.Reporter, config *Config, - vsCodeService *ide.VSCode, - vsCodeWebService *ide.VSCodeWeb, + ideFactory ide.Factory, secretResolverFactory *secret.ResolverFactory, ) Orchestrator { return orchestrator{ @@ -76,8 +74,7 @@ func NewOrchestrator( containerOrchestrator: containerOrchestrator, eventReporter: eventReporter, config: config, - vsCodeService: vsCodeService, - vsCodeWebService: vsCodeWebService, + ideFactory: ideFactory, secretResolverFactory: secretResolverFactory, } } @@ -344,27 +341,12 @@ func (o orchestrator) emitGitspaceEvent( }) } -func (o orchestrator) getIDEService(gitspaceConfig types.GitspaceConfig) (ide.IDE, error) { - var ideService ide.IDE - - switch gitspaceConfig.IDE { - case enum.IDETypeVSCode: - ideService = o.vsCodeService - case enum.IDETypeVSCodeWeb: - ideService = o.vsCodeWebService - default: - return nil, fmt.Errorf("unsupported IDE: %s", gitspaceConfig.IDE) - } - - return ideService, nil -} - func (o orchestrator) getPortsRequiredForGitspace( gitspaceConfig types.GitspaceConfig, devcontainerConfig types.DevcontainerConfig, ) ([]types.GitspacePort, error) { // TODO: What if the required ports in the config have deviated from when the last instance was created? - resolvedIDE, err := o.getIDEService(gitspaceConfig) + resolvedIDE, err := o.ideFactory.GetIDE(gitspaceConfig.IDE) if err != nil { return nil, fmt.Errorf("unable to get IDE service while checking required Gitspace ports: %w", err) } diff --git a/app/gitspace/orchestrator/template/template.go b/app/gitspace/orchestrator/template/template.go index 145549134..68f86835b 100644 --- a/app/gitspace/orchestrator/template/template.go +++ b/app/gitspace/orchestrator/template/template.go @@ -73,8 +73,12 @@ type SetupSSHServerPayload struct { Username string AccessType enum.GitspaceAccessType OSInfoScript string - Extensions string - RepoName string +} + +type SetupVSCodeExtensionsPayload struct { + Username string + Extensions string + RepoName string } type RunSSHServerPayload struct { diff --git a/app/gitspace/orchestrator/template/templates/setup_ssh_server.sh b/app/gitspace/orchestrator/template/templates/setup_ssh_server.sh index f1da24244..2489f3867 100644 --- a/app/gitspace/orchestrator/template/templates/setup_ssh_server.sh +++ b/app/gitspace/orchestrator/template/templates/setup_ssh_server.sh @@ -41,7 +41,6 @@ fi username={{ .Username }} accessType={{ .AccessType }} -repoName="{{ .RepoName }}" # Configure SSH to allow this user config_file='/etc/ssh/sshd_config' @@ -81,28 +80,3 @@ fi mkdir -p /var/run/sshd -# Create .vscode/extensions.json for the current user -USER_HOME=$(eval echo ~$username) -VSCODE_REMOTE_DIR="$USER_HOME/$repoName/.vscode" -EXTENSIONS_FILE="$VSCODE_REMOTE_DIR/extensions.json" - -# Create .vscode directory with correct ownership -if [ ! -d "$VSCODE_REMOTE_DIR" ]; then - echo "Creating directory: $VSCODE_REMOTE_DIR" - mkdir -p "$VSCODE_REMOTE_DIR" - chown "$username:$username" "$VSCODE_REMOTE_DIR" -fi - -# Create extensions.json file with correct ownership -if [ ! -f "$EXTENSIONS_FILE" ]; then - echo "Creating extensions.json file for user $username" - cat < "$EXTENSIONS_FILE" -{ - "recommendations": {{ .Extensions }} -} -EOF - chmod 644 "$EXTENSIONS_FILE" - chown "$username:$username" "$EXTENSIONS_FILE" -else - echo "extensions.json already exists for user $username" -fi diff --git a/app/gitspace/orchestrator/template/templates/setup_vscode_extensions.sh b/app/gitspace/orchestrator/template/templates/setup_vscode_extensions.sh new file mode 100644 index 000000000..796dd5855 --- /dev/null +++ b/app/gitspace/orchestrator/template/templates/setup_vscode_extensions.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +username={{ .Username }} +repoName="{{ .RepoName }}" + +# Create .vscode/extensions.json for the current user +USER_HOME=$(eval echo ~$username) +VSCODE_REMOTE_DIR="$USER_HOME/$repoName/.vscode" +EXTENSIONS_FILE="$VSCODE_REMOTE_DIR/extensions.json" + +# Create .vscode directory with correct ownership +if [ ! -d "$VSCODE_REMOTE_DIR" ]; then + echo "Creating directory: $VSCODE_REMOTE_DIR" + mkdir -p "$VSCODE_REMOTE_DIR" + chown "$username:$username" "$VSCODE_REMOTE_DIR" +fi + +# Create extensions.json file with correct ownership +if [ ! -f "$EXTENSIONS_FILE" ]; then + echo "Creating extensions.json file for user $username" + cat < "$EXTENSIONS_FILE" +{ + "recommendations": {{ .Extensions }} +} +EOF + chmod 644 "$EXTENSIONS_FILE" + chown "$username:$username" "$EXTENSIONS_FILE" +else + echo "extensions.json already exists for user $username" +fi diff --git a/app/gitspace/orchestrator/wire.go b/app/gitspace/orchestrator/wire.go index 6cdc14ad5..795d4f844 100644 --- a/app/gitspace/orchestrator/wire.go +++ b/app/gitspace/orchestrator/wire.go @@ -40,8 +40,7 @@ func ProvideOrchestrator( containerOrchestrator container.Orchestrator, reporter *events.Reporter, config *Config, - vsCodeService *ide.VSCode, - vsCodeWebService *ide.VSCodeWeb, + ideFactory ide.Factory, secretResolverFactory *secret.ResolverFactory, ) Orchestrator { return NewOrchestrator( @@ -52,8 +51,7 @@ func ProvideOrchestrator( containerOrchestrator, reporter, config, - vsCodeService, - vsCodeWebService, + ideFactory, secretResolverFactory, ) } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index f592ab658..7592b916f 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -333,9 +333,10 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro vsCode := ide.ProvideVSCodeService(vsCodeConfig) vsCodeWebConfig := server.ProvideIDEVSCodeWebConfig(config) vsCodeWeb := ide.ProvideVSCodeWebService(vsCodeWebConfig) + ideFactory := ide.ProvideIDEFactory(vsCode, vsCodeWeb) passwordResolver := secret.ProvidePasswordResolver() resolverFactory := secret.ProvideResolverFactory(passwordResolver) - orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, platformConnector, infraProviderResourceStore, infraProvisioner, containerOrchestrator, eventsReporter, orchestratorConfig, vsCode, vsCodeWeb, resolverFactory) + orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, platformConnector, infraProviderResourceStore, infraProvisioner, containerOrchestrator, eventsReporter, orchestratorConfig, ideFactory, resolverFactory) gitspaceService := gitspace.ProvideGitspace(transactor, gitspaceConfigStore, gitspaceInstanceStore, eventsReporter, gitspaceEventStore, spaceStore, infraproviderService, orchestratorOrchestrator, scmSCM, config) spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, listService, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceService, labelService, instrumentService, executionStore, rulesService) reporter3, err := events5.ProvideReporter(eventsSystem) diff --git a/types/config.go b/types/config.go index ab9860fc7..d986c5fa1 100644 --- a/types/config.go +++ b/types/config.go @@ -411,6 +411,11 @@ type Config struct { // Port is the port on which the SSH server for VSCode will be accessible. Port int `envconfig:"GITNESS_IDE_VSCODE_PORT" default:"8088"` } + + Intellij struct { + // Port is the port on which the SSH server for Intellij will be accessible. + Port int `envconfig:"GITNESS_IDE_INTELLIJ_PORT" default:"8090"` + } } Gitspace struct { diff --git a/types/enum/ide.go b/types/enum/ide.go index 4ac0fedba..a1fe8cdc5 100644 --- a/types/enum/ide.go +++ b/types/enum/ide.go @@ -18,9 +18,10 @@ type IDEType string func (IDEType) Enum() []interface{} { return toInterfaceSlice(ideTypes) } -var ideTypes = []IDEType{IDETypeVSCode, IDETypeVSCodeWeb} +var ideTypes = []IDEType{IDETypeVSCode, IDETypeVSCodeWeb, IDETypeIntellij} const ( IDETypeVSCode IDEType = "vs_code" IDETypeVSCodeWeb IDEType = "vs_code_web" + IDETypeIntellij IDEType = "intellij" )