Skip to content

Commit 112eefc

Browse files
[papi] List editor options (#18530)
* options.init * Respond with options * Move to editor service * Unused imports * Mount ot the correct service * Sort by orderkey * Simplify enum name * Add tests * Options -> `result` for consistency * fix result field
1 parent 5304d7a commit 112eefc

File tree

16 files changed

+1337
-15
lines changed

16 files changed

+1337
-15
lines changed

components/gitpod-protocol/go/gitpod-service.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type APIInterface interface {
4444
GetFeaturedRepositories(ctx context.Context) (res []*WhitelistedRepository, err error)
4545
GetSuggestedContextURLs(ctx context.Context) (res []*string, err error)
4646
GetWorkspace(ctx context.Context, id string) (res *WorkspaceInfo, err error)
47+
GetIDEOptions(ctx context.Context) (res *IDEOptions, err error)
4748
IsWorkspaceOwner(ctx context.Context, workspaceID string) (res bool, err error)
4849
CreateWorkspace(ctx context.Context, options *CreateWorkspaceOptions) (res *WorkspaceCreationResult, err error)
4950
StartWorkspace(ctx context.Context, id string, options *StartWorkspaceOptions) (res *StartWorkspaceResult, err error)
@@ -145,6 +146,8 @@ const (
145146
FunctionGetSuggestedContextURLs FunctionName = "getSuggestedContextURLs"
146147
// FunctionGetWorkspace is the name of the getWorkspace function
147148
FunctionGetWorkspace FunctionName = "getWorkspace"
149+
// FunctionGetIDEOptions is the name of the getIDEOptions function
150+
FunctionGetIDEOptions FunctionName = "getIDEOptions"
148151
// FunctionIsWorkspaceOwner is the name of the isWorkspaceOwner function
149152
FunctionIsWorkspaceOwner FunctionName = "isWorkspaceOwner"
150153
// FunctionCreateWorkspace is the name of the createWorkspace function
@@ -712,6 +715,25 @@ func (gp *APIoverJSONRPC) GetWorkspace(ctx context.Context, id string) (res *Wor
712715
return
713716
}
714717

718+
// GetIDEOptions calls getIDEOptions on the server
719+
func (gp *APIoverJSONRPC) GetIDEOptions(ctx context.Context) (res *IDEOptions, err error) {
720+
if gp == nil {
721+
err = errNotConnected
722+
return
723+
}
724+
var _params []interface{}
725+
726+
var result IDEOptions
727+
err = gp.C.Call(ctx, "getIDEOptions", _params, &result)
728+
if err != nil {
729+
return
730+
}
731+
732+
res = &result
733+
734+
return
735+
}
736+
715737
// IsWorkspaceOwner calls isWorkspaceOwner on the server
716738
func (gp *APIoverJSONRPC) IsWorkspaceOwner(ctx context.Context, workspaceID string) (res bool, err error) {
717739
if gp == nil {
@@ -2249,3 +2271,77 @@ type CreateProjectOptions struct {
22492271
CloneURL string `json:"cloneUrl,omitempty"`
22502272
AppInstallationID string `json:"appInstallationId,omitempty"`
22512273
}
2274+
2275+
type IDEType string
2276+
2277+
const (
2278+
IDETypeBrowser IDEType = "browser"
2279+
IDETypeDesktop IDEType = "desktop"
2280+
)
2281+
2282+
type IDEConfig struct {
2283+
SupervisorImage string `json:"supervisorImage"`
2284+
IdeOptions IDEOptions `json:"ideOptions"`
2285+
}
2286+
2287+
type IDEOptions struct {
2288+
// Options is a list of available IDEs.
2289+
Options map[string]IDEOption `json:"options"`
2290+
// DefaultIde when the user has not specified one.
2291+
DefaultIde string `json:"defaultIde"`
2292+
// DefaultDesktopIde when the user has not specified one.
2293+
DefaultDesktopIde string `json:"defaultDesktopIde"`
2294+
// Clients specific IDE options.
2295+
Clients map[string]IDEClient `json:"clients"`
2296+
}
2297+
2298+
type IDEOption struct {
2299+
// OrderKey to ensure a stable order one can set an `orderKey`.
2300+
OrderKey string `json:"orderKey,omitempty"`
2301+
// Title with human readable text of the IDE (plain text only).
2302+
Title string `json:"title"`
2303+
// Type of the IDE, currently 'browser' or 'desktop'.
2304+
Type IDEType `json:"type"`
2305+
// Logo URL for the IDE. See also components/ide-proxy/static/image/ide-log/ folder
2306+
Logo string `json:"logo"`
2307+
// Tooltip plain text only
2308+
Tooltip string `json:"tooltip,omitempty"`
2309+
// Label is next to the IDE option like “Browser” (plain text only).
2310+
Label string `json:"label,omitempty"`
2311+
// Notes to the IDE option that are rendered in the preferences when a user chooses this IDE.
2312+
Notes []string `json:"notes,omitempty"`
2313+
// Hidden this IDE option is not visible in the IDE preferences.
2314+
Hidden bool `json:"hidden,omitempty"`
2315+
// Experimental this IDE option is to only be shown to some users
2316+
Experimental bool `json:"experimental,omitempty"`
2317+
// Image ref to the IDE image.
2318+
Image string `json:"image"`
2319+
// LatestImage ref to the IDE image, this image ref always resolve to digest.
2320+
LatestImage string `json:"latestImage,omitempty"`
2321+
// ResolveImageDigest when this is `true`, the tag of this image is resolved to the latest image digest regularly.
2322+
// This is useful if this image points to a tag like `nightly` that will be updated regularly. When `resolveImageDigest` is `true`, we make sure that we resolve the tag regularly to the most recent image version.
2323+
ResolveImageDigest bool `json:"resolveImageDigest,omitempty"`
2324+
// PluginImage ref for the IDE image, this image ref always resolve to digest.
2325+
// DEPRECATED use ImageLayers instead
2326+
PluginImage string `json:"pluginImage,omitempty"`
2327+
// PluginLatestImage ref for the latest IDE image, this image ref always resolve to digest.
2328+
// DEPRECATED use LatestImageLayers instead
2329+
PluginLatestImage string `json:"pluginLatestImage,omitempty"`
2330+
// ImageVersion the semantic version of the IDE image.
2331+
ImageVersion string `json:"imageVersion,omitempty"`
2332+
// LatestImageVersion the semantic version of the latest IDE image.
2333+
LatestImageVersion string `json:"latestImageVersion,omitempty"`
2334+
// ImageLayers for additional ide layers and dependencies
2335+
ImageLayers []string `json:"imageLayers,omitempty"`
2336+
// LatestImageLayers for latest additional ide layers and dependencies
2337+
LatestImageLayers []string `json:"latestImageLayers,omitempty"`
2338+
}
2339+
2340+
type IDEClient struct {
2341+
// DefaultDesktopIDE when the user has not specified one.
2342+
DefaultDesktopIDE string `json:"defaultDesktopIDE,omitempty"`
2343+
// DesktopIDEs supported by the client.
2344+
DesktopIDEs []string `json:"desktopIDEs,omitempty"`
2345+
// InstallationSteps to install the client on user machine.
2346+
InstallationSteps []string `json:"installationSteps,omitempty"`
2347+
}

components/gitpod-protocol/go/mock.go

Lines changed: 30 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"sort"
10+
11+
connect "github.com/bufbuild/connect-go"
12+
"github.com/gitpod-io/gitpod/common-go/log"
13+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
14+
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
15+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
16+
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
17+
)
18+
19+
func NewEditorService(pool proxy.ServerConnectionPool) *EditorService {
20+
return &EditorService{
21+
connectionPool: pool,
22+
}
23+
}
24+
25+
var _ v1connect.EditorServiceHandler = (*EditorService)(nil)
26+
27+
type EditorService struct {
28+
connectionPool proxy.ServerConnectionPool
29+
30+
v1connect.UnimplementedEditorServiceHandler
31+
}
32+
33+
func (s *EditorService) ListEditorOptions(ctx context.Context, req *connect.Request[v1.ListEditorOptionsRequest]) (*connect.Response[v1.ListEditorOptionsResponse], error) {
34+
conn, err := getConnection(ctx, s.connectionPool)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
options, err := conn.GetIDEOptions(ctx)
40+
if err != nil {
41+
log.Extract(ctx).WithError(err).Error("Failed to list editor options.")
42+
return nil, proxy.ConvertError(err)
43+
}
44+
45+
// Sort the response by OrderKey
46+
var keys []string
47+
for key := range options.Options {
48+
keys = append(keys, key)
49+
}
50+
sort.Slice(keys, func(i, j int) bool {
51+
return options.Options[keys[i]].OrderKey < options.Options[keys[j]].OrderKey
52+
})
53+
54+
convertedOptions := make([]*v1.EditorOption, 0, len(options.Options))
55+
for _, key := range keys {
56+
option := options.Options[key]
57+
convertedOptions = append(convertedOptions, convertEditorOption(&option, key))
58+
}
59+
60+
return connect.NewResponse(&v1.ListEditorOptionsResponse{
61+
Result: convertedOptions,
62+
}), nil
63+
}
64+
65+
func convertEditorOption(ideOption *protocol.IDEOption, id string) *v1.EditorOption {
66+
var editorType *v1.EditorOption_Type
67+
switch ideOption.Type {
68+
case "browser":
69+
editorType = v1.EditorOption_TYPE_BROWSER.Enum()
70+
case "desktop":
71+
editorType = v1.EditorOption_TYPE_DESKTOP.Enum()
72+
default:
73+
editorType = v1.EditorOption_TYPE_UNSPECIFIED.Enum()
74+
}
75+
76+
return &v1.EditorOption{
77+
Id: id,
78+
Title: ideOption.Title,
79+
Type: *editorType,
80+
Logo: ideOption.Logo,
81+
Label: ideOption.Label,
82+
Stable: &v1.EditorOption_Kind{
83+
Version: ideOption.ImageVersion,
84+
},
85+
Latest: &v1.EditorOption_Kind{
86+
Version: ideOption.LatestImageVersion,
87+
},
88+
}
89+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
"github.com/bufbuild/connect-go"
14+
"github.com/gitpod-io/gitpod/components/public-api/go/config"
15+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
16+
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
17+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
18+
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
19+
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
20+
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
21+
"github.com/golang/mock/gomock"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestEditorService_ListEditorOptions(t *testing.T) {
26+
t.Run("proxies request to server", func(t *testing.T) {
27+
serverMock, client := setupEditorService(t)
28+
29+
serverMock.EXPECT().GetIDEOptions(gomock.Any()).Return(&protocol.IDEOptions{Options: map[string]protocol.IDEOption{
30+
"code": {
31+
OrderKey: "02",
32+
Title: "VS Code",
33+
Logo: "https://gitpod.io/icons/vscode.svg",
34+
ImageVersion: "1.68.0",
35+
LatestImageVersion: "1.69.0",
36+
},
37+
"theia": {
38+
OrderKey: "01",
39+
Title: "Theia",
40+
Logo: "https://gitpod.io/icons/theia.svg",
41+
ImageVersion: "1.68.0",
42+
},
43+
}, DefaultIde: "", DefaultDesktopIde: "", Clients: map[string]protocol.IDEClient{}}, nil)
44+
45+
retrieved, err := client.ListEditorOptions(context.Background(), connect.NewRequest(&v1.ListEditorOptionsRequest{}))
46+
require.NoError(t, err)
47+
requireEqualProto(t, &v1.ListEditorOptionsResponse{
48+
Result: []*v1.EditorOption{
49+
{
50+
Title: "Theia",
51+
Id: "theia",
52+
Logo: "https://gitpod.io/icons/theia.svg",
53+
Stable: &v1.EditorOption_Kind{
54+
Version: "1.68.0",
55+
},
56+
Latest: &v1.EditorOption_Kind{},
57+
},
58+
{
59+
Title: "VS Code",
60+
Id: "code",
61+
Logo: "https://gitpod.io/icons/vscode.svg",
62+
Stable: &v1.EditorOption_Kind{
63+
Version: "1.68.0",
64+
},
65+
Latest: &v1.EditorOption_Kind{
66+
Version: "1.69.0",
67+
},
68+
},
69+
},
70+
}, retrieved.Msg)
71+
})
72+
}
73+
74+
func setupEditorService(t *testing.T) (*protocol.MockAPIInterface, v1connect.EditorServiceClient) {
75+
t.Helper()
76+
77+
ctrl := gomock.NewController(t)
78+
t.Cleanup(ctrl.Finish)
79+
80+
serverMock := protocol.NewMockAPIInterface(ctrl)
81+
82+
svc := NewEditorService(&FakeServerConnPool{
83+
api: serverMock,
84+
})
85+
86+
keyset := jwstest.GenerateKeySet(t)
87+
rsa256, err := jws.NewRSA256(keyset)
88+
require.NoError(t, err)
89+
90+
_, handler := v1connect.NewEditorServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
91+
Issuer: "unitetest.com",
92+
Cookie: config.CookieConfig{
93+
Name: "cookie_jwt",
94+
},
95+
}, rsa256)))
96+
97+
srv := httptest.NewServer(handler)
98+
t.Cleanup(srv.Close)
99+
100+
client := v1connect.NewEditorServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
101+
auth.NewClientInterceptor("auth-token"),
102+
))
103+
104+
return serverMock, client
105+
}

components/public-api-server/pkg/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func register(srv *baseserver.Server, deps *registerDependencies) error {
199199
rootHandler.Mount(v1connect.NewTeamsServiceHandler(apiv1.NewTeamsService(deps.connPool), handlerOptions...))
200200
rootHandler.Mount(v1connect.NewUserServiceHandler(apiv1.NewUserService(deps.connPool), handlerOptions...))
201201
rootHandler.Mount(v1connect.NewSCMServiceHandler(apiv1.NewSCMService(deps.connPool), handlerOptions...))
202+
rootHandler.Mount(v1connect.NewEditorServiceHandler(apiv1.NewEditorService(deps.connPool), handlerOptions...))
202203
rootHandler.Mount(v1connect.NewIDEClientServiceHandler(apiv1.NewIDEClientService(deps.connPool), handlerOptions...))
203204
rootHandler.Mount(v1connect.NewProjectsServiceHandler(apiv1.NewProjectsService(deps.connPool), handlerOptions...))
204205
rootHandler.Mount(v1connect.NewOIDCServiceHandler(apiv1.NewOIDCService(deps.connPool, deps.expClient, deps.dbConn, deps.cipher), handlerOptions...))

0 commit comments

Comments
 (0)