Path: blob/main/components/public-api-server/pkg/apiv1/workspace.go
2499 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package apiv156import (7"context"8"fmt"910"path/filepath"1112connect "github.com/bufbuild/connect-go"13"github.com/gitpod-io/gitpod/common-go/experiments"14"github.com/gitpod-io/gitpod/common-go/log"15v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"16"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"17protocol "github.com/gitpod-io/gitpod/gitpod-protocol"18"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"19"google.golang.org/protobuf/types/known/timestamppb"20)2122func NewWorkspaceService(serverConnPool proxy.ServerConnectionPool, expClient experiments.Client) *WorkspaceService {23return &WorkspaceService{24connectionPool: serverConnPool,25expClient: expClient,26}27}2829type WorkspaceService struct {30connectionPool proxy.ServerConnectionPool31expClient experiments.Client3233v1connect.UnimplementedWorkspacesServiceHandler34}3536func (s *WorkspaceService) CreateAndStartWorkspace(ctx context.Context, req *connect.Request[v1.CreateAndStartWorkspaceRequest]) (*connect.Response[v1.CreateAndStartWorkspaceResponse], error) {3738conn, err := getConnection(ctx, s.connectionPool)39if err != nil {40return nil, err41}4243ws, err := conn.CreateWorkspace(ctx, &protocol.CreateWorkspaceOptions{44ContextURL: req.Msg.GetContextUrl(),45OrganizationId: req.Msg.GetOrganizationId(),46IgnoreRunningWorkspaceOnSameCommit: req.Msg.GetIgnoreRunningWorkspaceOnSameCommit(),47IgnoreRunningPrebuild: req.Msg.GetIgnoreRunningPrebuild(),48AllowUsingPreviousPrebuilds: req.Msg.GetAllowUsingPreviousPrebuilds(),49ForceDefaultConfig: req.Msg.GetForceDefaultConfig(),50StartWorkspaceOptions: protocol.StartWorkspaceOptions{51WorkspaceClass: req.Msg.GetStartSpec().GetWorkspaceClass(),52Region: req.Msg.GetStartSpec().GetRegion(),53IdeSettings: &protocol.IDESettings{54DefaultIde: req.Msg.StartSpec.IdeSettings.GetDefaultIde(),55UseLatestVersion: req.Msg.StartSpec.IdeSettings.GetUseLatestVersion(),56},57},58})59if err != nil {60log.Extract(ctx).WithError(err).Error("Failed to create workspace.")61return nil, proxy.ConvertError(err)62}6364return connect.NewResponse(&v1.CreateAndStartWorkspaceResponse{65WorkspaceId: ws.CreatedWorkspaceID,66}), nil67}6869func (s *WorkspaceService) GetWorkspace(ctx context.Context, req *connect.Request[v1.GetWorkspaceRequest]) (*connect.Response[v1.GetWorkspaceResponse], error) {70workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())71if err != nil {72return nil, err73}7475conn, err := getConnection(ctx, s.connectionPool)76if err != nil {77return nil, err78}7980ws, err := conn.GetWorkspace(ctx, workspaceID)81if err != nil {82log.Extract(ctx).WithError(err).Error("Failed to get workspace.")83return nil, proxy.ConvertError(err)84}8586workspace, err := s.convertWorkspaceInfo(ctx, ws)87if err != nil {88log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")89return nil, err90}9192return connect.NewResponse(&v1.GetWorkspaceResponse{93Result: workspace,94}), nil95}9697func (s *WorkspaceService) StreamWorkspaceStatus(ctx context.Context, req *connect.Request[v1.StreamWorkspaceStatusRequest], stream *connect.ServerStream[v1.StreamWorkspaceStatusResponse]) error {98workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())99if err != nil {100return err101}102103conn, err := getConnection(ctx, s.connectionPool)104if err != nil {105return err106}107108workspace, err := conn.GetWorkspace(ctx, workspaceID)109if err != nil {110log.Extract(ctx).WithError(err).Error("Failed to get workspace.")111return proxy.ConvertError(err)112}113114if workspace.LatestInstance == nil {115log.Extract(ctx).WithError(err).Error("Failed to get latest instance.")116return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("instance not found"))117}118119ch, err := conn.WorkspaceUpdates(ctx, workspaceID)120if err != nil {121log.Extract(ctx).WithError(err).Error("Failed to get workspace instance updates.")122return proxy.ConvertError(err)123}124125for update := range ch {126instance, err := convertWorkspaceInstance(update, workspace.Workspace.Context, workspace.Workspace.Config, workspace.Workspace.Shareable)127if err != nil {128log.Extract(ctx).WithError(err).Error("Failed to convert workspace instance.")129return proxy.ConvertError(err)130}131err = stream.Send(&v1.StreamWorkspaceStatusResponse{132Result: &v1.WorkspaceStatus{133Instance: instance,134},135})136if err != nil {137log.Extract(ctx).WithError(err).Error("Failed to stream workspace status.")138return proxy.ConvertError(err)139}140}141142return nil143}144145func (s *WorkspaceService) GetOwnerToken(ctx context.Context, req *connect.Request[v1.GetOwnerTokenRequest]) (*connect.Response[v1.GetOwnerTokenResponse], error) {146workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())147if err != nil {148return nil, err149}150151conn, err := getConnection(ctx, s.connectionPool)152if err != nil {153return nil, err154}155156ownerToken, err := conn.GetOwnerToken(ctx, workspaceID)157158if err != nil {159log.Extract(ctx).WithError(err).Error("Failed to get owner token.")160return nil, proxy.ConvertError(err)161}162163return connect.NewResponse(&v1.GetOwnerTokenResponse{Token: ownerToken}), nil164}165166func (s *WorkspaceService) ListWorkspaces(ctx context.Context, req *connect.Request[v1.ListWorkspacesRequest]) (*connect.Response[v1.ListWorkspacesResponse], error) {167conn, err := getConnection(ctx, s.connectionPool)168if err != nil {169return nil, err170}171172limit, err := getLimitFromPagination(req.Msg.GetPagination())173if err != nil {174// getLimitFromPagination returns gRPC errors175return nil, err176}177serverResp, err := conn.GetWorkspaces(ctx, &protocol.GetWorkspacesOptions{178Limit: float64(limit),179OrganizationId: req.Msg.GetOrganizationId(),180})181if err != nil {182return nil, proxy.ConvertError(err)183}184185res := make([]*v1.Workspace, 0, len(serverResp))186for _, ws := range serverResp {187workspace, err := s.convertWorkspaceInfo(ctx, ws)188if err != nil {189// convertWorkspaceInfo returns gRPC errors190return nil, err191}192res = append(res, workspace)193}194195return connect.NewResponse(196&v1.ListWorkspacesResponse{197Result: res,198},199), nil200}201202func (s *WorkspaceService) UpdatePort(ctx context.Context, req *connect.Request[v1.UpdatePortRequest]) (*connect.Response[v1.UpdatePortResponse], error) {203workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())204if err != nil {205return nil, err206}207208conn, err := getConnection(ctx, s.connectionPool)209if err != nil {210return nil, err211}212213var portVisibility string214var portProtocol string215216switch req.Msg.GetPort().GetPolicy() {217case v1.PortPolicy_PORT_POLICY_PRIVATE:218portVisibility = protocol.PortVisibilityPrivate219case v1.PortPolicy_PORT_POLICY_PUBLIC:220portVisibility = protocol.PortVisibilityPublic221default:222return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown port policy specified."))223}224switch req.Msg.GetPort().GetProtocol() {225case v1.PortProtocol_PORT_PROTOCOL_HTTP, v1.PortProtocol_PORT_PROTOCOL_UNSPECIFIED:226portProtocol = protocol.PortProtocolHTTP227case v1.PortProtocol_PORT_PROTOCOL_HTTPS:228portProtocol = protocol.PortProtocolHTTPS229default:230return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown port protocol specified."))231}232_, err = conn.OpenPort(ctx, workspaceID, &protocol.WorkspaceInstancePort{233Port: float64(req.Msg.Port.Port),234Visibility: portVisibility,235Protocol: portProtocol,236})237if err != nil {238log.Extract(ctx).Error("Failed to update port")239return nil, proxy.ConvertError(err)240}241242return connect.NewResponse(243&v1.UpdatePortResponse{},244), nil245}246247func (s *WorkspaceService) StartWorkspace(ctx context.Context, req *connect.Request[v1.StartWorkspaceRequest]) (*connect.Response[v1.StartWorkspaceResponse], error) {248workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())249if err != nil {250return nil, err251}252253conn, err := getConnection(ctx, s.connectionPool)254if err != nil {255return nil, err256}257258_, err = conn.StartWorkspace(ctx, workspaceID, &protocol.StartWorkspaceOptions{})259if err != nil {260log.Extract(ctx).WithError(err).Error("Failed to start workspace.")261return nil, proxy.ConvertError(err)262}263264ws, err := conn.GetWorkspace(ctx, workspaceID)265if err != nil {266log.Extract(ctx).WithError(err).Error("Failed to get workspace.")267return nil, proxy.ConvertError(err)268}269270workspace, err := s.convertWorkspaceInfo(ctx, ws)271if err != nil {272log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")273return nil, err274}275276return connect.NewResponse(&v1.StartWorkspaceResponse{Result: workspace}), nil277}278279func (s *WorkspaceService) StopWorkspace(ctx context.Context, req *connect.Request[v1.StopWorkspaceRequest]) (*connect.Response[v1.StopWorkspaceResponse], error) {280workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())281if err != nil {282return nil, err283}284285conn, err := getConnection(ctx, s.connectionPool)286if err != nil {287return nil, err288}289290err = conn.StopWorkspace(ctx, workspaceID)291if err != nil {292log.Extract(ctx).WithError(err).Error("Failed to stop workspace.")293return nil, proxy.ConvertError(err)294}295296ws, err := conn.GetWorkspace(ctx, workspaceID)297if err != nil {298log.Extract(ctx).WithError(err).Error("Failed to get workspace.")299return nil, proxy.ConvertError(err)300}301302workspace, err := s.convertWorkspaceInfo(ctx, ws)303if err != nil {304log.Extract(ctx).WithError(err).Error("Failed to convert workspace.")305return nil, err306}307308return connect.NewResponse(&v1.StopWorkspaceResponse{Result: workspace}), nil309}310311func (s *WorkspaceService) DeleteWorkspace(ctx context.Context, req *connect.Request[v1.DeleteWorkspaceRequest]) (*connect.Response[v1.DeleteWorkspaceResponse], error) {312workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())313if err != nil {314return nil, err315}316317conn, err := getConnection(ctx, s.connectionPool)318if err != nil {319return nil, err320}321322err = conn.DeleteWorkspace(ctx, workspaceID)323if err != nil {324log.Extract(ctx).WithError(err).Error("Failed to delete workspace.")325return nil, proxy.ConvertError(err)326}327328return connect.NewResponse(&v1.DeleteWorkspaceResponse{}), nil329}330331func (s *WorkspaceService) ListWorkspaceClasses(ctx context.Context, req *connect.Request[v1.ListWorkspaceClassesRequest]) (*connect.Response[v1.ListWorkspaceClassesResponse], error) {332conn, err := getConnection(ctx, s.connectionPool)333if err != nil {334return nil, err335}336337classes, err := conn.GetSupportedWorkspaceClasses(ctx)338if err != nil {339log.Extract(ctx).WithError(err).Error("Failed to get workspace classes.")340return nil, proxy.ConvertError(err)341}342343res := make([]*v1.WorkspaceClass, 0, len(classes))344for _, c := range classes {345res = append(res, &v1.WorkspaceClass{346Id: c.ID,347DisplayName: c.DisplayName,348Description: c.Description,349IsDefault: c.IsDefault,350})351}352353return connect.NewResponse(354&v1.ListWorkspaceClassesResponse{355Result: res,356},357), nil358}359360func (s *WorkspaceService) GetDefaultWorkspaceImage(ctx context.Context, req *connect.Request[v1.GetDefaultWorkspaceImageRequest]) (*connect.Response[v1.GetDefaultWorkspaceImageResponse], error) {361conn, err := getConnection(ctx, s.connectionPool)362if err != nil {363return nil, err364}365wsImage, err := conn.GetDefaultWorkspaceImage(ctx, &protocol.GetDefaultWorkspaceImageParams{366WorkspaceID: req.Msg.GetWorkspaceId(),367})368if err != nil {369log.Extract(ctx).WithError(err).Error("Failed to get default workspace image.")370return nil, proxy.ConvertError(err)371}372373source := v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_UNSPECIFIED374if wsImage.Source == protocol.WorkspaceImageSourceInstallation {375source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_INSTALLATION376} else if wsImage.Source == protocol.WorkspaceImageSourceOrganization {377source = v1.GetDefaultWorkspaceImageResponse_IMAGE_SOURCE_ORGANIZATION378}379380return connect.NewResponse(&v1.GetDefaultWorkspaceImageResponse{381Image: wsImage.Image,382Source: source,383}), nil384}385386func getLimitFromPagination(pagination *v1.Pagination) (int, error) {387const (388defaultLimit = 20389maxLimit = 100390)391392if pagination == nil {393return defaultLimit, nil394}395if pagination.PageSize == 0 {396return defaultLimit, nil397}398if pagination.PageSize < 0 || maxLimit < pagination.PageSize {399return 0, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid pagination page size (must be 0 < x < %d)", maxLimit))400}401402return int(pagination.PageSize), nil403}404405func (s *WorkspaceService) convertWorkspaceInfo(ctx context.Context, input *protocol.WorkspaceInfo) (*v1.Workspace, error) {406return convertWorkspaceInfo(input)407}408409// convertWorkspaceInfo converts a "protocol workspace" to a "public API workspace". Returns gRPC errors if things go wrong.410func convertWorkspaceInfo(input *protocol.WorkspaceInfo) (*v1.Workspace, error) {411instance, err := convertWorkspaceInstance(input.LatestInstance, input.Workspace.Context, input.Workspace.Config, input.Workspace.Shareable)412if err != nil {413return nil, err414}415return &v1.Workspace{416WorkspaceId: input.Workspace.ID,417OwnerId: input.Workspace.OwnerID,418ProjectId: "",419Context: &v1.WorkspaceContext{420ContextUrl: input.Workspace.ContextURL,421Details: &v1.WorkspaceContext_Git_{Git: &v1.WorkspaceContext_Git{422NormalizedContextUrl: input.Workspace.Context.NormalizedContextURL,423Repository: &v1.WorkspaceContext_Repository{424Name: input.Workspace.Context.Repository.Name,425Owner: input.Workspace.Context.Repository.Owner,426},427}},428},429Description: input.Workspace.Description,430Status: &v1.WorkspaceStatus{431Instance: instance,432},433}, nil434}435436func convertIdeConfig(ideConfig *protocol.WorkspaceInstanceIDEConfig) *v1.WorkspaceInstanceStatus_EditorReference {437if ideConfig == nil {438return nil439}440ideVersion := "stable"441if ideConfig.UseLatest {442ideVersion = "latest"443}444return &v1.WorkspaceInstanceStatus_EditorReference{445Name: ideConfig.IDE,446Version: ideVersion,447PreferToolbox: ideConfig.PreferToolbox,448}449}450451func convertWorkspaceInstance(wsi *protocol.WorkspaceInstance, wsCtx *protocol.WorkspaceContext, config *protocol.WorkspaceConfig, shareable bool) (*v1.WorkspaceInstance, error) {452if wsi == nil {453return nil, nil454}455456creationTime, err := parseGitpodTimestamp(wsi.CreationTime)457if err != nil {458// TODO(cw): should this really return an error and possibly fail the entire operation?459return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot parse creation time: %v", err))460}461462var phase v1.WorkspaceInstanceStatus_Phase463switch wsi.Status.Phase {464case "unknown":465phase = v1.WorkspaceInstanceStatus_PHASE_UNSPECIFIED466case "preparing":467phase = v1.WorkspaceInstanceStatus_PHASE_PREPARING468case "building":469phase = v1.WorkspaceInstanceStatus_PHASE_IMAGEBUILD470case "pending":471phase = v1.WorkspaceInstanceStatus_PHASE_PENDING472case "creating":473phase = v1.WorkspaceInstanceStatus_PHASE_CREATING474case "initializing":475phase = v1.WorkspaceInstanceStatus_PHASE_INITIALIZING476case "running":477phase = v1.WorkspaceInstanceStatus_PHASE_RUNNING478case "interrupted":479phase = v1.WorkspaceInstanceStatus_PHASE_INTERRUPTED480case "stopping":481phase = v1.WorkspaceInstanceStatus_PHASE_STOPPING482case "stopped":483phase = v1.WorkspaceInstanceStatus_PHASE_STOPPED484default:485// TODO(cw): should this really return an error and possibly fail the entire operation?486return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot convert instance phase: %s", wsi.Status.Phase))487}488489var admissionLevel v1.AdmissionLevel490if shareable {491admissionLevel = v1.AdmissionLevel_ADMISSION_LEVEL_EVERYONE492} else {493admissionLevel = v1.AdmissionLevel_ADMISSION_LEVEL_OWNER_ONLY494}495496var firstUserActivity *timestamppb.Timestamp497if fua := wsi.Status.Conditions.FirstUserActivity; fua != "" {498firstUserActivity, _ = parseGitpodTimestamp(fua)499}500501var ports []*v1.Port502for _, p := range wsi.Status.ExposedPorts {503port := &v1.Port{504Port: uint64(p.Port),505Url: p.URL,506}507if p.Visibility == protocol.PortVisibilityPublic {508port.Policy = v1.PortPolicy_PORT_POLICY_PUBLIC509} else {510port.Policy = v1.PortPolicy_PORT_POLICY_PRIVATE511}512if p.Protocol == protocol.PortProtocolHTTPS {513port.Protocol = v1.PortProtocol_PORT_PROTOCOL_HTTPS514} else {515port.Protocol = v1.PortProtocol_PORT_PROTOCOL_HTTP516}517518ports = append(ports, port)519}520521// Calculate initial workspace folder location522var recentFolders []string523location := ""524if config != nil {525location = config.WorkspaceLocation526if location == "" {527location = config.CheckoutLocation528}529}530if location == "" && wsCtx != nil && wsCtx.Repository != nil {531location = wsCtx.Repository.Name532533}534recentFolders = append(recentFolders, filepath.Join("/workspace", location))535536gitStatus := convertGitStatus(wsi.GitStatus)537538var editor *v1.WorkspaceInstanceStatus_EditorReference539if wsi.Configuration != nil && wsi.Configuration.IDEConfig != nil {540editor = convertIdeConfig(wsi.Configuration.IDEConfig)541}542543return &v1.WorkspaceInstance{544InstanceId: wsi.ID,545WorkspaceId: wsi.WorkspaceID,546CreatedAt: creationTime,547Status: &v1.WorkspaceInstanceStatus{548StatusVersion: uint64(wsi.Status.Version),549Phase: phase,550Message: wsi.Status.Message,551Url: wsi.IdeURL,552Admission: admissionLevel,553Conditions: &v1.WorkspaceInstanceStatus_Conditions{554Failed: wsi.Status.Conditions.Failed,555Timeout: wsi.Status.Conditions.Timeout,556FirstUserActivity: firstUserActivity,557},558Ports: ports,559RecentFolders: recentFolders,560GitStatus: gitStatus,561Editor: editor,562},563}, nil564}565566func convertGitStatus(repo *protocol.WorkspaceInstanceRepoStatus) *v1.GitStatus {567if repo == nil {568return nil569}570return &v1.GitStatus{571Branch: repo.Branch,572LatestCommit: repo.LatestCommit,573TotalUncommitedFiles: int32(repo.TotalUncommitedFiles),574TotalUntrackedFiles: int32(repo.TotalUntrackedFiles),575TotalUnpushedCommits: int32(repo.TotalUnpushedCommits),576UncommitedFiles: repo.UncommitedFiles,577UntrackedFiles: repo.UntrackedFiles,578UnpushedCommits: repo.UnpushedCommits,579}580}581582583