From 843dc4df5f45a08d8ce2b5ab7e6fbbb146285ff0 Mon Sep 17 00:00:00 2001 From: Ansuman Satapathy Date: Wed, 27 Nov 2024 13:14:44 +0000 Subject: [PATCH] feat: [CDE-525]: Add support for customization IDE extensions from devcontainer.json (#3057) * feat: [CDE-525]: lint fixes * feat: [CDE-525]: lint fixes * feat: [CDE-525]: setup vscode web plugins * feat: [CDE-525]: remove unused fields during serde from devcontainer.json * feat: [CDE-525]: remove unused fields during serde from devcontainer.json --- .../container/devcontainer_step_utils.go | 16 +----- .../embedded_docker_container_orchestrator.go | 17 +++++- app/gitspace/orchestrator/ide/ide.go | 8 ++- app/gitspace/orchestrator/ide/vscode.go | 19 +++++-- app/gitspace/orchestrator/ide/vscodeweb.go | 53 +++++++++++++++---- .../orchestrator/template/template.go | 10 ++-- .../templates}/install_vscode_web.sh | 0 .../template/templates/run_vscode_web.sh | 9 ++++ types/devcontainer_config_customizations.go | 46 +++++++++++++++- 9 files changed, 142 insertions(+), 36 deletions(-) rename app/gitspace/orchestrator/{ide/script => template/templates}/install_vscode_web.sh (100%) diff --git a/app/gitspace/orchestrator/container/devcontainer_step_utils.go b/app/gitspace/orchestrator/container/devcontainer_step_utils.go index 479a920ed..646288597 100644 --- a/app/gitspace/orchestrator/container/devcontainer_step_utils.go +++ b/app/gitspace/orchestrator/container/devcontainer_step_utils.go @@ -156,25 +156,11 @@ func SetupIDE( return nil } -func RunIDE( - ctx context.Context, - exec *devcontainer.Exec, - ideService ide.IDE, - gitspaceLogger gitspaceTypes.GitspaceLogger, -) error { - gitspaceLogger.Info("Running the IDE inside container: " + string(ideService.Type())) - err := ideService.Run(ctx, exec, nil, gitspaceLogger) - if err != nil { - return logStreamWrapError(gitspaceLogger, "Error while running IDE inside container", err) - } - return nil -} - func RunIDEWithArgs( ctx context.Context, exec *devcontainer.Exec, ideService ide.IDE, - args map[string]string, + args map[string]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, ) error { gitspaceLogger.Info("Running the IDE inside container: " + string(ideService.Type())) diff --git a/app/gitspace/orchestrator/container/embedded_docker_container_orchestrator.go b/app/gitspace/orchestrator/container/embedded_docker_container_orchestrator.go index 04f86a08b..fc0e68674 100644 --- a/app/gitspace/orchestrator/container/embedded_docker_container_orchestrator.go +++ b/app/gitspace/orchestrator/container/embedded_docker_container_orchestrator.go @@ -29,6 +29,7 @@ import ( gitspaceTypes "github.com/harness/gitness/app/gitspace/types" "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" @@ -208,7 +209,8 @@ func (e *EmbeddedDockerOrchestrator) startStoppedGitspace( } // Run IDE setup - if err := RunIDE(ctx, exec, ideService, logStreamInstance); err != nil { + args := ExtractIDEArgs(ideService, resolvedRepoDetails.DevcontainerConfig) + if err := RunIDEWithArgs(ctx, exec, ideService, args, logStreamInstance); err != nil { return err } @@ -528,7 +530,8 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps( exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger, ) error { - return RunIDE(ctx, exec, ideService, gitspaceLogger) + args := ExtractIDEArgs(ideService, devcontainerConfig) + return RunIDEWithArgs(ctx, exec, ideService, args, gitspaceLogger) }, StopOnFailure: true, }, @@ -596,6 +599,16 @@ func (e *EmbeddedDockerOrchestrator) buildSetupSteps( } } +func ExtractIDEArgs(ideService ide.IDE, devcontainerConfig types.DevcontainerConfig) map[string]interface{} { + var args = make(map[string]interface{}) + if ideService.Type() == enum.IDETypeVSCodeWeb || ideService.Type() == enum.IDETypeVSCode { + if devcontainerConfig.Customizations.ExtractVSCodeSpec() != nil { + args["customization"] = *devcontainerConfig.Customizations.ExtractVSCodeSpec() + } + } + return args +} + // setupGitspaceAndIDE initializes Gitspace and IDE by registering and executing the setup steps. func (e *EmbeddedDockerOrchestrator) setupGitspaceAndIDE( ctx context.Context, diff --git a/app/gitspace/orchestrator/ide/ide.go b/app/gitspace/orchestrator/ide/ide.go index bd77abce9..2176e3df6 100644 --- a/app/gitspace/orchestrator/ide/ide.go +++ b/app/gitspace/orchestrator/ide/ide.go @@ -26,13 +26,17 @@ import ( type IDE interface { // Setup is responsible for doing all the operations for setting up the IDE in the container e.g. installation, // copying settings and configurations. - Setup(ctx context.Context, exec *devcontainer.Exec, gitspaceLogger gitspaceTypes.GitspaceLogger) error + Setup( + ctx context.Context, + exec *devcontainer.Exec, + gitspaceLogger gitspaceTypes.GitspaceLogger, + ) error // Run runs the IDE and supporting services. Run( ctx context.Context, exec *devcontainer.Exec, - args map[string]string, + args map[string]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, ) error diff --git a/app/gitspace/orchestrator/ide/vscode.go b/app/gitspace/orchestrator/ide/vscode.go index 29a365588..d25105261 100644 --- a/app/gitspace/orchestrator/ide/vscode.go +++ b/app/gitspace/orchestrator/ide/vscode.go @@ -77,17 +77,28 @@ func (v *VSCode) Setup( func (v *VSCode) Run( ctx context.Context, exec *devcontainer.Exec, - _ map[string]string, + args map[string]interface{}, gitspaceLogger gitspaceTypes.GitspaceLogger, ) error { + payload := template.RunSSHServerPayload{ + Port: strconv.Itoa(v.config.Port), + } runSSHScript, err := template.GenerateScriptFromTemplate( - templateRunSSHServer, &template.RunSSHServerPayload{ - Port: strconv.Itoa(v.config.Port), - }) + templateRunSSHServer, &payload) if err != nil { return fmt.Errorf( "failed to generate scipt to run ssh server from template %s: %w", templateRunSSHServer, err) } + if args != nil { + if customization, exists := args["customization"]; exists { + // Perform a type assertion to ensure customization is a VSCodeCustomizationSpecs + if vsCodeCustomizationSpecs, ok := customization.(types.VSCodeCustomizationSpecs); ok { + gitspaceLogger.Info(fmt.Sprintf("VSCode Customizations %v", vsCodeCustomizationSpecs)) + } else { + return fmt.Errorf("customization is not of type VSCodeCustomizationSpecs") + } + } + } gitspaceLogger.Info("SSH server run output...") err = common.ExecuteCommandInHomeDirAndLog(ctx, exec, runSSHScript, true, gitspaceLogger) if err != nil { diff --git a/app/gitspace/orchestrator/ide/vscodeweb.go b/app/gitspace/orchestrator/ide/vscodeweb.go index 27cef58b6..73f5699fe 100644 --- a/app/gitspace/orchestrator/ide/vscodeweb.go +++ b/app/gitspace/orchestrator/ide/vscodeweb.go @@ -36,9 +36,6 @@ import ( var _ IDE = (*VSCodeWeb)(nil) -//go:embed script/install_vscode_web.sh -var installScript string - //go:embed script/find_vscode_web_path.sh var findPathScript string @@ -46,6 +43,7 @@ var findPathScript string var mediaFiles embed.FS const templateRunVSCodeWeb = "run_vscode_web.sh" +const templateSetupVSCodeWeb = "install_vscode_web.sh" const startMarker = "START_MARKER" const endMarker = "END_MARKER" @@ -69,8 +67,17 @@ func (v *VSCodeWeb) Setup( ) error { gitspaceLogger.Info("Installing VSCode Web inside container.") gitspaceLogger.Info("IDE setup output...") + payload := &template.SetupVSCodeWebPayload{} + setupScript, err := template.GenerateScriptFromTemplate(templateSetupVSCodeWeb, payload) + if err != nil { + return fmt.Errorf( + "failed to generate script to setup VSCode Web from template %s: %w", + templateRunVSCodeWeb, + err, + ) + } outputCh := make(chan []byte) - err := exec.ExecuteCommandInHomeDirectory(ctx, installScript, true, false, outputCh) + err = exec.ExecuteCommandInHomeDirectory(ctx, setupScript, true, false, outputCh) if err != nil { return fmt.Errorf("failed to install VSCode Web: %w", err) } @@ -112,15 +119,17 @@ func (v *VSCodeWeb) Setup( func (v *VSCodeWeb) Run( ctx context.Context, exec *devcontainer.Exec, - args map[string]string, - gitspaceLogger gitspaceTypes.GitspaceLogger) error { + args map[string]interface{}, + gitspaceLogger gitspaceTypes.GitspaceLogger, +) error { payload := &template.RunVSCodeWebPayload{ Port: strconv.Itoa(v.config.Port), } + if args != nil { - // Set ProxyURI only if present in the map - if proxyURI, ok := args["VSCODE_PROXY_URI"]; ok { - payload.ProxyURI = proxyURI + err := updatePayloadFromArgs(args, payload, gitspaceLogger) + if err != nil { + return err } } runScript, err := template.GenerateScriptFromTemplate(templateRunVSCodeWeb, payload) @@ -133,12 +142,38 @@ func (v *VSCodeWeb) Run( } gitspaceLogger.Info("Starting IDE ...") outputCh := make(chan []byte) + // Execute the script in the home directory err = exec.ExecuteCommandInHomeDirectory(ctx, runScript, false, false, outputCh) if err != nil { return fmt.Errorf("failed to run VSCode Web: %w", err) } + return nil +} +func updatePayloadFromArgs( + args map[string]interface{}, + payload *template.RunVSCodeWebPayload, + gitspaceLogger gitspaceTypes.GitspaceLogger, +) error { + if proxyURI, exists := args["VSCODE_PROXY_URI"]; exists { + // Perform a type assertion to ensure proxyURI is a string + proxyURIStr, ok := proxyURI.(string) + if !ok { + return fmt.Errorf("VSCODE_PROXY_URI is not a string") + } + payload.ProxyURI = proxyURIStr + } + + if customization, exists := args["customization"]; exists { + // Perform a type assertion to ensure customization is a VSCodeCustomizationSpecs + vsCodeCustomizationSpecs, ok := customization.(types.VSCodeCustomizationSpecs) + if !ok { + return fmt.Errorf("customization is not of type VSCodeCustomizationSpecs") + } + payload.Extensions = vsCodeCustomizationSpecs.Extensions + gitspaceLogger.Info(fmt.Sprintf("VSCode Customizations %v", vsCodeCustomizationSpecs)) + } return nil } diff --git a/app/gitspace/orchestrator/template/template.go b/app/gitspace/orchestrator/template/template.go index a809eb599..3ff15e134 100644 --- a/app/gitspace/orchestrator/template/template.go +++ b/app/gitspace/orchestrator/template/template.go @@ -53,9 +53,13 @@ type SetupGitCredentialsPayload struct { } type RunVSCodeWebPayload struct { - Port string - Arguments string - ProxyURI string + Port string + Arguments string + ProxyURI string + Extensions []string +} + +type SetupVSCodeWebPayload struct { } type SetupUserPayload struct { diff --git a/app/gitspace/orchestrator/ide/script/install_vscode_web.sh b/app/gitspace/orchestrator/template/templates/install_vscode_web.sh similarity index 100% rename from app/gitspace/orchestrator/ide/script/install_vscode_web.sh rename to app/gitspace/orchestrator/template/templates/install_vscode_web.sh diff --git a/app/gitspace/orchestrator/template/templates/run_vscode_web.sh b/app/gitspace/orchestrator/template/templates/run_vscode_web.sh index 7f1956dd1..b7582dc50 100644 --- a/app/gitspace/orchestrator/template/templates/run_vscode_web.sh +++ b/app/gitspace/orchestrator/template/templates/run_vscode_web.sh @@ -5,6 +5,7 @@ echo "Running VSCode Web" # Default port comes from the Go templating variable {{ .Port }} port={{ .Port }} proxyuri="{{ .ProxyURI }}" +extensions={{ range .Extensions }}"{{ . }}" {{ end }} # Ensure the configuration directory exists mkdir -p $HOME/.config/code-server @@ -16,6 +17,14 @@ auth: none cert: false EOF +# Install extensions using code-server CLI and display errors if any +for extension in $extensions; do + echo "Installing extension: $extension" + if ! code-server --install-extension "$extension"; then + echo "Error installing extension: $extension" >&2 + fi +done + # Export the Proxy URI only if set if [ -n "$proxyuri" ]; then export VSCODE_PROXY_URI="$proxyuri" diff --git a/types/devcontainer_config_customizations.go b/types/devcontainer_config_customizations.go index 35c2651e8..486180964 100644 --- a/types/devcontainer_config_customizations.go +++ b/types/devcontainer_config_customizations.go @@ -14,10 +14,15 @@ package types -import "encoding/json" +import ( + "encoding/json" + + "github.com/rs/zerolog/log" +) const ( GitspaceCustomizationsKey CustomizationsKey = "harnessGitspaces" + VSCodeCustomizationsKey CustomizationsKey = "vscode" ) type CustomizationsKey string @@ -45,6 +50,45 @@ func (dcc DevContainerConfigCustomizations) ExtractGitspaceSpec() *GitspaceCusto return &gitspaceSpecs } +func (dcc DevContainerConfigCustomizations) ExtractVSCodeSpec() *VSCodeCustomizationSpecs { + val, ok := dcc[VSCodeCustomizationsKey.String()] + if !ok { + // Log that the key is missing, but return nil + log.Warn().Msgf("VSCode customization key %q not found, returning empty struct", + VSCodeCustomizationsKey.String()) + return nil + } + + data, ok := val.(map[string]interface{}) + if !ok { + // Log the type mismatch and return nil + log.Warn().Msgf("Unexpected data type for key %q, expected map[string]interface{}, but got %T", + VSCodeCustomizationsKey.String(), val) + return nil + } + + rawData, err := json.Marshal(data) + if err != nil { + // Log the error during marshalling and return nil + log.Printf("Failed to marshal data for key %q: %v", VSCodeCustomizationsKey.String(), err) + return nil + } + + var vsCodeCustomizationSpecs VSCodeCustomizationSpecs + if err := json.Unmarshal(rawData, &vsCodeCustomizationSpecs); err != nil { + // Log the error during unmarshalling and return nil + log.Printf("Failed to unmarshal data for key %q: %v", VSCodeCustomizationsKey.String(), err) + return nil + } + + return &vsCodeCustomizationSpecs +} + +type VSCodeCustomizationSpecs struct { + Extensions []string `json:"extensions"` + Settings map[string]interface{} `json:"settings"` +} + type GitspaceCustomizationSpecs struct { Connectors []struct { Type string `json:"type"`