diff --git a/components/ide-service/pkg/server/server.go b/components/ide-service/pkg/server/server.go index de64ce5c5fb28e..361ee90de3f0a5 100644 --- a/components/ide-service/pkg/server/server.go +++ b/components/ide-service/pkg/server/server.go @@ -301,10 +301,11 @@ func grpcProbe(cfg baseserver.ServerConfiguration) func() error { } type IDESettings struct { - DefaultIde string `json:"defaultIde,omitempty"` - UseLatestVersion bool `json:"useLatestVersion,omitempty"` - PreferToolbox bool `json:"preferToolbox,omitempty"` - PinnedIDEversions map[string]string `json:"pinnedIDEversions,omitempty"` + DefaultIde string `json:"defaultIde,omitempty"` + UseLatestVersion bool `json:"useLatestVersion,omitempty"` + PreferToolbox bool `json:"preferToolbox,omitempty"` + PinnedIDEversions map[string]string `json:"pinnedIDEversions,omitempty"` + RestrictedEditorNames []string `json:"restrictedEditorNames,omitempty"` } type WorkspaceContext struct { @@ -395,6 +396,11 @@ func (s *IDEServiceServer) ResolveWorkspaceConfig(ctx context.Context, req *api. } pinnedIDEversions := make(map[string]string) + restrictedEditorNames := make(map[string]struct{}) + + for _, editorName := range ideSettings.RestrictedEditorNames { + restrictedEditorNames[editorName] = struct{}{} + } if ideSettings != nil { pinnedIDEversions = ideSettings.PinnedIDEversions @@ -482,6 +488,11 @@ func (s *IDEServiceServer) ResolveWorkspaceConfig(ctx context.Context, req *api. resp.WebImage = getUserIDEImage(ideConfig.IdeOptions.DefaultIde, useLatest) resp.IdeImageLayers = getUserImageLayers(ideConfig.IdeOptions.DefaultIde, useLatest) + if _, ok := restrictedEditorNames["code"]; ok { + resp.WebImage = "" + resp.IdeImageLayers = []string{} + } + var desktopImageLayer string var desktopUserImageLayers []string if chosenIDE.Type == config.IDETypeDesktop { diff --git a/components/server/src/ide-service.spec.ts b/components/server/src/ide-service.spec.ts index e92a2fc514a4f2..f67111c3236768 100644 --- a/components/server/src/ide-service.spec.ts +++ b/components/server/src/ide-service.spec.ts @@ -13,7 +13,7 @@ const expect = chai.expect; describe("ide-service", function () { describe("migrateSettings", function () { const ideService = new IDEService(); - it("with no ideSettings should be undefined", function () { + it("with no ideSettings should be undefined", async function () { const user: User = { id: "string", @@ -21,11 +21,11 @@ describe("ide-service", function () { identities: [], additionalData: {}, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result).to.undefined; }); - it("with settingVersion 2.0 should be latest", function () { + it("with settingVersion 2.0 should be latest", async function () { const user: User = { id: "string", creationDate: "string", @@ -38,7 +38,7 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result).to.deep.equal({ settingVersion: IDESettingsVersion, defaultIde: "code", @@ -46,7 +46,7 @@ describe("ide-service", function () { }); }); - it("with settingVersion 2.0 should be latest and remove intellij-previous", function () { + it("with settingVersion 2.0 should be latest and remove intellij-previous", async function () { const user: User = { id: "string", creationDate: "string", @@ -59,7 +59,7 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result).to.deep.equal({ settingVersion: IDESettingsVersion, defaultIde: "code", @@ -67,7 +67,7 @@ describe("ide-service", function () { }); }); - it("with settingVersion latest should be undefined", function () { + it("with settingVersion latest should be undefined", async function () { const user: User = { id: "string", creationDate: "string", @@ -80,11 +80,11 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result).to.undefined; }); - it("with code-latest should be code latest", function () { + it("with code-latest should be code latest", async function () { const user: User = { id: "string", creationDate: "string", @@ -96,12 +96,12 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("code"); expect(result?.useLatestVersion ?? false).to.be.true; }); - it("with code-desktop-insiders should be code-desktop latest", function () { + it("with code-desktop-insiders should be code-desktop latest", async function () { const user: User = { id: "string", creationDate: "string", @@ -114,12 +114,12 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("code-desktop"); expect(result?.useLatestVersion ?? false).to.be.true; }); - it("with code-desktop should be code-desktop", function () { + it("with code-desktop should be code-desktop", async function () { const user: User = { id: "string", creationDate: "string", @@ -132,12 +132,12 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("code-desktop"); expect(result?.useLatestVersion ?? false).to.be.false; }); - it("with intellij should be intellij", function () { + it("with intellij should be intellij", async function () { const user: User = { id: "string", creationDate: "string", @@ -151,12 +151,12 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("intellij"); expect(result?.useLatestVersion ?? false).to.be.false; }); - it("with intellij latest version should be intellij latest", function () { + it("with intellij latest version should be intellij latest", async function () { const user: User = { id: "string", creationDate: "string", @@ -170,12 +170,12 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("intellij"); expect(result?.useLatestVersion ?? false).to.be.true; }); - it("with user desktopIde false should be code latest", function () { + it("with user desktopIde false should be code latest", async function () { const user: User = { id: "string", creationDate: "string", @@ -189,7 +189,7 @@ describe("ide-service", function () { }, }, }; - const result = ideService.migrateSettings(user); + const result = await ideService.migrateSettings(user); expect(result?.defaultIde).to.equal("code"); expect(result?.useLatestVersion ?? false).to.be.true; }); diff --git a/components/server/src/ide-service.ts b/components/server/src/ide-service.ts index 0ae135a95449b4..f976f03d46d41c 100644 --- a/components/server/src/ide-service.ts +++ b/components/server/src/ide-service.ts @@ -33,6 +33,7 @@ interface ExtendedIDEOptions extends Omit { export interface ExtendedIDESettings extends IDESettings { pinnedIDEversions?: { [key: string]: string }; + restrictedEditorNames?: string[]; } export interface IDEConfig { @@ -75,7 +76,7 @@ export class IDEService { return Object.keys(config.ideOptions.options).includes(ide); } - migrateSettings(user: User): IDESettings | undefined { + async migrateSettings(user: User): Promise { if ( !user?.additionalData?.ideSettings || user.additionalData.ideSettings.settingVersion === IDESettingsVersion @@ -102,7 +103,7 @@ export class IDEService { newIDESettings.useLatestVersion = useLatest; } - if (ideSettings.defaultIde && !this.isIDEAvailable(ideSettings.defaultIde, { user })) { + if (ideSettings.defaultIde && !(await this.isIDEAvailable(ideSettings.defaultIde, { user }))) { ideSettings.defaultIde = "code"; } return newIDESettings; @@ -117,7 +118,10 @@ export class IDEService { workspace.type === "prebuild" ? IdeServiceApi.WorkspaceType.PREBUILD : IdeServiceApi.WorkspaceType.REGULAR; // in case users have `auto-start` options set - if (userSelectedIdeSettings?.defaultIde && !this.isIDEAvailable(userSelectedIdeSettings.defaultIde, { user })) { + if ( + userSelectedIdeSettings?.defaultIde && + !(await this.isIDEAvailable(userSelectedIdeSettings.defaultIde, { user })) + ) { userSelectedIdeSettings.defaultIde = "code"; } diff --git a/components/server/src/workspace/workspace-service.ts b/components/server/src/workspace/workspace-service.ts index b19d0088d8a12a..3f3ec72d4eb9df 100644 --- a/components/server/src/workspace/workspace-service.ts +++ b/components/server/src/workspace/workspace-service.ts @@ -863,6 +863,12 @@ export class WorkspaceService { options.ideSettings.pinnedIDEversions = orgSettings.pinnedEditorVersions; } + if (orgSettings.restrictedEditorNames) { + if (!options.ideSettings) { + options.ideSettings = {}; + } + options.ideSettings.restrictedEditorNames = orgSettings.restrictedEditorNames; + } // at this point we're about to actually start a new workspace const result = await this.workspaceStarter.startWorkspace(ctx, workspace, user, await projectPromise, options); this.asyncUpdateDeletionEligibilityTime(user.id, workspaceId, true); diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index ae9fb0805092e0..bbaa70dfedfea2 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -458,7 +458,7 @@ export class WorkspaceStarter { ) { const span = TraceContext.startSpan("resolveIDEConfiguration", ctx); try { - const migrated = this.ideService.migrateSettings(user); + const migrated = await this.ideService.migrateSettings(user); if (user.additionalData?.ideSettings && migrated) { user.additionalData.ideSettings = migrated; } diff --git a/components/supervisor/pkg/supervisor/config.go b/components/supervisor/pkg/supervisor/config.go index d2b42f175e5d29..13e0f91444a16d 100644 --- a/components/supervisor/pkg/supervisor/config.go +++ b/components/supervisor/pkg/supervisor/config.go @@ -41,7 +41,7 @@ const supervisorConfigFile = "supervisor-config.json" // Config configures supervisor. type Config struct { StaticConfig - IDE IDEConfig + IDE *IDEConfig DesktopIDEs []*IDEConfig WorkspaceConfig } @@ -573,9 +573,12 @@ func GetConfig() (*Config, error) { return nil, err } - ide, err := loadIDEConfigFromFile(static.IDEConfigLocation) - if err != nil { - return nil, err + var ide *IDEConfig + if _, err := os.Stat(static.IDEConfigLocation); !os.IsNotExist(err) { + ide, err = loadIDEConfigFromFile(static.IDEConfigLocation) + if err != nil { + return nil, err + } } desktopIDEs, err := loadDesktopIDEs(static) if err != nil { @@ -589,7 +592,7 @@ func GetConfig() (*Config, error) { return &Config{ StaticConfig: *static, - IDE: *ide, + IDE: ide, DesktopIDEs: desktopIDEs, WorkspaceConfig: *workspace, }, nil diff --git a/components/supervisor/pkg/supervisor/services.go b/components/supervisor/pkg/supervisor/services.go index f701b15fd3b4bd..8ea63bd4835b04 100644 --- a/components/supervisor/pkg/supervisor/services.go +++ b/components/supervisor/pkg/supervisor/services.go @@ -141,17 +141,20 @@ func (s *statusService) SupervisorStatus(ctx context.Context, req *api.Superviso func (s *statusService) IDEStatus(ctx context.Context, req *api.IDEStatusRequest) (*api.IDEStatusResponse, error) { if req.Wait { - select { - case <-s.ideReady.Wait(): - { - // do nothing - } - case <-ctx.Done(): - if errors.Is(ctx.Err(), context.Canceled) { - return nil, status.Error(codes.Canceled, "execution canceled") - } + if s.ideReady != nil { - return nil, status.Error(codes.DeadlineExceeded, ctx.Err().Error()) + select { + case <-s.ideReady.Wait(): + { + // do nothing + } + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.Canceled) { + return nil, status.Error(codes.Canceled, "execution canceled") + } + + return nil, status.Error(codes.DeadlineExceeded, ctx.Err().Error()) + } } if s.desktopIdeReady != nil { @@ -169,8 +172,10 @@ func (s *statusService) IDEStatus(ctx context.Context, req *api.IDEStatusRequest } } } - - ok, _ := s.ideReady.Get() + ok := true + if s.ideReady != nil { + ok, _ = s.ideReady.Get() + } desktopStatus := &api.IDEStatusResponse_DesktopStatus{} if s.desktopIdeReady != nil { okR, i := s.desktopIdeReady.Get() diff --git a/components/supervisor/pkg/supervisor/supervisor.go b/components/supervisor/pkg/supervisor/supervisor.go index 258fee5c3b7c1e..dfc81b69fee7a2 100644 --- a/components/supervisor/pkg/supervisor/supervisor.go +++ b/components/supervisor/pkg/supervisor/supervisor.go @@ -190,7 +190,7 @@ func Run(options ...RunOption) { } var ( - ideReady = newIDEReadyState(&cfg.IDE) + ideReady *ideReadyState = nil desktopIdeReady *ideReadyState = nil cstate = NewInMemoryContentState(cfg.RepoRoot) @@ -282,6 +282,9 @@ func Run(options ...RunOption) { }, tokenService) } + if cfg.IDE != nil { + ideReady = newIDEReadyState(cfg.IDE) + } if cfg.GetDesktopIDE() != nil { desktopIdeReady = newIDEReadyState(cfg.GetDesktopIDE()) } @@ -415,8 +418,10 @@ func Run(options ...RunOption) { // - JB backend-plugin https://github.com/gitpod-io/gitpod/blob/main/components/ide/jetbrains/launcher/main.go#L80 shouldWaitBackend := shouldShutdown var ideWG sync.WaitGroup - ideWG.Add(1) - go startAndWatchIDE(ctx, cfg, &cfg.IDE, &ideWG, cstate, ideReady, WebIDE, supervisorMetrics, shouldWaitBackend) + if cfg.IDE != nil { + ideWG.Add(1) + go startAndWatchIDE(ctx, cfg, cfg.IDE, &ideWG, cstate, ideReady, WebIDE, supervisorMetrics, shouldWaitBackend) + } if cfg.GetDesktopIDE() != nil { ideWG.Add(1) go startAndWatchIDE(ctx, cfg, cfg.GetDesktopIDE(), &ideWG, cstate, desktopIdeReady, DesktopIDE, supervisorMetrics, shouldWaitBackend) @@ -1122,7 +1127,7 @@ func buildChildProcEnv(cfg *Config, envvars []string, runGP bool) []string { getEnv := func(name string) string { return envs[name] } - for _, ide := range []*IDEConfig{&cfg.IDE, cfg.GetDesktopIDE()} { + for _, ide := range []*IDEConfig{cfg.IDE, cfg.GetDesktopIDE()} { if ide == nil || ide.Env == nil { continue } @@ -1967,10 +1972,12 @@ func trackReadiness(ctx context.Context, w analytics.Writer, cfg *Config, cstate <-cstate.ContentReady() trackFn(cfg, readinessKindContent) }() - go func() { - <-ideReady.Wait() - trackFn(cfg, readinessKindIDE) - }() + if cfg.IDE != nil { + go func() { + <-ideReady.Wait() + trackFn(cfg, readinessKindIDE) + }() + } if cfg.GetDesktopIDE() != nil { go func() { <-desktopIdeReady.Wait() @@ -1999,26 +2006,26 @@ func handleExit(ec *int) { } func waitForIde(parent context.Context, ideReady *ideReadyState, desktopIdeReady *ideReadyState, timeout time.Duration) (bool, string) { - if ideReady == nil { - return true, "" - } ctx, cancel := context.WithTimeout(parent, timeout) defer cancel() - select { - case <-ctx.Done(): - return false, ideReady.ideConfig.DisplayName - case <-ideReady.Wait(): + + wait := func(ready *ideReadyState) bool { + if ready == nil { + return true + } + select { + case <-ctx.Done(): + return false + case <-ready.Wait(): + } + return true } - if desktopIdeReady == nil { - return true, "" + if !wait(ideReady) { + return false, ideReady.ideConfig.DisplayName } - select { - case <-ctx.Done(): - // We assume desktop editors should have backend/server anyway - // "IntelliJ backend timed out to start after 5 minutes" + if !wait(desktopIdeReady) { return false, desktopIdeReady.ideConfig.DisplayName + " backend" - case <-desktopIdeReady.Wait(): } return true, "" } diff --git a/components/ws-proxy/BUILD.yaml b/components/ws-proxy/BUILD.yaml index b7bbf632b185dc..7dac01adaf7269 100644 --- a/components/ws-proxy/BUILD.yaml +++ b/components/ws-proxy/BUILD.yaml @@ -6,6 +6,7 @@ packages: - "go.mod" - "go.sum" - "public/**" + - "pkg/proxy/ide-fallback.html" deps: - components/common-go:lib - components/gitpod-protocol/go:lib @@ -45,6 +46,7 @@ packages: - "go.mod" - "go.sum" - "public/**" + - "pkg/proxy/ide-fallback.html" deps: - components/common-go:lib - components/gitpod-protocol/go:lib diff --git a/components/ws-proxy/debug.sh b/components/ws-proxy/debug.sh index d6235165838d01..1337d5dbc5d10a 100755 --- a/components/ws-proxy/debug.sh +++ b/components/ws-proxy/debug.sh @@ -4,4 +4,4 @@ # See License.AGPL.txt in the project root for license information. set -Eeuo pipefail -source /workspace/gitpod/scripts/ws-deploy.sh deployment ws-proxy false +source /workspace/gitpod/scripts/ws-deploy.sh deployment ws-proxy true diff --git a/components/ws-proxy/pkg/proxy/ide-fallback.html b/components/ws-proxy/pkg/proxy/ide-fallback.html new file mode 100644 index 00000000000000..6d933bf7cc5c09 --- /dev/null +++ b/components/ws-proxy/pkg/proxy/ide-fallback.html @@ -0,0 +1,17 @@ + + + + + + + + Gitpod + + + + + diff --git a/components/ws-proxy/pkg/proxy/infoprovider.go b/components/ws-proxy/pkg/proxy/infoprovider.go index 9403a51a6f881c..8d5fe8bbdeb254 100644 --- a/components/ws-proxy/pkg/proxy/infoprovider.go +++ b/components/ws-proxy/pkg/proxy/infoprovider.go @@ -203,6 +203,10 @@ func (r *CRDWorkspaceInfoProvider) Reconcile(ctx context.Context, req ctrl.Reque IsManagedByMk2: managedByMk2, } + if wsinfo.IDEImage == "" { + wsinfo.IDEImage = "fake:builtin" + } + r.store.Update(req.Name, wsinfo) r.invalidateConnectionContext(wsinfo) log.WithField("workspace", req.Name).WithField("details", wsinfo).Debug("adding/updating workspace details") diff --git a/components/ws-proxy/pkg/proxy/routes.go b/components/ws-proxy/pkg/proxy/routes.go index 46e89fbefe3630..d46218751ea50d 100644 --- a/components/ws-proxy/pkg/proxy/routes.go +++ b/components/ws-proxy/pkg/proxy/routes.go @@ -11,6 +11,7 @@ import ( "crypto/elliptic" crand "crypto/rand" "crypto/tls" + _ "embed" "encoding/base64" "encoding/json" "encoding/pem" @@ -840,6 +841,9 @@ func isWebSocketUpgrade(req *http.Request) bool { strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") } +//go:embed ide-fallback.html +var ideFallbackPage []byte + func (t *blobserveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { if isWebSocketUpgrade(req) { return nil, xerrors.Errorf("blobserve: websocket not supported") @@ -847,6 +851,17 @@ func (t *blobserveTransport) RoundTrip(req *http.Request) (resp *http.Response, image := t.resolveImage(t, req) + if image == "fake:builtin" { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(ideFallbackPage)), + Header: http.Header{"Content-Type": {"text/html; charset=utf-8"}}, + Request: req, + ContentLength: int64(len(ideFallbackPage)), + Status: "200 OK", + }, nil + } + resp, err = t.DoRoundTrip(req) if err != nil { return nil, err