Skip to content

[papi] List editor options #18530

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

Merged
merged 12 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
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
96 changes: 96 additions & 0 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type APIInterface interface {
GetFeaturedRepositories(ctx context.Context) (res []*WhitelistedRepository, err error)
GetSuggestedContextURLs(ctx context.Context) (res []*string, err error)
GetWorkspace(ctx context.Context, id string) (res *WorkspaceInfo, err error)
GetIDEOptions(ctx context.Context) (res *IDEOptions, err error)
IsWorkspaceOwner(ctx context.Context, workspaceID string) (res bool, err error)
CreateWorkspace(ctx context.Context, options *CreateWorkspaceOptions) (res *WorkspaceCreationResult, err error)
StartWorkspace(ctx context.Context, id string, options *StartWorkspaceOptions) (res *StartWorkspaceResult, err error)
Expand Down Expand Up @@ -145,6 +146,8 @@ const (
FunctionGetSuggestedContextURLs FunctionName = "getSuggestedContextURLs"
// FunctionGetWorkspace is the name of the getWorkspace function
FunctionGetWorkspace FunctionName = "getWorkspace"
// FunctionGetIDEOptions is the name of the getIDEOptions function
FunctionGetIDEOptions FunctionName = "getIDEOptions"
// FunctionIsWorkspaceOwner is the name of the isWorkspaceOwner function
FunctionIsWorkspaceOwner FunctionName = "isWorkspaceOwner"
// FunctionCreateWorkspace is the name of the createWorkspace function
Expand Down Expand Up @@ -712,6 +715,25 @@ func (gp *APIoverJSONRPC) GetWorkspace(ctx context.Context, id string) (res *Wor
return
}

// GetIDEOptions calls getIDEOptions on the server
func (gp *APIoverJSONRPC) GetIDEOptions(ctx context.Context) (res *IDEOptions, err error) {
if gp == nil {
err = errNotConnected
return
}
var _params []interface{}

var result IDEOptions
err = gp.C.Call(ctx, "getIDEOptions", _params, &result)
if err != nil {
return
}

res = &result

return
}

// IsWorkspaceOwner calls isWorkspaceOwner on the server
func (gp *APIoverJSONRPC) IsWorkspaceOwner(ctx context.Context, workspaceID string) (res bool, err error) {
if gp == nil {
Expand Down Expand Up @@ -2249,3 +2271,77 @@ type CreateProjectOptions struct {
CloneURL string `json:"cloneUrl,omitempty"`
AppInstallationID string `json:"appInstallationId,omitempty"`
}

type IDEType string

const (
IDETypeBrowser IDEType = "browser"
IDETypeDesktop IDEType = "desktop"
)

type IDEConfig struct {
SupervisorImage string `json:"supervisorImage"`
IdeOptions IDEOptions `json:"ideOptions"`
}

type IDEOptions struct {
// Options is a list of available IDEs.
Options map[string]IDEOption `json:"options"`
// DefaultIde when the user has not specified one.
DefaultIde string `json:"defaultIde"`
// DefaultDesktopIde when the user has not specified one.
DefaultDesktopIde string `json:"defaultDesktopIde"`
// Clients specific IDE options.
Clients map[string]IDEClient `json:"clients"`
}

type IDEOption struct {
// OrderKey to ensure a stable order one can set an `orderKey`.
OrderKey string `json:"orderKey,omitempty"`
// Title with human readable text of the IDE (plain text only).
Title string `json:"title"`
// Type of the IDE, currently 'browser' or 'desktop'.
Type IDEType `json:"type"`
// Logo URL for the IDE. See also components/ide-proxy/static/image/ide-log/ folder
Logo string `json:"logo"`
// Tooltip plain text only
Tooltip string `json:"tooltip,omitempty"`
// Label is next to the IDE option like “Browser” (plain text only).
Label string `json:"label,omitempty"`
// Notes to the IDE option that are rendered in the preferences when a user chooses this IDE.
Notes []string `json:"notes,omitempty"`
// Hidden this IDE option is not visible in the IDE preferences.
Hidden bool `json:"hidden,omitempty"`
// Experimental this IDE option is to only be shown to some users
Experimental bool `json:"experimental,omitempty"`
// Image ref to the IDE image.
Image string `json:"image"`
// LatestImage ref to the IDE image, this image ref always resolve to digest.
LatestImage string `json:"latestImage,omitempty"`
// ResolveImageDigest when this is `true`, the tag of this image is resolved to the latest image digest regularly.
// 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.
ResolveImageDigest bool `json:"resolveImageDigest,omitempty"`
// PluginImage ref for the IDE image, this image ref always resolve to digest.
// DEPRECATED use ImageLayers instead
PluginImage string `json:"pluginImage,omitempty"`
// PluginLatestImage ref for the latest IDE image, this image ref always resolve to digest.
// DEPRECATED use LatestImageLayers instead
PluginLatestImage string `json:"pluginLatestImage,omitempty"`
// ImageVersion the semantic version of the IDE image.
ImageVersion string `json:"imageVersion,omitempty"`
// LatestImageVersion the semantic version of the latest IDE image.
LatestImageVersion string `json:"latestImageVersion,omitempty"`
// ImageLayers for additional ide layers and dependencies
ImageLayers []string `json:"imageLayers,omitempty"`
// LatestImageLayers for latest additional ide layers and dependencies
LatestImageLayers []string `json:"latestImageLayers,omitempty"`
}

type IDEClient struct {
// DefaultDesktopIDE when the user has not specified one.
DefaultDesktopIDE string `json:"defaultDesktopIDE,omitempty"`
// DesktopIDEs supported by the client.
DesktopIDEs []string `json:"desktopIDEs,omitempty"`
// InstallationSteps to install the client on user machine.
InstallationSteps []string `json:"installationSteps,omitempty"`
}
45 changes: 30 additions & 15 deletions components/gitpod-protocol/go/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions components/public-api-server/pkg/apiv1/editor_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2023 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.

package apiv1

import (
"context"
"sort"

connect "github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/log"
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
)

func NewEditorService(pool proxy.ServerConnectionPool) *EditorService {
return &EditorService{
connectionPool: pool,
}
}

var _ v1connect.EditorServiceHandler = (*EditorService)(nil)

type EditorService struct {
connectionPool proxy.ServerConnectionPool

v1connect.UnimplementedEditorServiceHandler
}

func (s *EditorService) ListEditorOptions(ctx context.Context, req *connect.Request[v1.ListEditorOptionsRequest]) (*connect.Response[v1.ListEditorOptionsResponse], error) {
conn, err := getConnection(ctx, s.connectionPool)
if err != nil {
return nil, err
}

options, err := conn.GetIDEOptions(ctx)
if err != nil {
log.Extract(ctx).WithError(err).Error("Failed to list editor options.")
return nil, proxy.ConvertError(err)
}

// Sort the response by OrderKey
var keys []string
for key := range options.Options {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return options.Options[keys[i]].OrderKey < options.Options[keys[j]].OrderKey
})

convertedOptions := make([]*v1.EditorOption, 0, len(options.Options))
for _, key := range keys {
option := options.Options[key]
convertedOptions = append(convertedOptions, convertEditorOption(&option, key))
}

return connect.NewResponse(&v1.ListEditorOptionsResponse{
Result: convertedOptions,
}), nil
}

func convertEditorOption(ideOption *protocol.IDEOption, id string) *v1.EditorOption {
var editorType *v1.EditorOption_Type
switch ideOption.Type {
case "browser":
editorType = v1.EditorOption_TYPE_BROWSER.Enum()
case "desktop":
editorType = v1.EditorOption_TYPE_DESKTOP.Enum()
default:
editorType = v1.EditorOption_TYPE_UNSPECIFIED.Enum()
}

return &v1.EditorOption{
Id: id,
Title: ideOption.Title,
Type: *editorType,
Logo: ideOption.Logo,
Label: ideOption.Label,
Stable: &v1.EditorOption_Kind{
Version: ideOption.ImageVersion,
},
Latest: &v1.EditorOption_Kind{
Version: ideOption.LatestImageVersion,
},
}
}
105 changes: 105 additions & 0 deletions components/public-api-server/pkg/apiv1/editor_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2023 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.

package apiv1

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/components/public-api/go/config"
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

func TestEditorService_ListEditorOptions(t *testing.T) {
t.Run("proxies request to server", func(t *testing.T) {
serverMock, client := setupEditorService(t)

serverMock.EXPECT().GetIDEOptions(gomock.Any()).Return(&protocol.IDEOptions{Options: map[string]protocol.IDEOption{
"code": {
OrderKey: "02",
Title: "VS Code",
Logo: "https://gitpod.io/icons/vscode.svg",
ImageVersion: "1.68.0",
LatestImageVersion: "1.69.0",
},
"theia": {
OrderKey: "01",
Title: "Theia",
Logo: "https://gitpod.io/icons/theia.svg",
ImageVersion: "1.68.0",
},
}, DefaultIde: "", DefaultDesktopIde: "", Clients: map[string]protocol.IDEClient{}}, nil)

retrieved, err := client.ListEditorOptions(context.Background(), connect.NewRequest(&v1.ListEditorOptionsRequest{}))
require.NoError(t, err)
requireEqualProto(t, &v1.ListEditorOptionsResponse{
Result: []*v1.EditorOption{
{
Title: "Theia",
Id: "theia",
Logo: "https://gitpod.io/icons/theia.svg",
Stable: &v1.EditorOption_Kind{
Version: "1.68.0",
},
Latest: &v1.EditorOption_Kind{},
},
{
Title: "VS Code",
Id: "code",
Logo: "https://gitpod.io/icons/vscode.svg",
Stable: &v1.EditorOption_Kind{
Version: "1.68.0",
},
Latest: &v1.EditorOption_Kind{
Version: "1.69.0",
},
},
},
}, retrieved.Msg)
})
}

func setupEditorService(t *testing.T) (*protocol.MockAPIInterface, v1connect.EditorServiceClient) {
t.Helper()

ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)

serverMock := protocol.NewMockAPIInterface(ctrl)

svc := NewEditorService(&FakeServerConnPool{
api: serverMock,
})

keyset := jwstest.GenerateKeySet(t)
rsa256, err := jws.NewRSA256(keyset)
require.NoError(t, err)

_, handler := v1connect.NewEditorServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
Issuer: "unitetest.com",
Cookie: config.CookieConfig{
Name: "cookie_jwt",
},
}, rsa256)))

srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)

client := v1connect.NewEditorServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
auth.NewClientInterceptor("auth-token"),
))

return serverMock, client
}
1 change: 1 addition & 0 deletions components/public-api-server/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func register(srv *baseserver.Server, deps *registerDependencies) error {
rootHandler.Mount(v1connect.NewTeamsServiceHandler(apiv1.NewTeamsService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewUserServiceHandler(apiv1.NewUserService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewSCMServiceHandler(apiv1.NewSCMService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewEditorServiceHandler(apiv1.NewEditorService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewIDEClientServiceHandler(apiv1.NewIDEClientService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewProjectsServiceHandler(apiv1.NewProjectsService(deps.connPool), handlerOptions...))
rootHandler.Mount(v1connect.NewOIDCServiceHandler(apiv1.NewOIDCService(deps.connPool, deps.expClient, deps.dbConn, deps.cipher), handlerOptions...))
Expand Down
Loading