mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
1275 lines
32 KiB
Go
1275 lines
32 KiB
Go
// 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 integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/status"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/e2e-framework/klient"
|
|
"sigs.k8s.io/e2e-framework/klient/k8s"
|
|
|
|
// Gitpod uses mysql, so it makes sense to make this DB driver available
|
|
// by default.
|
|
_ "github.com/go-sql-driver/mysql"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
csapi "github.com/gitpod-io/gitpod/content-service/api"
|
|
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
|
|
imgbldr "github.com/gitpod-io/gitpod/image-builder/api"
|
|
"github.com/gitpod-io/gitpod/test/pkg/integration/common"
|
|
wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
|
|
)
|
|
|
|
// API provides access to the individual component's API
|
|
func NewComponentAPI(ctx context.Context, namespace string, kubeconfig string, client klient.Client) *ComponentAPI {
|
|
return &ComponentAPI{
|
|
namespace: namespace,
|
|
kubeconfig: kubeconfig,
|
|
client: client,
|
|
|
|
closerMutex: sync.Mutex{},
|
|
|
|
wsmanStatusMu: sync.Mutex{},
|
|
contentServiceStatusMu: sync.Mutex{},
|
|
imgbldStatusMu: sync.Mutex{},
|
|
|
|
serverStatus: &serverStatus{
|
|
Client: make(map[string]*gitpod.APIoverJSONRPC),
|
|
Token: make(map[string]string),
|
|
},
|
|
}
|
|
}
|
|
|
|
type serverStatus struct {
|
|
Token map[string]string
|
|
Client map[string]*gitpod.APIoverJSONRPC
|
|
}
|
|
|
|
// ComponentAPI provides access to the individual component's API
|
|
type ComponentAPI struct {
|
|
namespace string
|
|
kubeconfig string
|
|
client klient.Client
|
|
|
|
closer []func() error
|
|
closerMutex sync.Mutex
|
|
|
|
serverStatus *serverStatus
|
|
|
|
wsmanStatus struct {
|
|
Port int
|
|
Client wsmanapi.WorkspaceManagerClient
|
|
}
|
|
contentServiceStatus struct {
|
|
Port int
|
|
BlobServiceClient csapi.BlobServiceClient
|
|
ContentService ContentService
|
|
}
|
|
imgbldStatus struct {
|
|
Port int
|
|
Client imgbldr.ImageBuilderClient
|
|
}
|
|
|
|
wsmanStatusMu sync.Mutex
|
|
contentServiceStatusMu sync.Mutex
|
|
imgbldStatusMu sync.Mutex
|
|
}
|
|
|
|
type EncryptionKeyMetadata struct {
|
|
Name string
|
|
Version int
|
|
}
|
|
|
|
type EncryptionKey struct {
|
|
Metadata EncryptionKeyMetadata
|
|
Material []byte
|
|
}
|
|
|
|
type DBConfig struct {
|
|
Host string
|
|
Port int32
|
|
ForwardPort *ForwardPort
|
|
Password string
|
|
EncryptionKeys EncryptionKey
|
|
}
|
|
|
|
type ForwardPort struct {
|
|
PodName string
|
|
RemotePort int32
|
|
}
|
|
|
|
type EncriptedDBData struct {
|
|
Data string `json:"data"`
|
|
KeyParams struct {
|
|
Iv string `json:"iv"`
|
|
} `json:"keyParams"`
|
|
KeyMetadata struct {
|
|
Name string `json:"name"`
|
|
Version int `json:"version"`
|
|
} `json:"keyMetadata"`
|
|
}
|
|
|
|
func EncryptValue(value []byte, key []byte) (data string, iv string) {
|
|
PKCS5Padding := func(ciphertext []byte, blockSize int, after int) []byte {
|
|
padding := (blockSize - len(ciphertext)%blockSize)
|
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
return append(ciphertext, padtext...)
|
|
}
|
|
|
|
ivData := []byte("1234567890123456")
|
|
|
|
block, _ := aes.NewCipher(key)
|
|
mode := cipher.NewCBCEncrypter(block, ivData)
|
|
|
|
paddedValue := PKCS5Padding(value, aes.BlockSize, len(value))
|
|
ciphertext := make([]byte, len(paddedValue))
|
|
mode.CryptBlocks(ciphertext, paddedValue)
|
|
|
|
data = base64.StdEncoding.EncodeToString(ciphertext)
|
|
iv = base64.StdEncoding.EncodeToString(ivData)
|
|
|
|
return
|
|
}
|
|
|
|
// Storage provides a url of the storage provider
|
|
// it takes a url as input and creates a port forward if required
|
|
// e.g. when minio running in gitpod cluster
|
|
// and modifies the url to refer to the localhost instead of dns name
|
|
func (c *ComponentAPI) Storage(connUrl string) (string, error) {
|
|
u, err := url.Parse(connUrl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
host, port, _ := net.SplitHostPort(u.Host)
|
|
if !strings.HasSuffix(host, ".svc.cluster.local") {
|
|
return connUrl, nil
|
|
}
|
|
serviceName := strings.Split(host, ".")[0]
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
targetPort, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfSvc, serviceName, localPort, targetPort)
|
|
if err != nil {
|
|
cancel()
|
|
return "", err
|
|
}
|
|
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
|
|
return strings.Replace(connUrl, u.Host, fmt.Sprintf("localhost:%d", localPort), 1), nil
|
|
}
|
|
|
|
// Supervisor provides a gRPC connection to a workspace's supervisor
|
|
func (c *ComponentAPI) Supervisor(instanceID string) (grpc.ClientConnInterface, error) {
|
|
pod, _, err := selectPod(ComponentWorkspace, selectPodOptions{
|
|
InstanceID: instanceID,
|
|
}, c.namespace, c.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, pod, localPort, 8080)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
|
|
conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", localPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.appendCloser(conn.Close)
|
|
return conn, nil
|
|
}
|
|
|
|
type gitpodServerOpts struct {
|
|
User string
|
|
}
|
|
|
|
// GitpodServerOpt specificies Gitpod server access
|
|
type GitpodServerOpt func(*gitpodServerOpts) error
|
|
|
|
// WithGitpodUser specifies the user as which we want to access the API.
|
|
func WithGitpodUser(name string) GitpodServerOpt {
|
|
return func(o *gitpodServerOpts) error {
|
|
o.User = name
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (c *ComponentAPI) CreateOAuth2Token(user string, scopes []string) (string, error) {
|
|
tkn, err := c.createGitpodToken(user, scopes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return tkn, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) ClearGitpodServerClientCache() {
|
|
c.serverStatus.Client = map[string]*gitpod.APIoverJSONRPC{}
|
|
}
|
|
|
|
// GitpodServer provides access to the Gitpod server API
|
|
func (c *ComponentAPI) GitpodServer(opts ...GitpodServerOpt) (gitpod.APIInterface, error) {
|
|
var options gitpodServerOpts
|
|
for _, o := range opts {
|
|
err := o(&options)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot access Gitpod server API: %q", err)
|
|
}
|
|
}
|
|
|
|
if cl, ok := c.serverStatus.Client[options.User]; ok {
|
|
return cl, nil
|
|
}
|
|
|
|
var res gitpod.APIInterface
|
|
err := func() error {
|
|
tkn := c.serverStatus.Token[options.User]
|
|
if tkn == "" {
|
|
var err error
|
|
tkn, err = c.createGitpodToken(options.User, []string{
|
|
"resource:default",
|
|
"function:*",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.serverStatus.Token[options.User] = tkn
|
|
}
|
|
|
|
var pods corev1.PodList
|
|
err := c.client.Resources(c.namespace).List(context.Background(), &pods, func(opts *metav1.ListOptions) {
|
|
opts.LabelSelector = "component=server"
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config, err := GetServerConfig(c.namespace, c.client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostURL := config.HostURL
|
|
if hostURL == "" {
|
|
return xerrors.Errorf("server config: empty HostURL")
|
|
}
|
|
|
|
hostURL = strings.ReplaceAll(hostURL, "http://", "ws://")
|
|
hostURL = strings.ReplaceAll(hostURL, "https://", "wss://")
|
|
endpoint, err := url.Parse(hostURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
endpoint.Path = "/api/v1"
|
|
|
|
cl, err := gitpod.ConnectToServer(endpoint.String(), gitpod.ConnectToServerOpts{
|
|
Token: tkn,
|
|
Log: log.Log,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.serverStatus.Client[options.User] = cl
|
|
res = cl
|
|
c.appendCloser(cl.Close)
|
|
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot access Gitpod server API: %q", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) GetServerEndpoint() (string, error) {
|
|
config, err := GetServerConfig(c.namespace, c.client)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hostURL := config.HostURL
|
|
if hostURL == "" {
|
|
return "", xerrors.Errorf("server config: empty HostURL")
|
|
}
|
|
|
|
endpoint, err := url.Parse(hostURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fmt.Sprintf("%s://%s/", "https", endpoint.Hostname()), nil
|
|
}
|
|
|
|
func (c *ComponentAPI) GitpodSessionCookie(userId string, secretKey string) (*http.Cookie, error) {
|
|
var res *http.Cookie
|
|
err := func() error {
|
|
config, err := GetServerConfig(c.namespace, c.client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostURL := config.HostURL
|
|
if hostURL == "" {
|
|
return xerrors.Errorf("server config: empty HostURL")
|
|
}
|
|
|
|
endpoint, err := url.Parse(hostURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
origin := fmt.Sprintf("%s://%s/", "https", endpoint.Hostname())
|
|
|
|
client := &http.Client{
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
req, _ := http.NewRequest("GET", hostURL+fmt.Sprintf("/api/login/ots/%s/%s", userId, secretKey), nil)
|
|
req.Header.Set("Origin", origin)
|
|
req.Header.Set("Cache-Control", "no-store")
|
|
|
|
httpresp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cookies := httpresp.Cookies()
|
|
if len(cookies) > 0 {
|
|
res = cookies[0]
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res == nil {
|
|
return nil, xerrors.Errorf("Server did not provide a session cookie")
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) GetUserId(user string) (userId string, err error) {
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var row *sql.Row
|
|
if user == "" {
|
|
row = db.QueryRow(`SELECT id FROM d_b_user WHERE NOT id = "` + gitpodBuiltinUserID + `" AND blocked = FALSE AND markedDeleted = FALSE`)
|
|
} else {
|
|
row = db.QueryRow("SELECT id FROM d_b_user WHERE name = ?", user)
|
|
}
|
|
|
|
var id string
|
|
err = row.Scan(&id)
|
|
if err == sql.ErrNoRows {
|
|
return "", xerrors.Errorf("no suitable user found: make sure there's at least one non-builtin user in the database (e.g. login)")
|
|
}
|
|
if err != nil {
|
|
return "", xerrors.Errorf("cannot look for users: %w", err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) UpdateUserFeatureFlag(userId, featureFlag string) error {
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = db.Exec("SELECT id FROM d_b_user WHERE id = ?", userId); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = db.Exec("UPDATE d_b_user SET featureFlags=? WHERE id = ?", fmt.Sprintf("{\"permanentWSFeatureFlags\":[%q]}", featureFlag), userId); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ComponentAPI) CreateUser(username string, token string) (string, error) {
|
|
dbConfig, err := FindDBConfigFromPodEnv("server", c.namespace, c.client)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var userId string
|
|
err = db.QueryRow(`SELECT id FROM d_b_user WHERE name = ?`, username).Scan(&userId)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return "", err
|
|
}
|
|
|
|
if userId == "" {
|
|
userUuid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
userId = userUuid.String()
|
|
_, err = db.Exec(`INSERT IGNORE INTO d_b_user (id, creationDate, avatarUrl, name, fullName, featureFlags) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
userId,
|
|
time.Now().Format(time.RFC3339),
|
|
"",
|
|
username,
|
|
username,
|
|
"{\"permanentWSFeatureFlags\":[]}",
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
var authId string
|
|
err = db.QueryRow(`SELECT authId FROM d_b_identity WHERE userId = ?`, userId).Scan(&authId)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return "", err
|
|
}
|
|
if authId == "" {
|
|
authId = strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
_, err = db.Exec(`INSERT IGNORE INTO d_b_identity (authProviderId, authId, authName, userId, tokens) VALUES (?, ?, ?, ?, ?)`,
|
|
"Public-GitHub",
|
|
authId,
|
|
username,
|
|
userId,
|
|
"[]",
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
var cnt int
|
|
err = db.QueryRow(`SELECT COUNT(1) AS cnt FROM d_b_token_entry WHERE authId = ?`, authId).Scan(&cnt)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return "", err
|
|
}
|
|
if cnt == 0 {
|
|
uid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Double Marshalling to be compatible with EncryptionServiceImpl
|
|
value := struct {
|
|
Value string `json:"value"`
|
|
Scopes []string `json:"scopes"`
|
|
}{
|
|
Value: token,
|
|
Scopes: []string{"user:email", "read:user", "public_repo"},
|
|
}
|
|
valueBytes, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
valueBytes2, err := json.Marshal(string(valueBytes))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
encryptedData, iv := EncryptValue(valueBytes2, dbConfig.EncryptionKeys.Material)
|
|
encrypted := EncriptedDBData{}
|
|
encrypted.Data = encryptedData
|
|
encrypted.KeyParams.Iv = iv
|
|
encrypted.KeyMetadata.Name = dbConfig.EncryptionKeys.Metadata.Name
|
|
encrypted.KeyMetadata.Version = dbConfig.EncryptionKeys.Metadata.Version
|
|
encryptedJson, err := json.Marshal(encrypted)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = db.Exec(`INSERT IGNORE INTO d_b_token_entry (authProviderId, authId, token, uid) VALUES (?, ?, ?, ?)`,
|
|
"Public-GitHub",
|
|
authId,
|
|
encryptedJson,
|
|
uid.String(),
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return userId, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) MakeUserUnleashedPlan(username string) error {
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
var userId string
|
|
err = db.QueryRow(`SELECT id FROM d_b_user WHERE name = ?`, username).Scan(&userId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var subId string
|
|
err = db.QueryRow(`SELECT uid FROM d_b_subscription WHERE userId = ? and planId = ?`, username, "professional-eur").Scan(&subId)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
if subId != "" {
|
|
return nil
|
|
}
|
|
|
|
// reset all of this user subscription
|
|
_, err = db.Exec(`DELETE from d_b_subscription WHERE userId = ?`, userId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = db.Exec(`INSERT INTO d_b_subscription (uid, userId, startDate, amount, planId) VALUES (?, ?, ?, ?, ?)`,
|
|
uid,
|
|
userId,
|
|
"2022-10-19T00:00:00.000Z",
|
|
11904,
|
|
"professional-eur",
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *ComponentAPI) createGitpodToken(user string, scopes []string) (tkn string, err error) {
|
|
id, err := c.GetUserId(user)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rawTkn, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tkn = rawTkn.String()
|
|
|
|
hash := sha256.New()
|
|
hash.Write([]byte(tkn))
|
|
hashVal := fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
// see https://github.com/gitpod-io/gitpod/blob/master/components/gitpod-protocol/src/protocol.ts#L274
|
|
const tokenTypeMachineAuthToken = 1
|
|
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = db.Exec("INSERT INTO d_b_gitpod_token (tokenHash, name, type, userId, scopes, created) VALUES (?, ?, ?, ?, ?, ?)",
|
|
hashVal,
|
|
fmt.Sprintf("integration-test-%d", time.Now().UnixNano()),
|
|
tokenTypeMachineAuthToken,
|
|
id,
|
|
strings.Join(scopes, ","),
|
|
time.Now().Format(time.RFC3339),
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
c.appendCloser(func() error {
|
|
_, err := db.Exec("DELETE FROM d_b_gitpod_token WHERE tokenHash = ?", hashVal)
|
|
return err
|
|
})
|
|
|
|
return tkn, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) CreateGitpodOneTimeSecret(value string) (id string, err error) {
|
|
dbConfig, err := FindDBConfigFromPodEnv("server", c.namespace, c.client)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
db, err := c.DB()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rawUuid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
id = rawUuid.String()
|
|
|
|
// Double Marshalling to be compatible with EncryptionServiceImpl
|
|
valueBytes, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
valueBytes2, err := json.Marshal(string(valueBytes))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
encryptedData, iv := EncryptValue(valueBytes2, dbConfig.EncryptionKeys.Material)
|
|
encrypted := EncriptedDBData{}
|
|
encrypted.Data = encryptedData
|
|
encrypted.KeyParams.Iv = iv
|
|
encrypted.KeyMetadata.Name = dbConfig.EncryptionKeys.Metadata.Name
|
|
encrypted.KeyMetadata.Version = dbConfig.EncryptionKeys.Metadata.Version
|
|
encryptedJson, err := json.Marshal(encrypted)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = db.Exec("INSERT INTO d_b_one_time_secret (id, value, expirationTime, deleted) VALUES (?, ?, ?, ?)",
|
|
id,
|
|
string(encryptedJson),
|
|
time.Now().Add(30*time.Minute).UTC().Format("2006-01-02 15:04:05.999999"),
|
|
false,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
c.appendCloser(func() error {
|
|
_, err := db.Exec("DELETE FROM d_b_one_time_secret WHERE id = ?", id)
|
|
return err
|
|
})
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// WorkspaceManager provides access to ws-manager
|
|
func (c *ComponentAPI) WorkspaceManager() (wsmanapi.WorkspaceManagerClient, error) {
|
|
if c.wsmanStatus.Client != nil {
|
|
return c.wsmanStatus.Client, nil
|
|
}
|
|
|
|
if c.wsmanStatus.Port == 0 {
|
|
c.wsmanStatusMu.Lock()
|
|
defer c.wsmanStatusMu.Unlock()
|
|
|
|
pod, _, err := selectPod(ComponentWorkspaceManager, selectPodOptions{}, c.namespace, c.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, pod, localPort, 8080)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
c.wsmanStatus.Port = localPort
|
|
}
|
|
|
|
secretName := "ws-manager-client-tls"
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
|
|
var secret corev1.Secret
|
|
err := c.client.Resources().Get(ctx, secretName, c.namespace, &secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
caCrt := secret.Data["ca.crt"]
|
|
tlsCrt := secret.Data["tls.crt"]
|
|
tlsKey := secret.Data["tls.key"]
|
|
|
|
certPool := x509.NewCertPool()
|
|
if !certPool.AppendCertsFromPEM(caCrt) {
|
|
return nil, xerrors.Errorf("failed appending CA cert")
|
|
}
|
|
cert, err := tls.X509KeyPair(tlsCrt, tlsKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
creds := credentials.NewTLS(&tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
RootCAs: certPool,
|
|
ServerName: "ws-manager",
|
|
})
|
|
dialOption := grpc.WithTransportCredentials(creds)
|
|
|
|
wsport := fmt.Sprintf("localhost:%d", c.wsmanStatus.Port)
|
|
conn, err := grpc.Dial(wsport, dialOption)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.appendCloser(conn.Close)
|
|
|
|
c.wsmanStatus.Client = wsmanapi.NewWorkspaceManagerClient(conn)
|
|
return c.wsmanStatus.Client, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) ClearWorkspaceManagerClientCache() {
|
|
c.wsmanStatus.Client = nil
|
|
c.wsmanStatus.Port = 0
|
|
}
|
|
|
|
// BlobService provides access to the blob service of the content service
|
|
func (c *ComponentAPI) BlobService() (csapi.BlobServiceClient, error) {
|
|
if c.contentServiceStatus.BlobServiceClient != nil {
|
|
return c.contentServiceStatus.BlobServiceClient, nil
|
|
}
|
|
|
|
if c.contentServiceStatus.Port == 0 {
|
|
c.contentServiceStatusMu.Lock()
|
|
defer c.contentServiceStatusMu.Unlock()
|
|
|
|
pod, _, err := selectPod(ComponentContentService, selectPodOptions{}, c.namespace, c.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, pod, localPort, 8080)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
c.contentServiceStatus.Port = localPort
|
|
}
|
|
|
|
conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", c.contentServiceStatus.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.appendCloser(conn.Close)
|
|
|
|
c.contentServiceStatus.BlobServiceClient = csapi.NewBlobServiceClient(conn)
|
|
return c.contentServiceStatus.BlobServiceClient, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) ClearBlobServiceClientCache() {
|
|
c.contentServiceStatus.BlobServiceClient = nil
|
|
c.contentServiceStatus.Port = 0
|
|
}
|
|
|
|
type dbOpts struct {
|
|
Database string
|
|
}
|
|
|
|
// DNOpt configures DB access
|
|
type DBOpt func(*dbOpts)
|
|
|
|
// DBName forces a particular database
|
|
func DBName(name string) DBOpt {
|
|
return func(o *dbOpts) {
|
|
o.Database = name
|
|
}
|
|
}
|
|
|
|
// DB provides access to the Gitpod database.
|
|
// Callers must never close the DB.
|
|
func (c *ComponentAPI) DB(options ...DBOpt) (*sql.DB, error) {
|
|
opts := dbOpts{
|
|
Database: "gitpod",
|
|
}
|
|
for _, o := range options {
|
|
o(&opts)
|
|
}
|
|
|
|
config, err := c.findDBConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if configured: setup local port-forward to DB pod
|
|
if config.ForwardPort != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, config.ForwardPort.PodName, int(config.Port), int(config.ForwardPort.RemotePort))
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
}
|
|
|
|
db, err := sql.Open("mysql", fmt.Sprintf("gitpod:%s@tcp(%s:%d)/%s", config.Password, config.Host, config.Port, opts.Database))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.appendCloser(db.Close)
|
|
return db, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) findDBConfig() (*DBConfig, error) {
|
|
config, err := FindDBConfigFromPodEnv("server", c.namespace, c.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// here we _assume_ that "config" points to a service: find us a concrete DB pod to forward to
|
|
var svc corev1.Service
|
|
err = c.client.Resources(c.namespace).Get(context.Background(), config.Host, c.namespace, &svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// find remotePort
|
|
var remotePort int32
|
|
for _, p := range svc.Spec.Ports {
|
|
if p.Port == config.Port {
|
|
remotePort = p.TargetPort.IntVal
|
|
if remotePort == 0 {
|
|
remotePort = p.Port
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if remotePort == 0 {
|
|
return nil, xerrors.Errorf("no ports found on service: %s", svc.Name)
|
|
}
|
|
|
|
// find pod to forward to
|
|
var pods corev1.PodList
|
|
err = c.client.Resources(c.namespace).List(context.Background(), &pods, func(opts *metav1.ListOptions) {
|
|
opts.LabelSelector = labels.SelectorFromSet(svc.Spec.Selector).String()
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pods.Items) == 0 {
|
|
return nil, xerrors.Errorf("no pods for service %s found", svc.Name)
|
|
}
|
|
var pod *corev1.Pod
|
|
for _, p := range pods.Items {
|
|
if p.Spec.NodeName == "" {
|
|
// no node means the pod can't be ready
|
|
continue
|
|
}
|
|
var isReady bool
|
|
for _, cond := range p.Status.Conditions {
|
|
if cond.Type == corev1.PodReady {
|
|
isReady = cond.Status == corev1.ConditionTrue
|
|
break
|
|
}
|
|
}
|
|
if !isReady {
|
|
continue
|
|
}
|
|
|
|
pod = &p
|
|
break
|
|
}
|
|
if pod == nil {
|
|
return nil, xerrors.Errorf("no active pod for service %s found", svc.Name)
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.Port = int32(localPort)
|
|
config.ForwardPort = &ForwardPort{
|
|
RemotePort: remotePort,
|
|
PodName: pod.Name,
|
|
}
|
|
config.Host = "127.0.0.1"
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func FindDBConfigFromPodEnv(componentName string, namespace string, client klient.Client) (*DBConfig, error) {
|
|
lblSelector := fmt.Sprintf("component=%s", componentName)
|
|
var list corev1.PodList
|
|
err := client.Resources(namespace).List(context.Background(), &list, func(opts *metav1.ListOptions) {
|
|
opts.LabelSelector = lblSelector
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(list.Items) == 0 {
|
|
return nil, xerrors.Errorf("no pods found for: %s", lblSelector)
|
|
}
|
|
pod := list.Items[0]
|
|
|
|
var password, host string
|
|
var dbEncryptionKeys *EncryptionKey
|
|
var port int32
|
|
OuterLoop:
|
|
for _, c := range pod.Spec.Containers {
|
|
for _, v := range c.Env {
|
|
var findErr error
|
|
if v.Name == "DB_PASSWORD" {
|
|
password, findErr = FindValueFromEnvVar(v, client, namespace)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
} else if v.Name == "DB_ENCRYPTION_KEYS" {
|
|
raw, findErr := FindValueFromEnvVar(v, client, namespace)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
|
|
var k []struct {
|
|
Name string `json:"name"`
|
|
Version int `json:"version"`
|
|
Material []byte `json:"material"`
|
|
}
|
|
err = json.Unmarshal([]byte(raw), &k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(k) > 0 {
|
|
dbEncryptionKeys = &EncryptionKey{
|
|
Metadata: EncryptionKeyMetadata{
|
|
Name: k[0].Name,
|
|
Version: k[0].Version,
|
|
},
|
|
Material: k[0].Material,
|
|
}
|
|
}
|
|
} else if v.Name == "DB_PORT" {
|
|
var portStr string
|
|
portStr, findErr = FindValueFromEnvVar(v, client, namespace)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
pPort, err := strconv.ParseUint(portStr, 10, 16)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("error parsing DB_PORT '%s' on pod %s!", v.Value, pod.Name)
|
|
}
|
|
port = int32(pPort)
|
|
} else if v.Name == "DB_HOST" {
|
|
host, findErr = FindValueFromEnvVar(v, client, namespace)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
}
|
|
if password != "" && port != 0 && host != "" && dbEncryptionKeys != nil {
|
|
break OuterLoop
|
|
}
|
|
}
|
|
}
|
|
if password == "" || port == 0 || host == "" || dbEncryptionKeys == nil {
|
|
return nil, xerrors.Errorf("could not find complete DBConfig on pod %s!", pod.Name)
|
|
}
|
|
config := DBConfig{
|
|
Host: host,
|
|
Port: port,
|
|
Password: password,
|
|
EncryptionKeys: *dbEncryptionKeys,
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
func FindValueFromEnvVar(ev corev1.EnvVar, client klient.Client, namespace string) (string, error) {
|
|
// we have a value, just return it
|
|
if ev.Value != "" {
|
|
return ev.Value, nil
|
|
}
|
|
|
|
if ev.ValueFrom == nil {
|
|
return "", xerrors.Errorf("Neither Value or ValueFrom exist for %s", ev.Name)
|
|
}
|
|
|
|
// value doesn't exist for ENV VARs set by config or secret
|
|
// instead, valueFrom will contain a reference to the backing config or secret
|
|
// secret references look like:
|
|
// '{"name":"DB_PORT","valueFrom":{"secretKeyRef":{"name":"mysql","key":"port"}}}'
|
|
if ev.ValueFrom.SecretKeyRef != nil {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var secret corev1.Secret
|
|
secretRef := ev.ValueFrom.SecretKeyRef
|
|
err := client.Resources().Get(ctx, secretRef.Name, namespace, &secret)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
secretValue := string(secret.Data[secretRef.Key])
|
|
return secretValue, nil
|
|
} else {
|
|
return "", xerrors.Errorf("A secret reference was expected for %s", ev.Name)
|
|
}
|
|
}
|
|
|
|
// APIImageBuilderOpt configures the image builder API access
|
|
type APIImageBuilderOpt func(*apiImageBuilderOpts)
|
|
|
|
type apiImageBuilderOpts struct {
|
|
SelectMK3 bool
|
|
}
|
|
|
|
// ImageBuilder provides access to the image builder service.
|
|
func (c *ComponentAPI) ImageBuilder(opts ...APIImageBuilderOpt) (imgbldr.ImageBuilderClient, error) {
|
|
var cfg apiImageBuilderOpts
|
|
for _, o := range opts {
|
|
o(&cfg)
|
|
}
|
|
|
|
if c.imgbldStatus.Client != nil {
|
|
return c.imgbldStatus.Client, nil
|
|
}
|
|
|
|
err := func() error {
|
|
if c.imgbldStatus.Port == 0 {
|
|
c.imgbldStatusMu.Lock()
|
|
defer c.imgbldStatusMu.Unlock()
|
|
|
|
pod, _, err := selectPod(ComponentImageBuilderMK3, selectPodOptions{}, c.namespace, c.client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, pod, localPort, 8080)
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
c.imgbldStatus.Port = localPort
|
|
}
|
|
|
|
conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", c.imgbldStatus.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.appendCloser(conn.Close)
|
|
|
|
c.imgbldStatus.Client = imgbldr.NewImageBuilderClient(conn)
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.imgbldStatus.Client, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) ClearImageBuilderClientCache() {
|
|
c.imgbldStatus.Client = nil
|
|
c.imgbldStatus.Port = 0
|
|
}
|
|
|
|
// ContentService groups content service interfaces for convenience
|
|
type ContentService interface {
|
|
csapi.ContentServiceClient
|
|
csapi.WorkspaceServiceClient
|
|
}
|
|
|
|
func (c *ComponentAPI) ContentService() (ContentService, error) {
|
|
if c.contentServiceStatus.ContentService != nil {
|
|
return c.contentServiceStatus.ContentService, nil
|
|
}
|
|
if c.contentServiceStatus.Port == 0 {
|
|
pod, _, err := selectPod(ComponentContentService, selectPodOptions{}, c.namespace, c.client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localPort, err := getFreePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
err = c.portFwdWithRetry(ctx, common.ForwardPortOfPod, pod, localPort, 8080)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
c.appendCloser(func() error { cancel(); return nil })
|
|
c.contentServiceStatus.Port = localPort
|
|
}
|
|
|
|
conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", c.contentServiceStatus.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.appendCloser(conn.Close)
|
|
|
|
type cs struct {
|
|
csapi.ContentServiceClient
|
|
csapi.WorkspaceServiceClient
|
|
}
|
|
|
|
c.contentServiceStatus.ContentService = cs{
|
|
ContentServiceClient: csapi.NewContentServiceClient(conn),
|
|
WorkspaceServiceClient: csapi.NewWorkspaceServiceClient(conn),
|
|
}
|
|
|
|
return c.contentServiceStatus.ContentService, nil
|
|
}
|
|
|
|
func (c *ComponentAPI) ClearContentServiceClientCache() {
|
|
c.contentServiceStatus.ContentService = nil
|
|
c.contentServiceStatus.Port = 0
|
|
}
|
|
|
|
func (c *ComponentAPI) Done(t *testing.T) {
|
|
// Much "defer", we run the closer in reversed order. This way, we can
|
|
// append to this list quite naturally, and still break things down in
|
|
// the correct order.
|
|
for i := len(c.closer) - 1; i >= 0; i-- {
|
|
err := c.closer[i]()
|
|
if err != nil {
|
|
t.Logf("cleanup failed: %q", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *ComponentAPI) appendCloser(closer func() error) {
|
|
c.closerMutex.Lock()
|
|
defer c.closerMutex.Unlock()
|
|
c.closer = append(c.closer, closer)
|
|
}
|
|
|
|
type portFwdFunc = func(ctx context.Context, kubeconfig string, namespace, name, port string) (chan struct{}, chan error)
|
|
|
|
func (c *ComponentAPI) portFwdWithRetry(ctx context.Context, portFwdF portFwdFunc, serviceName string, localPort int, targetPort int) error {
|
|
for {
|
|
ready, errc := portFwdF(ctx, c.kubeconfig, c.namespace, serviceName, fmt.Sprintf("%d:%d", localPort, targetPort))
|
|
select {
|
|
case err := <-errc:
|
|
if err == io.EOF {
|
|
time.Sleep(10 * time.Second)
|
|
} else if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
|
|
time.Sleep(10 * time.Second)
|
|
} else {
|
|
return err
|
|
}
|
|
case <-ready:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *ComponentAPI) IsPVCExist(pvcName string) bool {
|
|
var pvc corev1.PersistentVolumeClaim
|
|
return c.client.Resources().Get(context.Background(), pvcName, c.namespace, &pvc) == nil
|
|
}
|
|
|
|
// RestartDeployment rollout restart the deployment by updating the
|
|
// spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"] = time.Now()
|
|
func (c *ComponentAPI) RestartDeployment(deployName, namespace string, wait bool) error {
|
|
var deploy appsv1.Deployment
|
|
if err := c.client.Resources().WithNamespace(namespace).Get(context.Background(), deployName, namespace, &deploy); err != nil {
|
|
return err
|
|
}
|
|
|
|
patchData := map[string]interface{}{
|
|
"spec": map[string]interface{}{
|
|
"template": map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"annotations": map[string]interface{}{
|
|
"kubectl.kubernetes.io/restartedAt": time.Now().Format(time.Stamp),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
encodedPatchData, err := json.Marshal(patchData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.client.Resources().WithNamespace(namespace).Patch(context.Background(), &deploy, k8s.Patch{PatchType: types.MergePatchType, Data: encodedPatchData}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !wait {
|
|
return nil
|
|
}
|
|
|
|
// waits for the deployment rollout status, maximum to one minute
|
|
for i := 0; i < 10; i++ {
|
|
if err := c.client.Resources().WithNamespace(namespace).Get(context.Background(), deployName, namespace, &deploy); err != nil {
|
|
return err
|
|
}
|
|
if deploy.Status.UnavailableReplicas == 0 {
|
|
break
|
|
}
|
|
time.Sleep(6 * time.Second)
|
|
}
|
|
return nil
|
|
}
|