Skip to content

poc #20788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

poc #20788

Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions components/ide-service/pkg/server/server.go
Original file line number Diff line number Diff line change
@@ -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 {
40 changes: 20 additions & 20 deletions components/server/src/ide-service.spec.ts
Original file line number Diff line number Diff line change
@@ -13,19 +13,19 @@ 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",

creationDate: "string",
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,15 +38,15 @@ describe("ide-service", function () {
},
},
};
const result = ideService.migrateSettings(user);
const result = await ideService.migrateSettings(user);
expect(result).to.deep.equal({
settingVersion: IDESettingsVersion,
defaultIde: "code",
useLatestVersion: true,
});
});

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,15 +59,15 @@ describe("ide-service", function () {
},
},
};
const result = ideService.migrateSettings(user);
const result = await ideService.migrateSettings(user);
expect(result).to.deep.equal({
settingVersion: IDESettingsVersion,
defaultIde: "code",
useLatestVersion: false,
});
});

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;
});
10 changes: 7 additions & 3 deletions components/server/src/ide-service.ts
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ interface ExtendedIDEOptions extends Omit<IDEOptions, "options"> {

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<IDESettings | undefined> {
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";
}

6 changes: 6 additions & 0 deletions components/server/src/workspace/workspace-service.ts
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 1 addition & 1 deletion components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 8 additions & 5 deletions components/supervisor/pkg/supervisor/config.go
Original file line number Diff line number Diff line change
@@ -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
29 changes: 17 additions & 12 deletions components/supervisor/pkg/supervisor/services.go
Original file line number Diff line number Diff line change
@@ -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()
51 changes: 29 additions & 22 deletions components/supervisor/pkg/supervisor/supervisor.go
Original file line number Diff line number Diff line change
@@ -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, ""
}
2 changes: 2 additions & 0 deletions components/ws-proxy/BUILD.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion components/ws-proxy/debug.sh
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions components/ws-proxy/pkg/proxy/ide-fallback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<!--
Copyright (c) 2025 Gitpod GmbH. All rights reserved.
Licensed under the GNU Affero General Public License (AGPL).
See License.AGPL.txt in the project root for license information.
-->

<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gitpod</title>
</head>
<body>
<script type="text/javascript" src="/_supervisor/frontend/main.js" charset="utf-8"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions components/ws-proxy/pkg/proxy/infoprovider.go
Original file line number Diff line number Diff line change
@@ -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")
15 changes: 15 additions & 0 deletions components/ws-proxy/pkg/proxy/routes.go
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import (
"crypto/elliptic"
crand "crypto/rand"
"crypto/tls"
_ "embed"
"encoding/base64"
"encoding/json"
"encoding/pem"
@@ -840,13 +841,27 @@ 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")
}

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