// Copyright (c) 2020 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 theialib import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "time" "github.com/pkg/errors" ) // HTTPTheiaService provides access to Theia's CLI service type HTTPTheiaService struct { URL string Token string ideReady chan struct{} ideError error ideClient *http.Client } var _ TheiaCLIService = &HTTPTheiaService{} // NewServiceFromEnv produces a new Theia service client configured from environment variables func NewServiceFromEnv() (*HTTPTheiaService, error) { theiaPort := os.Getenv("GITPOD_THEIA_PORT") if theiaPort == "" { theiaPort = "23000" } url := fmt.Sprintf("http://localhost:%s/gitpod/cli", theiaPort) apiToken := os.Getenv("GITPOD_CLI_APITOKEN") if apiToken == "" { return nil, fmt.Errorf("No GITPOD_CLI_APITOKEN environment variable set") } service := &HTTPTheiaService{ URL: url, Token: apiToken, ideReady: make(chan struct{}), ideClient: &http.Client{Timeout: 30 * time.Second}, } go func() { defer close(service.ideReady) supervisorAddr := os.Getenv("SUPERVISOR_ADDR") if supervisorAddr == "" { supervisorAddr = "localhost:22999" } resp, err := http.Get(fmt.Sprintf("http://%s/_supervisor/v1/status/ide/wait/true", supervisorAddr)) if err != nil { service.ideError = err } if resp.StatusCode != http.StatusOK { service.ideError = fmt.Errorf("IDE is not ready, %d %s", resp.StatusCode, resp.Status) } }() return service, nil } type request struct { Method string `json:"method"` Params interface{} `json:"params"` } var ( // ErrNotFound is returned when an object is not found ErrNotFound = fmt.Errorf("not found") ) func (service *HTTPTheiaService) sendRequest(req request) ([]byte, error) { <-service.ideReady if service.ideError != nil { return nil, service.ideError } body, err := json.Marshal(req) if err != nil { return nil, errors.Wrap(err, "cannot marshal request body") } httpreq, err := http.NewRequest("POST", service.URL, bytes.NewBuffer(body)) if err != nil { return nil, errors.Wrap(err, "cannot create new request") } httpreq.Header.Set("Content-Type", "application/json") httpreq.Header["X-AuthToken"] = []string{service.Token} resp, err := service.ideClient.Do(httpreq) if resp != nil && resp.StatusCode == 404 && req.Method == "getGitToken" { supervisorAddr := os.Getenv("SUPERVISOR_ADDR") if supervisorAddr == "" { supervisorAddr = "localhost:22999" } resp, err = http.Get(fmt.Sprintf("http://%s/_supervisor/v1/token/git/%s/", supervisorAddr, req.Params.(GetGitTokenRequest).Host)) } if err != nil { return nil, errors.Wrap(err, "error while issuing request") } if resp.StatusCode == 403 { return nil, fmt.Errorf("not authenticated") } else if resp.StatusCode == 404 { return nil, ErrNotFound } else if resp.StatusCode != 200 { return nil, fmt.Errorf("invalid request: %v", resp.StatusCode) } res, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return res, nil } // GetGitToken obtains a user's Git token for a particular action func (service *HTTPTheiaService) GetGitToken(params GetGitTokenRequest) (*GetGitTokenResponse, error) { req := request{Method: "getGitToken", Params: params} rawResult, err := service.sendRequest(req) if err != nil { return nil, err } var res GetGitTokenResponse err = json.Unmarshal(rawResult, &res) if err != nil { return nil, err } return &res, nil } // OpenPreview opens a file using Theia's preview capability func (service *HTTPTheiaService) OpenPreview(params OpenPreviewRequest) (*OpenPreviewResponse, error) { req := request{Method: "openPreview", Params: params} _, err := service.sendRequest(req) if err != nil { return nil, err } return &OpenPreviewResponse{}, nil } // OpenFile opens a file in an editor func (service *HTTPTheiaService) OpenFile(params OpenFileRequest) (*OpenFileResponse, error) { absPath, err := filepath.Abs(params.Path) if err != nil { return nil, err } if stat, err := os.Stat(absPath); os.IsNotExist(err) { if err := os.WriteFile(absPath, []byte{}, 0644); err != nil { return nil, err } } else if err != nil { return nil, err } else if stat.IsDir() { return nil, fmt.Errorf("%s is a directory - can only open files", absPath) } params.Path = absPath req := request{Method: "openFile", Params: params} _, err = service.sendRequest(req) if err != nil { return nil, err } return &OpenFileResponse{}, nil } // IsFileOpen returns true if a file is open func (service *HTTPTheiaService) IsFileOpen(params IsFileOpenRequest) (*IsFileOpenResponse, error) { absPath, err := filepath.Abs(params.Path) if err != nil { return nil, err } params.Path = absPath req := request{Method: "isFileOpen", Params: params} rawResult, err := service.sendRequest(req) if err != nil { return nil, err } var res IsFileOpenResponse err = json.Unmarshal(rawResult, &res) if err != nil { return nil, err } return &res, nil } // SetEnvVar sets a gitpod environment variable func (service *HTTPTheiaService) SetEnvVar(params SetEnvvarRequest) (*SetEnvvarResponse, error) { req := request{Method: "setEnvVar", Params: params} _, err := service.sendRequest(req) if err != nil { return nil, err } return &SetEnvvarResponse{}, nil } // GetEnvVars returns the list of Gitpod env vars for this workspace func (service *HTTPTheiaService) GetEnvVars(GetEnvvarsRequest) (*GetEnvvarsResponse, error) { req := request{Method: "getEnvVars", Params: GetEnvvarsRequest{}} rawResult, err := service.sendRequest(req) if err != nil { return nil, err } var res GetEnvvarsResponse err = json.Unmarshal(rawResult, &res) if err != nil { return nil, err } return &res, nil } // DeleteEnvVar deletes environment variables func (service *HTTPTheiaService) DeleteEnvVar(params DeleteEnvvarRequest) (*DeleteEnvvarResponse, error) { req := request{Method: "deleteEnvVar", Params: params} rawResult, err := service.sendRequest(req) if err != nil { return nil, err } var res DeleteEnvvarResponse err = json.Unmarshal(rawResult, &res) if err != nil { return nil, err } return &res, nil } // GetPortURL returns the public, outward-facing URL of a port func (service *HTTPTheiaService) GetPortURL(params GetPortURLRequest) (*GetPortURLResponse, error) { req := request{Method: "getPortURL", Params: params} rawResult, err := service.sendRequest(req) if err != nil { return nil, err } var res GetPortURLResponse err = json.Unmarshal(rawResult, &res) if err != nil { return nil, err } return &res, nil }