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
pull/3597/head
Ansuman Satapathy 2024-11-27 13:14:44 +00:00 committed by Harness
parent 323511819d
commit 843dc4df5f
9 changed files with 142 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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