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
BT-10437
Deepak Bhatt 2024-12-20 04:52:13 +00:00 committed by Harness
parent 2ced78f102
commit 4f739d5127
15 changed files with 201 additions and 82 deletions

View File

@ -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)
}
}

View File

@ -74,6 +74,9 @@ func InstallTools(
return err
}
return nil
case enum.IDETypeIntellij:
// not installing any tools for intellij
return nil
}
return nil
}

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 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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 <<EOF > "$EXTENSIONS_FILE"
{
"recommendations": {{ .Extensions }}
}
EOF
chmod 644 "$EXTENSIONS_FILE"
chown "$username:$username" "$EXTENSIONS_FILE"
else
echo "extensions.json already exists for user $username"
fi

View File

@ -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 <<EOF > "$EXTENSIONS_FILE"
{
"recommendations": {{ .Extensions }}
}
EOF
chmod 644 "$EXTENSIONS_FILE"
chown "$username:$username" "$EXTENSIONS_FILE"
else
echo "extensions.json already exists for user $username"
fi

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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"
)