Sven Efftinge f8086b9a25 [server] use owner and repo name for workspace id
This change introduces optional arguments in generateWorkspaceId
for the first two segments. And makes use of it in workspace factory
using the repos org/group and name.

fixes https://github.com/gitpod-io/gitpod/issues/4129
2022-01-07 10:33:00 +01:00

317 lines
8.4 KiB
Go

// Copyright (c) 2021 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 main
import (
_ "embed"
"path/filepath"
"runtime"
"strconv"
"time"
"context"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
appapi "github.com/gitpod-io/gitpod/local-app/api"
"github.com/gitpod-io/local-app/pkg/auth"
"github.com/gitpod-io/local-app/pkg/bastion"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/sirupsen/logrus"
cli "github.com/urfave/cli/v2"
keyring "github.com/zalando/go-keyring"
"google.golang.org/grpc"
)
var (
// Version : current version
Version string = strings.TrimSpace(version)
//go:embed version.txt
version string
)
func main() {
sshConfig := os.Getenv("GITPOD_LCA_SSH_CONFIG")
if sshConfig == "" {
if runtime.GOOS == "windows" {
sshConfig = filepath.Join(os.TempDir(), "gitpod_ssh_config")
} else {
sshConfig = filepath.Join("/tmp", "gitpod_ssh_config")
}
}
app := cli.App{
Name: "gitpod-local-companion",
Usage: "connect your Gitpod workspaces",
Action: DefaultCommand("run"),
EnableBashCompletion: true,
Version: Version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "gitpod-host",
Usage: "URL of the Gitpod installation to connect to",
EnvVars: []string{
"GITPOD_HOST",
},
Value: "https://gitpod.io",
},
&cli.BoolFlag{
Name: "mock-keyring",
Usage: "Don't use system native keyring, but store Gitpod token in memory",
},
&cli.BoolFlag{
Name: "allow-cors-from-port",
Usage: "Allow CORS requests from workspace port location",
},
&cli.IntFlag{
Name: "api-port",
Usage: "Local App API endpoint's port",
EnvVars: []string{
"GITPOD_LCA_API_PORT",
},
Value: 63100,
},
&cli.BoolFlag{
Name: "auto-tunnel",
Usage: "Enable auto tunneling",
EnvVars: []string{
"GITPOD_LCA_AUTO_TUNNEL",
},
Value: true,
},
&cli.StringFlag{
Name: "auth-redirect-url",
EnvVars: []string{
"GITPOD_LCA_AUTH_REDIRECT_URL",
},
},
&cli.BoolFlag{
Name: "verbose",
Usage: "Enable verbose logging",
EnvVars: []string{
"GITPOD_LCA_VERBOSE",
},
Value: false,
},
&cli.DurationFlag{
Name: "auth-timeout",
Usage: "Auth timeout in seconds",
EnvVars: []string{
"GITPOD_LCA_AUTH_TIMEOUT",
},
Value: 30,
},
&cli.StringFlag{
Name: "timeout",
Usage: "How long the local app can run if last workspace was stopped",
EnvVars: []string{
"GITPOD_LCA_TIMEOUT",
},
Value: "0",
},
},
Commands: []*cli.Command{
{
Name: "run",
Action: func(c *cli.Context) error {
if c.Bool("mock-keyring") {
keyring.MockInit()
}
return run(c.String("gitpod-host"), c.String("ssh_config"), c.Int("api-port"), c.Bool("allow-cors-from-port"),
c.Bool("auto-tunnel"), c.String("auth-redirect-url"), c.Bool("verbose"), c.Duration("auth-timeout"), c.Duration("timeout"))
},
Flags: []cli.Flag{
&cli.PathFlag{
Name: "ssh_config",
Usage: "produce and update an OpenSSH compatible ssh_config file (defaults to $GITPOD_LCA_SSH_CONFIG)",
Value: sshConfig,
},
},
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func DefaultCommand(name string) cli.ActionFunc {
return func(ctx *cli.Context) error {
return ctx.App.Command(name).Run(ctx)
}
}
func run(origin, sshConfig string, apiPort int, allowCORSFromPort bool, autoTunnel bool, authRedirectUrl string, verbose bool, authTimeout time.Duration, localAppTimeout time.Duration) error {
if verbose {
logrus.SetLevel(logrus.DebugLevel)
}
logrus.WithField("ssh_config", sshConfig).Info("writing workspace ssh_config file")
// Trailing slash(es) result in connection issues, so remove them preemptively
origin = strings.TrimRight(origin, "/")
originURL, err := url.Parse(origin)
if err != nil {
return err
}
wsHostRegex := "(\\.[^.]+)\\." + strings.ReplaceAll(originURL.Host, ".", "\\.")
wsHostRegex = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11})" + wsHostRegex
if allowCORSFromPort {
wsHostRegex = "([0-9]+)-" + wsHostRegex
}
hostRegex, err := regexp.Compile("^" + wsHostRegex)
if err != nil {
return err
}
var b *bastion.Bastion
client, err := connectToServer(auth.LoginOpts{GitpodURL: origin, RedirectURL: authRedirectUrl, AuthTimeout: authTimeout}, func() {
if b != nil {
b.FullUpdate()
}
}, func(closeErr error) {
logrus.WithError(closeErr).Error("server connection failed")
os.Exit(1)
})
if err != nil {
return err
}
cb := bastion.CompositeCallbacks{
&logCallbacks{},
}
s := &bastion.SSHConfigWritingCallback{Path: sshConfig}
if sshConfig != "" {
cb = append(cb, s)
}
b = bastion.New(client, localAppTimeout, cb)
b.EnableAutoTunnel = autoTunnel
grpcServer := grpc.NewServer()
appapi.RegisterLocalAppServer(grpcServer, bastion.NewLocalAppService(b, s))
allowOrigin := func(origin string) bool {
// Is the origin a subdomain of the installations hostname?
return hostRegex.Match([]byte(origin))
}
go func() {
err := http.ListenAndServe("localhost:"+strconv.Itoa(apiPort), grpcweb.WrapServer(grpcServer,
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
grpcweb.WithOriginFunc(allowOrigin),
grpcweb.WithWebsockets(true),
grpcweb.WithWebsocketOriginFunc(func(req *http.Request) bool {
origin, err := grpcweb.WebsocketRequestOrigin(req)
if err != nil {
return false
}
return allowOrigin(origin)
}),
grpcweb.WithWebsocketPingInterval(15*time.Second),
))
if err != nil {
logrus.WithError(err).Error("API endpoint failed to start")
os.Exit(1)
}
}()
defer grpcServer.Stop()
return b.Run()
}
func connectToServer(loginOpts auth.LoginOpts, reconnectionHandler func(), closeHandler func(error)) (*gitpod.APIoverJSONRPC, error) {
var client *gitpod.APIoverJSONRPC
onClose := func(closeErr error) {
if client != nil {
closeHandler(closeErr)
}
}
tkn, err := auth.GetToken(loginOpts.GitpodURL)
if err != nil {
return nil, err
}
if tkn != "" {
// try to connect with existing token
client, err = tryConnectToServer(loginOpts.GitpodURL, tkn, reconnectionHandler, onClose)
if client != nil {
return client, err
}
_, invalid := err.(*auth.ErrInvalidGitpodToken)
if !invalid {
return nil, err
}
// existing token is invalid, try again
logrus.WithError(err).WithField("origin", loginOpts.GitpodURL).Error()
}
tkn, err = login(loginOpts)
if err != nil {
return nil, err
}
client, err = tryConnectToServer(loginOpts.GitpodURL, tkn, reconnectionHandler, onClose)
return client, err
}
func tryConnectToServer(gitpodUrl string, tkn string, reconnectionHandler func(), closeHandler func(error)) (*gitpod.APIoverJSONRPC, error) {
wshost := gitpodUrl
wshost = strings.ReplaceAll(wshost, "https://", "wss://")
wshost = strings.ReplaceAll(wshost, "http://", "ws://")
wshost += "/api/v1"
client, err := gitpod.ConnectToServer(wshost, gitpod.ConnectToServerOpts{
Context: context.Background(),
Token: tkn,
Log: logrus.NewEntry(logrus.StandardLogger()),
ReconnectionHandler: reconnectionHandler,
CloseHandler: closeHandler,
ExtraHeaders: map[string]string{
"User-Agent": "gitpod/local-companion",
"X-Client-Version": Version,
},
})
if err != nil {
return nil, err
}
err = auth.ValidateToken(client, tkn)
if err == nil {
return client, nil
}
closeErr := client.Close()
if closeErr != nil {
logrus.WithError(closeErr).WithField("origin", gitpodUrl).Warn("failed to close connection to gitpod server")
}
deleteErr := auth.DeleteToken(gitpodUrl)
if deleteErr != nil {
logrus.WithError(deleteErr).WithField("origin", gitpodUrl).Warn("failed to delete gitpod token")
}
return nil, err
}
func login(loginOpts auth.LoginOpts) (string, error) {
tkn, err := auth.Login(context.Background(), loginOpts)
if tkn != "" {
err = auth.SetToken(loginOpts.GitpodURL, tkn)
if err != nil {
logrus.WithField("origin", loginOpts.GitpodURL).Warnf("could not write token to keyring: %s", err)
// Allow to continue
err = nil
}
}
return tkn, err
}
type logCallbacks struct{}
func (*logCallbacks) InstanceUpdate(w *bastion.Workspace) {
logrus.WithField("workspace", w).Info("instance update")
}