// 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) 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
}
