diff --git a/.werft/build.js b/.werft/build.js index 5b5031a6e04312..92d001d21af8bd 100644 --- a/.werft/build.js +++ b/.werft/build.js @@ -107,6 +107,7 @@ async function build(context, version) { const noPreview = "no-preview" in buildConfig || publishRelease; const registryFacadeHandover = "registry-facade-handover" in buildConfig; const storage = buildConfig["storage"] || ""; + const withIntegrationTests = buildConfig["with-integration-tests"] == "true"; werft.log("job config", JSON.stringify({ buildConfig, version, @@ -119,6 +120,7 @@ async function build(context, version) { noPreview, registryFacadeHandover, storage: storage, + withIntegrationTests, })); /** @@ -162,21 +164,35 @@ async function build(context, version) { if (noPreview) { werft.phase("deploy", "not deploying"); console.log("no-preview or publish-release is set"); - } else { - await deployToDev(version, workspaceFeatureFlags, dynamicCPULimits, registryFacadeHandover, storage); + + return } -} + + const destname = version.split(".")[0]; + const namespace = `staging-${destname}`; + const domain = `${destname}.staging.gitpod-dev.com`; + const url = `https://${domain}`; + const deploymentConfig = { + version, + destname, + namespace, + domain, + url, + }; + await deployToDev(deploymentConfig, workspaceFeatureFlags, dynamicCPULimits, registryFacadeHandover, storage); + if (withIntegrationTests) { + exec(`git config --global user.name "${context.Owner}"`); + exec(`werft run --follow-with-prefix="int-tests: " --remote-job-path .werft/run-integration-tests.yaml -a version=${deploymentConfig.version} -a namespace=${deploymentConfig.namespace} github`); + } +} /** * Deploy dev */ -async function deployToDev(version, workspaceFeatureFlags, dynamicCPULimits, registryFacadeHandover, storage) { +async function deployToDev(deploymentConfig, workspaceFeatureFlags, dynamicCPULimits, registryFacadeHandover, storage) { werft.phase("deploy", "deploying to dev"); - const destname = version.split(".")[0]; - const namespace = `staging-${destname}`; - const domain = `${destname}.staging.gitpod-dev.com`; - const url = `https://${domain}`; + const { version, destname, namespace, domain, url } = deploymentConfig; const wsdaemonPort = `1${Math.floor(Math.random()*1000)}`; const registryProxyPort = `2${Math.floor(Math.random()*1000)}`; const registryNodePort = `${30000 + Math.floor(Math.random()*1000)}`; diff --git a/.werft/run-integration-tests.yaml b/.werft/run-integration-tests.yaml new file mode 100644 index 00000000000000..5615b1a3bdb007 --- /dev/null +++ b/.werft/run-integration-tests.yaml @@ -0,0 +1,79 @@ +args: +- name: version + desc: "The version of the integration tests to use" + required: true +- name: namespace + desc: "The namespace to run the integration test against" + required: true +- name: username + desc: "The username to run the integration test with" + required: false +pod: + serviceAccount: werft + nodeSelector: + dev/workload: builds + imagePullSecrets: + - name: eu-gcr-io-pull-secret + volumes: + - name: gcp-sa + secret: + secretName: gcp-sa-gitpod-dev-deployer + - name: config + emptyDir: {} + initContainers: + - name: gcloud + image: eu.gcr.io/gitpod-core-dev/dev/dev-environment:gpl-bump-helm.12 + workingDir: /workspace + imagePullPolicy: Always + volumeMounts: + - name: gcp-sa + mountPath: /mnt/secrets/gcp-sa + readOnly: true + - name: config + mountPath: /config + readOnly: false + command: + - bash + - -c + - | + + echo "[prep] preparing config." + + gcloud auth activate-service-account --key-file /mnt/secrets/gcp-sa/service-account.json + cp -R /home/gitpod/.config/gcloud /config/gcloud + cp /home/gitpod/.kube/config /config/kubeconfig + + echo "[prep] copied config..." + containers: + - name: tests + image: eu.gcr.io/gitpod-core-dev/build/integration-tests:{{ .Annotations.version }} + workingDir: /workspace + imagePullPolicy: Always + volumeMounts: + - name: config + mountPath: /config + readOnly: true + command: + - /bin/bash + - -c + - | + sleep 1 + set -Eeuo pipefail + + echo "[prep] receiving config..." + mkdir /root/.config + cp -R /config/gcloud /root/.config/gcloud + export GOOGLE_APPLICATION_CREDENTIALS=/config/gcloud/legacy_credentials/gitpod-deployer@gitpod-core-dev.iam.gserviceaccount.com/adc.json + echo "[prep] received config." + + USERNAME="{{ .Annotations.username }}" + if [[ "$USERNAME" == "" ]]; then + USERNAME="" + fi + echo "[prep] using username: $USERNAME" + echo "[prep|DONE]" + + /entrypoint.sh -kubeconfig=/config/kubeconfig -namespace={{ .Annotations.namespace }} -username=$USERNAME 2>&1 | ts "[int-tests] " + + RC=${PIPESTATUS[0]} + if [ $RC -eq 1 ]; then echo "[int-tests|FAIL]"; else echo "[int-tests|DONE]"; fi \ No newline at end of file diff --git a/components/gitpod-protocol/go/reconnecting-ws.go b/components/gitpod-protocol/go/reconnecting-ws.go index 86afcc52db5d36..6b81eb871aafe5 100644 --- a/components/gitpod-protocol/go/reconnecting-ws.go +++ b/components/gitpod-protocol/go/reconnecting-ws.go @@ -124,6 +124,8 @@ func (rc *ReconnectingWebsocket) Dial() { case err := <-rc.errCh: log.WithError(err).WithField("url", rc.url).Warn("connection has been closed, reconnecting...") conn.Close() + + time.Sleep(1 * time.Second) conn = rc.connect() } } diff --git a/test/BUILD.yaml b/test/BUILD.yaml index bd75f366c140aa..966acb97e810d4 100644 --- a/test/BUILD.yaml +++ b/test/BUILD.yaml @@ -28,6 +28,8 @@ packages: dontTest: true - name: docker type: docker + srcs: + - entrypoint.sh deps: - :app argdeps: diff --git a/test/README.md b/test/README.md index 7d40e2e1a7e032..539af93dcd3f4b 100644 --- a/test/README.md +++ b/test/README.md @@ -15,4 +15,17 @@ Such tests are for example: - instrumentation: agents that are compiled before/during the test, uploaded to a pod and executed there. They communicate with the test using net/rpc. - API access: to all internal APIs, including ws-manager, ws-daemon, image-builder, registry-facade, server -- DB access to the Gitpod DB \ No newline at end of file +- DB access to the Gitpod DB + +## Run + +There is a [werft job](../.werft/run-integration-tests.yaml) that runs the integration tests against `core-dev` preview environments. + + > For tests that require an existing user the framework tries to automatically select one from the DB. + > - On preview envs make sure to create one before running tests against it! + > - If it's important to use a certain user (with fixed settings, for example) pass the additional `username` parameter. + +Example command: +``` +werft job run github -j .werft/run-integration-tests.yaml -a namespace=staging-gpl-2658-int-tests -a version=gpl-2658-int-tests.57 -f +``` \ No newline at end of file diff --git a/test/entrypoint.sh b/test/entrypoint.sh new file mode 100755 index 00000000000000..130fbbd5941e58 --- /dev/null +++ b/test/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -ex + +for i in $(ls /tests/*.test); do + $i $*; +done diff --git a/test/go.sum b/test/go.sum index 315ba1fd724871..c0a04f915cc034 100644 --- a/test/go.sum +++ b/test/go.sum @@ -106,6 +106,7 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gitpod-io/gitpod v0.6.0-beta3 h1:WGLut/rxCjgg+RDHzMzu1jgztbIlQU8U/aFm4bVudmc= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/test/leeway.Dockerfile b/test/leeway.Dockerfile index 7ee0e22d6526a4..8f76ae0a755bf4 100644 --- a/test/leeway.Dockerfile +++ b/test/leeway.Dockerfile @@ -7,8 +7,11 @@ FROM alpine:3.13 # Ensure latest packages are present, like security updates. RUN apk upgrade --no-cache \ && apk add --no-cache ca-certificates + +# convenience scripting tools +RUN apk add --no-cache bash moreutils COPY test--app/bin /tests ENV PATH=$PATH:/tests -RUN sh -c "echo '#!/bin/sh' > /entrypoint.sh; echo 'set -ex' >> /entrypoint.sh; echo 'for i in \$(ls /tests/*.test); do \$i \$*; done' >> /entrypoint.sh; chmod +x /entrypoint.sh" +COPY entrypoint.sh /entrypoint.sh ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/test/pkg/integration/apis.go b/test/pkg/integration/apis.go index ee6f1ead37bb3b..6816496dec6b25 100644 --- a/test/pkg/integration/apis.go +++ b/test/pkg/integration/apis.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net/url" + "strconv" "strings" "time" @@ -65,9 +66,8 @@ type ComponentAPI struct { BlobServiceClient csapi.BlobServiceClient } dbStatus struct { - Port int - Password string - DB *sql.DB + Config *DBConfig + DB *sql.DB } imgbldStatus struct { Port int @@ -75,6 +75,18 @@ type ComponentAPI struct { } } +type DBConfig struct { + Host string + Port int32 + ForwardPort *ForwardPort + Password string +} + +type ForwardPort struct { + PodName string + RemotePort int32 +} + // Supervisor provides a gRPC connection to a workspace's supervisor func (c *ComponentAPI) Supervisor(instanceID string) (res grpc.ClientConnInterface) { pod, _, err := c.t.selectPod(ComponentWorkspace, selectPodOptions{InstanceID: instanceID}) @@ -202,7 +214,7 @@ func (c *ComponentAPI) createGitpodToken(user string) (tkn string, err error) { row *sql.Row ) if user == "" { - row = db.QueryRow(`SELECT id FROM d_b_user WHERE NOT id = "` + gitpodBuiltinUserID + `"`) + 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) } @@ -370,81 +382,31 @@ func (c *ComponentAPI) DB() *sql.DB { c.t.t.Fatalf("cannot access database: %q", rerr) }() - if c.dbStatus.Port == 0 { - svc, err := c.t.clientset.CoreV1().Services(c.t.namespace).Get(context.Background(), "db", metav1.GetOptions{}) - if err != nil { - rerr = err - return nil - } - pods, err := c.t.clientset.CoreV1().Pods(c.t.namespace).List(context.Background(), metav1.ListOptions{ - LabelSelector: labels.SelectorFromSet(svc.Spec.Selector).String(), - }) + if c.dbStatus.Config == nil { + config, err := c.findDBConfig() if err != nil { rerr = err return nil } - if len(pods.Items) == 0 { - rerr = xerrors.Errorf("no pods for service %s found", svc.Name) - return nil - } - 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 { - rerr = xerrors.Errorf("no active pod for service %s found", svc.Name) - return nil - } + c.dbStatus.Config = config + } + config := c.dbStatus.Config - localPort, err := getFreePort() - if err != nil { - rerr = err - return nil - } + // if configured: setup local port-forward to DB pod + if config.ForwardPort != nil { ctx, cancel := context.WithCancel(context.Background()) - ready, errc := forwardPort(ctx, c.t.restConfig, c.t.namespace, pod.Name, fmt.Sprintf("%d:3306", localPort)) + ready, errc := forwardPort(ctx, c.t.restConfig, c.t.namespace, config.ForwardPort.PodName, fmt.Sprintf("%d:%d", config.Port, config.ForwardPort.RemotePort)) select { - case err = <-errc: + case err := <-errc: cancel() rerr = err return nil case <-ready: } c.t.closer = append(c.t.closer, func() error { cancel(); return nil }) - - c.dbStatus.Port = localPort - } - if c.dbStatus.Password == "" { - sct, err := c.t.clientset.CoreV1().Secrets(c.t.namespace).Get(context.Background(), "db-password", metav1.GetOptions{}) - if err != nil { - rerr = err - return nil - } - pwd, ok := sct.Data["mysql-root-password"] - if !ok { - rerr = xerrors.Errorf("no mysql-root-password data present in secret %s", sct.Name) - return nil - } - c.dbStatus.Password = string(pwd) } - db, err := sql.Open("mysql", fmt.Sprintf("gitpod:%s@tcp(127.0.0.1:%d)/gitpod", c.dbStatus.Password, c.dbStatus.Port)) + db, err := sql.Open("mysql", fmt.Sprintf("gitpod:%s@tcp(%s:%d)/gitpod", config.Password, config.Host, config.Port)) if err != nil { rerr = err return nil @@ -455,6 +417,127 @@ func (c *ComponentAPI) DB() *sql.DB { return db } +func (c *ComponentAPI) findDBConfig() (*DBConfig, error) { + config, err := c.findDBConfigFromPodEnv("server") + if err != nil { + return nil, err + } + + // here we _assume_ that "config" points to a service: find us a concrete DB pod to forward to + svc, err := c.t.clientset.CoreV1().Services(c.t.namespace).Get(context.Background(), config.Host, metav1.GetOptions{}) + 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, fmt.Errorf("no ports found on service: %s", svc.Name) + } + + // find pod to forward to + pods, err := c.t.clientset.CoreV1().Pods(c.t.namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(svc.Spec.Selector).String(), + }) + if err != nil { + return nil, err + } + if len(pods.Items) == 0 { + return nil, fmt.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, fmt.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 (c *ComponentAPI) findDBConfigFromPodEnv(componentName string) (*DBConfig, error) { + lblSelector := fmt.Sprintf("component=%s", componentName) + list, err := c.t.clientset.CoreV1().Pods(c.t.namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: lblSelector, + }) + if err != nil { + return nil, err + } + if len(list.Items) == 0 { + return nil, fmt.Errorf("no pods found for: %s", lblSelector) + } + pod := list.Items[0] + + var password string + var port int32 + var host string +OuterLoop: + for _, c := range pod.Spec.Containers { + for _, v := range c.Env { + if v.Name == "DB_PASSWORD" { + password = v.Value + } else if v.Name == "DB_PORT" { + pPort, err := strconv.Atoi(v.Value) + if err != nil { + return nil, fmt.Errorf("error parsing DB_PORT '%s' on pod %s!", v.Value, pod.Name) + } + port = int32(pPort) + } else if v.Name == "DB_HOST" { + host = v.Value + } + if password != "" && port != 0 && host != "" { + break OuterLoop + } + } + } + if password == "" || port == 0 || host == "" { + return nil, fmt.Errorf("could not find complete DBConfig on pod %s!", pod.Name) + } + config := DBConfig{ + Host: host, + Port: port, + Password: password, + } + return &config, nil +} + // ImageBuilder provides access to the image builder service. func (c *ComponentAPI) ImageBuilder() imgbldr.ImageBuilderClient { if c.imgbldStatus.Client != nil { diff --git a/test/pkg/integration/integration.go b/test/pkg/integration/integration.go index a0020b0920ce21..5f26dfcf04a186 100644 --- a/test/pkg/integration/integration.go +++ b/test/pkg/integration/integration.go @@ -40,18 +40,22 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ) -const cfgflagDefault = "$HOME/.kube/config" +const cfgFlagDefault = "$HOME/.kube/config" var ( - cfgflag = flag.String("kubeconfig", cfgflagDefault, "path to the kubeconfig file, use \"in-cluster\" to make use of in cluster Kubernetes config") + cfgFlag = flag.String("kubeconfig", cfgFlagDefault, "path to the kubeconfig file, use \"in-cluster\" to make use of in cluster Kubernetes config") + namespaceFlag = flag.String("namespace", "", "namespace to execute the test against. Defaults to the one configured in \"kubeconfig\".") + usernameFlag = flag.String("username", "", "username to execute the tests with. Chooses one automatically if left blank.") ) // NewTest produces a new integration test instance -func NewTest(t *testing.T) *Test { +func NewTest(t *testing.T, timeout time.Duration) (*Test, context.Context) { flag.Parse() + kubeconfig := *cfgFlag + namespaceOverride := *namespaceFlag + username := *usernameFlag - kubeconfig := *cfgflag - if kubeconfig == cfgflagDefault { + if kubeconfig == cfgFlagDefault { home, err := os.UserHomeDir() if err != nil { t.Fatal("cannot determine user home dir", err) @@ -70,12 +74,20 @@ func NewTest(t *testing.T) *Test { t.Fatal("cannot connecto Kubernetes", err) } + if namespaceOverride != "" { + ns = namespaceOverride + } + + ctx, ctxCancel := context.WithTimeout(context.Background(), timeout) return &Test{ t: t, clientset: client, restConfig: restConfig, namespace: ns, - } + ctx: ctx, + ctxCancel: ctxCancel, + username: username, + }, ctx } // GetKubeconfig loads kubernetes connection config from a kubeconfig file @@ -118,25 +130,37 @@ type Test struct { clientset kubernetes.Interface restConfig *rest.Config namespace string + ctx context.Context + + closer []func() error + ctxCancel func() + api *ComponentAPI - closer []func() error - api *ComponentAPI + // username contains the string passed to the test per flag. Might be empty. + username string } // Done must be called after the test has run. It cleans up instrumentation // and modifications made by the test. -func (t *Test) Done() { +func (it *Test) Done() { + it.ctxCancel() + // 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(t.closer) - 1; i >= 0; i-- { - err := t.closer[i]() + for i := len(it.closer) - 1; i >= 0; i-- { + err := it.closer[i]() if err != nil { - t.t.Logf("cleanup failed: %q", err) + it.t.Logf("cleanup failed: %q", err) } } } +// Username returns the username passed to the test per flag. Might be empty. +func (it *Test) Username() string { + return it.username +} + // InstrumentOption configures an Instrument call type InstrumentOption func(*instrumentOptions) error @@ -165,7 +189,7 @@ func WithInstanceID(instanceID string) InstrumentOption { // The binary is copied to the destination pod, started and port-forwarded. Then we // create an RPC client. // Test.Done() will stop the agent and port-forwarding. -func (t *Test) Instrument(component ComponentType, agentName string, opts ...InstrumentOption) (agent *rpc.Client, err error) { +func (it *Test) Instrument(component ComponentType, agentName string, opts ...InstrumentOption) (agent *rpc.Client, err error) { var options instrumentOptions for _, o := range opts { err := o(&options) @@ -177,21 +201,21 @@ func (t *Test) Instrument(component ComponentType, agentName string, opts ...Ins expectedBinaryName := fmt.Sprintf("gitpod-integration-test-%s-agent", agentName) agentLoc, _ := exec.LookPath(expectedBinaryName) if agentLoc == "" { - agentLoc, err = t.buildAgent(agentName) + agentLoc, err = it.buildAgent(agentName) if err != nil { return nil, err } defer os.Remove(agentLoc) - t.t.Log("agent compiled at", agentLoc) + it.t.Log("agent compiled at", agentLoc) } - podName, containerName, err := t.selectPod(component, options.SPO) + podName, containerName, err := it.selectPod(component, options.SPO) if err != nil { return nil, err } tgtFN := filepath.Base(agentLoc) - err = t.uploadAgent(agentLoc, tgtFN, podName, containerName) + err = it.uploadAgent(agentLoc, tgtFN, podName, containerName) if err != nil { return nil, err } @@ -204,7 +228,7 @@ func (t *Test) Instrument(component ComponentType, agentName string, opts ...Ins execErrs := make(chan error, 1) go func() { defer close(execErrs) - execErr := t.executeAgent(filepath.Join("/tmp", tgtFN), podName, containerName, localAgentPort) + execErr := it.executeAgent(filepath.Join("/tmp", tgtFN), podName, containerName, localAgentPort) if err != nil { execErrs <- execErr } @@ -221,7 +245,7 @@ func (t *Test) Instrument(component ComponentType, agentName string, opts ...Ins ctx, cancel := context.WithCancel(context.Background()) defer func() { if err == nil { - t.closer = append(t.closer, func() error { + it.closer = append(it.closer, func() error { cancel() return nil }) @@ -229,7 +253,7 @@ func (t *Test) Instrument(component ComponentType, agentName string, opts ...Ins cancel() } }() - fwdReady, fwdErr := forwardPort(ctx, t.restConfig, t.namespace, podName, strconv.Itoa(localAgentPort)) + fwdReady, fwdErr := forwardPort(ctx, it.restConfig, it.namespace, podName, strconv.Itoa(localAgentPort)) select { case <-fwdReady: case err = <-execErrs: @@ -256,7 +280,7 @@ func (t *Test) Instrument(component ComponentType, agentName string, opts ...Ins return nil, err } - t.closer = append(t.closer, func() error { + it.closer = append(it.closer, func() error { err := res.Call(MethodTestAgentShutdown, new(TestAgentShutdownRequest), new(TestAgentShutdownResponse)) if err != nil && strings.Contains(err.Error(), "connection is shut down") { return nil @@ -341,12 +365,12 @@ func forwardPort(ctx context.Context, config *rest.Config, namespace, pod, port return } -func (t *Test) executeAgent(tgtPath string, pod, container string, port int) (err error) { - restClient := t.clientset.CoreV1().RESTClient() +func (it *Test) executeAgent(tgtPath string, pod, container string, port int) (err error) { + restClient := it.clientset.CoreV1().RESTClient() req := restClient.Post(). Resource("pods"). Name(pod). - Namespace(t.namespace). + Namespace(it.namespace). SubResource("exec"). Param("container", container) req.VersionedParams(&corev1.PodExecOptions{ @@ -358,7 +382,7 @@ func (t *Test) executeAgent(tgtPath string, pod, container string, port int) (er TTY: true, }, scheme.ParameterCodec) - exec, err := remotecommand.NewSPDYExecutor(t.restConfig, "POST", req.URL()) + exec, err := remotecommand.NewSPDYExecutor(it.restConfig, "POST", req.URL()) if err != nil { return err } @@ -370,7 +394,7 @@ func (t *Test) executeAgent(tgtPath string, pod, container string, port int) (er }) } -func (t *Test) uploadAgent(srcFN, tgtFN string, pod, container string) (err error) { +func (it *Test) uploadAgent(srcFN, tgtFN string, pod, container string) (err error) { stat, err := os.Stat(srcFN) if err != nil { return xerrors.Errorf("cannot upload agent: %w", err) @@ -383,11 +407,11 @@ func (t *Test) uploadAgent(srcFN, tgtFN string, pod, container string) (err erro tarIn, tarOut := io.Pipe() - restClient := t.clientset.CoreV1().RESTClient() + restClient := it.clientset.CoreV1().RESTClient() req := restClient.Post(). Resource("pods"). Name(pod). - Namespace(t.namespace). + Namespace(it.namespace). SubResource("exec"). Param("container", container) req.VersionedParams(&corev1.PodExecOptions{ @@ -399,7 +423,7 @@ func (t *Test) uploadAgent(srcFN, tgtFN string, pod, container string) (err erro TTY: false, }, scheme.ParameterCodec) - exec, err := remotecommand.NewSPDYExecutor(t.restConfig, "POST", req.URL()) + exec, err := remotecommand.NewSPDYExecutor(it.restConfig, "POST", req.URL()) if err != nil { return xerrors.Errorf("cannot upload agent: %w", err) } diff --git a/test/pkg/integration/workspace.go b/test/pkg/integration/workspace.go index 3b8ead51b7dadb..5a7b0764dee766 100644 --- a/test/pkg/integration/workspace.go +++ b/test/pkg/integration/workspace.go @@ -23,6 +23,7 @@ import ( const ( gitpodBuiltinUserID = "builtin-user-workspace-probe-0000000" + perCallTimeout = 20 * time.Second ) type launchWorkspaceDirectlyOptions struct { @@ -106,9 +107,9 @@ func LaunchWorkspaceDirectly(it *Test, opts ...LaunchWorkspaceDirectlyOpt) (res var workspaceImage string if options.BaseImage != "" { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + rctx, rcancel := context.WithTimeout(it.ctx, perCallTimeout) cl := it.API().ImageBuilder() - reslv, err := cl.ResolveWorkspaceImage(ctx, &imgbldr.ResolveWorkspaceImageRequest{ + reslv, err := cl.ResolveWorkspaceImage(rctx, &imgbldr.ResolveWorkspaceImageRequest{ Source: &imgbldr.BuildSource{ From: &imgbldr.BuildSource_Ref{ Ref: &imgbldr.BuildSourceReference{ @@ -124,7 +125,7 @@ func LaunchWorkspaceDirectly(it *Test, opts ...LaunchWorkspaceDirectlyOpt) (res }, }, }) - cancel() + rcancel() if err != nil { it.t.Fatal(err) return @@ -185,18 +186,17 @@ func LaunchWorkspaceDirectly(it *Test, opts ...LaunchWorkspaceDirectlyOpt) (res } } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - sresp, err := it.API().WorkspaceManager().StartWorkspace(ctx, req) - cancel() + sctx, scancel := context.WithTimeout(it.ctx, perCallTimeout) + sresp, err := it.API().WorkspaceManager().StartWorkspace(sctx, req) + scancel() if err != nil { it.t.Fatalf("cannot start workspace: %q", err) } + // TODO(geropl) It seems we're sometimes loosing events here. time.Sleep(2 * time.Second) - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - it.WaitForWorkspace(ctx, instanceID.String()) + it.WaitForWorkspace(it.ctx, instanceID.String()) it.t.Logf("workspace is running: instanceID=%s", instanceID.String()) @@ -211,43 +211,55 @@ func LaunchWorkspaceDirectly(it *Test, opts ...LaunchWorkspaceDirectlyOpt) (res // fail the test. // // When possible, prefer the less complex LaunchWorkspaceDirectly. -func LaunchWorkspaceFromContextURL(it *Test, contextURL string) *protocol.WorkspaceInfo { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() +func LaunchWorkspaceFromContextURL(it *Test, contextURL string, serverOpts ...GitpodServerOpt) (nfo *protocol.WorkspaceInfo, stopWs func(waitForStop bool)) { + var defaultServerOpts []GitpodServerOpt + if it.username != "" { + defaultServerOpts = []GitpodServerOpt{WithGitpodUser(it.username)} + } + server := it.API().GitpodServer(append(defaultServerOpts, serverOpts...)...) - server := it.API().GitpodServer() - resp, err := server.CreateWorkspace(ctx, &protocol.CreateWorkspaceOptions{ - ContextURL: "github.com/gitpod-io/gitpod", + cctx, ccancel := context.WithTimeout(it.ctx, perCallTimeout) + defer ccancel() + resp, err := server.CreateWorkspace(cctx, &protocol.CreateWorkspaceOptions{ + ContextURL: contextURL, Mode: "force-new", }) if err != nil { it.t.Fatalf("cannot start workspace: %q", err) } - defer func() { - cctx, ccancel := context.WithTimeout(context.Background(), 10*time.Second) - err := server.StopWorkspace(cctx, resp.CreatedWorkspaceID) - ccancel() + stopWs = func(waitForStop bool) { + sctx, scancel := context.WithTimeout(it.ctx, perCallTimeout) + err := server.StopWorkspace(sctx, resp.CreatedWorkspaceID) + scancel() if err != nil { it.t.Errorf("cannot stop workspace: %q", err) } + + if waitForStop { + it.WaitForWorkspaceStop(nfo.LatestInstance.ID) + } + } + defer func() { + if err != nil { + stopWs(false) + } }() it.t.Logf("created workspace: workspaceID=%s url=%s", resp.CreatedWorkspaceID, resp.WorkspaceURL) - nfo, err := server.GetWorkspace(ctx, resp.CreatedWorkspaceID) + nfo, err = server.GetWorkspace(it.ctx, resp.CreatedWorkspaceID) if err != nil { it.t.Fatalf("cannot get workspace: %q", err) } if nfo.LatestInstance == nil { - it.t.Fatal("CreateWorkspace did not start the workspace") + err = fmt.Errorf("CreateWorkspace did not start the workspace") + it.t.Fatal(err) } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - it.WaitForWorkspace(ctx, nfo.LatestInstance.ID) + it.WaitForWorkspace(it.ctx, nfo.LatestInstance.ID) it.t.Logf("workspace is running: instanceID=%s", nfo.LatestInstance.ID) - return nfo + return nfo, stopWs } // WaitForWorkspace waits until a workspace is running. Fails the test if the workspace @@ -324,11 +336,11 @@ func (t *Test) WaitForWorkspace(ctx context.Context, instanceID string) { // WaitForWorkspaceStop waits until a workspace is stopped. Fails the test if the workspace // fails or does not stop before the context is canceled. -func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (lastStatus *wsmanapi.WorkspaceStatus) { - wsman := t.API().WorkspaceManager() - sub, err := wsman.Subscribe(ctx, &wsmanapi.SubscribeRequest{}) +func (it *Test) WaitForWorkspaceStop(instanceID string) (lastStatus *wsmanapi.WorkspaceStatus) { + wsman := it.API().WorkspaceManager() + sub, err := wsman.Subscribe(it.ctx, &wsmanapi.SubscribeRequest{}) if err != nil { - t.t.Fatalf("cannot listen for workspace updates: %q", err) + it.t.Fatalf("cannot listen for workspace updates: %q", err) return } defer sub.CloseSend() @@ -339,7 +351,7 @@ func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (las for { resp, err := sub.Recv() if err != nil { - t.t.Fatalf("workspace update error: %q", err) + it.t.Fatalf("workspace update error: %q", err) return } status := resp.GetStatus() @@ -351,7 +363,7 @@ func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (las } if status.Conditions.Failed != "" { - t.t.Fatalf("workspace instance %s failed: %s", instanceID, status.Conditions.Failed) + it.t.Fatalf("workspace instance %s failed: %s", instanceID, status.Conditions.Failed) return } if status.Phase == wsmanapi.WorkspacePhase_STOPPED { @@ -362,7 +374,7 @@ func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (las }() // maybe the workspace has started in the meantime and we've missed the update - desc, _ := wsman.DescribeWorkspace(ctx, &wsmanapi.DescribeWorkspaceRequest{Id: instanceID}) + desc, _ := wsman.DescribeWorkspace(it.ctx, &wsmanapi.DescribeWorkspaceRequest{Id: instanceID}) if desc != nil { switch desc.Status.Phase { case wsmanapi.WorkspacePhase_STOPPED: @@ -371,8 +383,8 @@ func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (las } select { - case <-ctx.Done(): - t.t.Fatalf("cannot wait for workspace: %q", ctx.Err()) + case <-it.ctx.Done(): + it.t.Fatalf("cannot wait for workspace: %q", it.ctx.Err()) case <-done: } return @@ -381,11 +393,11 @@ func (t *Test) WaitForWorkspaceStop(ctx context.Context, instanceID string) (las // DeleteWorkspace cleans up a workspace started during an integration test func DeleteWorkspace(it *Test, instanceID string) { err := func() error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(it.ctx, perCallTimeout) + defer cancel() _, err := it.API().WorkspaceManager().StopWorkspace(ctx, &wsmanapi.StopWorkspaceRequest{ Id: instanceID, }) - cancel() if err == nil { return nil diff --git a/test/tests/examples/db_test.go b/test/tests/examples/db_test.go index 350c86501040d1..0d95cc0085ccda 100644 --- a/test/tests/examples/db_test.go +++ b/test/tests/examples/db_test.go @@ -6,12 +6,13 @@ package examples import ( "testing" + "time" "github.com/gitpod-io/gitpod/test/pkg/integration" ) func TestBuiltinUserExists(t *testing.T) { - it := integration.NewTest(t) + it, _ := integration.NewTest(t, 30*time.Second) defer it.Done() db := it.API().DB() diff --git a/test/tests/examples/server_test.go b/test/tests/examples/server_test.go index 416a6301916ad3..f7e6bbc788db23 100644 --- a/test/tests/examples/server_test.go +++ b/test/tests/examples/server_test.go @@ -14,12 +14,9 @@ import ( ) func TestServerAccess(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Second) defer it.Done() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - server := it.API().GitpodServer() res, err := server.GetLoggedInUser(ctx) if err != nil { @@ -29,12 +26,9 @@ func TestServerAccess(t *testing.T) { } func TestStartWorkspace(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Minute) defer it.Done() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - server := it.API().GitpodServer() resp, err := server.CreateWorkspace(ctx, &protocol.CreateWorkspaceOptions{ ContextURL: "github.com/gitpod-io/gitpod", @@ -61,8 +55,6 @@ func TestStartWorkspace(t *testing.T) { t.Fatal("CreateWorkspace did not start the workspace") } - ctx, cancel = context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() it.WaitForWorkspace(ctx, nfo.LatestInstance.ID) t.Logf("workspace is running: instanceID=%s", nfo.LatestInstance.ID) diff --git a/test/tests/examples/wsmanager_test.go b/test/tests/examples/wsmanager_test.go index 9b3f69e1a4d6cd..873f55f1258fc7 100644 --- a/test/tests/examples/wsmanager_test.go +++ b/test/tests/examples/wsmanager_test.go @@ -5,7 +5,6 @@ package examples import ( - "context" "testing" "time" @@ -14,12 +13,10 @@ import ( ) func TestGetWorkspaces(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Second) defer it.Done() wsman := it.API().WorkspaceManager() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() _, err := wsman.GetWorkspaces(ctx, &api.GetWorkspacesRequest{}) if err != nil { t.Fatal(err) diff --git a/test/tests/storage/content-service_test.go b/test/tests/storage/content-service_test.go index 199043f3200c07..8f02eb9fd9d151 100644 --- a/test/tests/storage/content-service_test.go +++ b/test/tests/storage/content-service_test.go @@ -5,7 +5,6 @@ package storage import ( - "context" "fmt" "io" "net/http" @@ -61,12 +60,10 @@ func TestUploadUrl(t *testing.T) { } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 15*time.Second) defer it.Done() bs := it.API().BlobService() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() resp, err := bs.UploadUrl(ctx, &api.UploadUrlRequest{OwnerId: test.InputOwnerID, Name: test.InputName}) if err != nil && test.ExpectedErrorCode == codes.OK { t.Fatal(err) @@ -104,12 +101,10 @@ func TestDownloadUrl(t *testing.T) { } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Second) defer it.Done() bs := it.API().BlobService() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() resp, err := bs.DownloadUrl(ctx, &api.DownloadUrlRequest{OwnerId: test.InputOwnerID, Name: test.InputName}) if err != nil && test.ExpectedErrorCode == codes.OK { t.Fatal(err) @@ -132,14 +127,12 @@ func TestDownloadUrl(t *testing.T) { } func TestUploadDownloadBlob(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Second) defer it.Done() blobContent := fmt.Sprintf("Hello Blobs! It's %s!", time.Now()) bs := it.API().BlobService() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() resp, err := bs.UploadUrl(ctx, &api.UploadUrlRequest{OwnerId: gitpodBuiltinUserID, Name: "test-blob"}) if err != nil { t.Fatal(err) @@ -164,12 +157,9 @@ func TestUploadDownloadBlob(t *testing.T) { // TestUploadDownloadBlobViaServer uploads a blob via server → content-server and downloads it afterwards func TestUploadDownloadBlobViaServer(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 10*time.Second) defer it.Done() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - blobContent := fmt.Sprintf("Hello Blobs! It's %s!", time.Now()) server := it.API().GitpodServer() diff --git a/test/tests/storage/storage_test.go b/test/tests/storage/storage_test.go index 76737d1e328c76..e7e86623908c2d 100644 --- a/test/tests/storage/storage_test.go +++ b/test/tests/storage/storage_test.go @@ -14,7 +14,7 @@ import ( ) func TestCreateBucket(t *testing.T) { - it := integration.NewTest(t) + it, _ := integration.NewTest(t, 30*time.Second) defer it.Done() rsa, err := it.Instrument(integration.ComponentWorkspaceDaemon, "daemon") diff --git a/test/tests/workspace/common/git-client.go b/test/tests/workspace/common/git-client.go new file mode 100644 index 00000000000000..9322b7af4c781f --- /dev/null +++ b/test/tests/workspace/common/git-client.go @@ -0,0 +1,102 @@ +// 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 common + +import ( + "fmt" + "net/rpc" + "strings" + + agent "github.com/gitpod-io/gitpod/test/tests/workspace/workspace_agent/api" +) + +type GitClient struct { + *rpc.Client +} + +func Git(rsa *rpc.Client) GitClient { + return GitClient{rsa} +} + +func (g GitClient) GetBranch(workspaceRoot string) (string, error) { + var resp agent.ExecResponse + err := g.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: workspaceRoot, + Command: "git", + Args: []string{"rev-parse", "--abbrev-ref", "HEAD"}, + }, &resp) + if err != nil { + return "", fmt.Errorf("getBranch error: %w", err) + } + if resp.ExitCode != 0 { + return "", fmt.Errorf("getBranch rc!=0: %d", resp.ExitCode) + } + return strings.Trim(resp.Stdout, " \t\n"), nil +} + +func (g GitClient) Add(dir string, files ...string) error { + args := []string{"add"} + if files == nil { + args = append(args, ".") + } else { + args = append(args, files...) + } + var resp agent.ExecResponse + err := g.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: dir, + Command: "git", + Args: args, + }, &resp) + if err != nil { + return err + } + if resp.ExitCode != 0 { + return fmt.Errorf("commit returned rc: %d", resp.ExitCode) + } + return nil +} + +func (g GitClient) Commit(dir string, message string, all bool) error { + args := []string{"commit", "-m", message} + if all { + args = append(args, "--all") + } + var resp agent.ExecResponse + err := g.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: dir, + Command: "git", + Args: args, + }, &resp) + if err != nil { + return err + } + if resp.ExitCode != 0 { + return fmt.Errorf("commit returned rc: %d", resp.ExitCode) + } + return nil +} + +func (g GitClient) Push(dir string, force bool, moreArgs ...string) error { + args := []string{"push"} + if moreArgs != nil { + args = append(args, moreArgs...) + } + if force { + args = append(args, "--force") + } + var resp agent.ExecResponse + err := g.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: dir, + Command: "git", + Args: args, + }, &resp) + if err != nil { + return err + } + if resp.ExitCode != 0 { + return fmt.Errorf("commit returned rc: %d", resp.ExitCode) + } + return nil +} diff --git a/test/tests/workspace/content_test.go b/test/tests/workspace/content_test.go index a0d40eb89a8abe..b535b0c127bfb8 100644 --- a/test/tests/workspace/content_test.go +++ b/test/tests/workspace/content_test.go @@ -17,7 +17,7 @@ import ( // TestBackup tests a basic start/modify/restart cycle func TestBackup(t *testing.T) { - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Minute) defer it.Done() ws := integration.LaunchWorkspaceDirectly(it) @@ -39,9 +39,9 @@ func TestBackup(t *testing.T) { } rsa.Close() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, err = it.API().WorkspaceManager().StopWorkspace(ctx, &wsapi.StopWorkspaceRequest{ + sctx, scancel := context.WithTimeout(ctx, 5*time.Second) + defer scancel() + _, err = it.API().WorkspaceManager().StopWorkspace(sctx, &wsapi.StopWorkspaceRequest{ Id: ws.Req.Id, }) if err != nil { @@ -49,9 +49,7 @@ func TestBackup(t *testing.T) { return } - ctx, cancel = context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - it.WaitForWorkspaceStop(ctx, ws.Req.Id) + it.WaitForWorkspaceStop(ws.Req.Id) ws = integration.LaunchWorkspaceDirectly(it, integration.WithRequestModifier(func(w *wsapi.StartWorkspaceRequest) error { w.ServicePrefix = ws.Req.ServicePrefix diff --git a/test/tests/workspace/contexts_test.go b/test/tests/workspace/contexts_test.go new file mode 100644 index 00000000000000..a10bc73e31448d --- /dev/null +++ b/test/tests/workspace/contexts_test.go @@ -0,0 +1,137 @@ +// 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 workspace_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gitpod-io/gitpod/test/pkg/integration" + "github.com/gitpod-io/gitpod/test/tests/workspace/common" +) + +type ContextTest struct { + Skip bool + Name string + ContextURL string + WorkspaceRoot string + ExpectedBranch string + ExpectedBranchFunc func(username string) string +} + +func TestGitHubContexts(t *testing.T) { + tests := []ContextTest{ + { + Name: "open repository", + ContextURL: "github.com/gitpod-io/gitpod", + WorkspaceRoot: "/workspace/gitpod", + ExpectedBranch: "main", + }, + { + Name: "open branch", + ContextURL: "github.com/gitpod-io/gitpod-test-repo/tree/integration-test-1", + WorkspaceRoot: "/workspace/gitpod-test-repo", + ExpectedBranch: "integration-test-1", + }, + { + Name: "open issue", + ContextURL: "github.com/gitpod-io/gitpod-test-repo/issues/88", + WorkspaceRoot: "/workspace/gitpod-test-repo", + ExpectedBranchFunc: func(username string) string { + return fmt.Sprintf("%s/integration-tests-test-context-88", username) + }, + }, + { + Name: "open tag", + ContextURL: "github.com/gitpod-io/gitpod-test-repo/tree/integration-test-context-tag", + WorkspaceRoot: "/workspace/gitpod-test-repo", + ExpectedBranch: "HEAD", + }, + } + runContextTests(t, tests) +} + +func TestGitLabContexts(t *testing.T) { + tests := []ContextTest{ + { + Name: "open repository", + ContextURL: "gitlab.com/AlexTugarev/gp-test", + WorkspaceRoot: "/workspace/gp-test", + ExpectedBranch: "master", + }, + { + Name: "open branch", + ContextURL: "gitlab.com/AlexTugarev/gp-test/tree/wip", + WorkspaceRoot: "/workspace/gp-test", + ExpectedBranch: "wip", + }, + { + Name: "open issue", + ContextURL: "gitlab.com/AlexTugarev/gp-test/issues/1", + WorkspaceRoot: "/workspace/gp-test", + ExpectedBranchFunc: func(username string) string { + return fmt.Sprintf("%s/write-a-readme-1", username) + }, + }, + { + Name: "open tag", + ContextURL: "gitlab.com/AlexTugarev/gp-test/merge_requests/2", + WorkspaceRoot: "/workspace/gp-test", + ExpectedBranch: "wip2", + }, + } + runContextTests(t, tests) +} + +func runContextTests(t *testing.T, tests []ContextTest) { + for _, test := range tests { + t.Run(test.ContextURL, func(t *testing.T) { + if test.Skip { + t.SkipNow() + } + // TODO(geropl) Why does this not work? Logs hint to a race around bucket creation...? + // t.Parallel() + + it, ctx := integration.NewTest(t, 5*time.Minute) + defer it.Done() + + if it.Username() == "" && test.ExpectedBranchFunc != nil { + t.Logf("skipping '%s' because there is not username configured", test.Name) + t.SkipNow() + } + + nfo, stopWS := integration.LaunchWorkspaceFromContextURL(it, test.ContextURL) + defer stopWS(false) // we do not wait for stopped here as it does not matter for this test case and speeds things up + + wctx, wcancel := context.WithTimeout(ctx, 1*time.Minute) + defer wcancel() + it.WaitForWorkspace(wctx, nfo.LatestInstance.ID) + + rsa, err := it.Instrument(integration.ComponentWorkspace, "workspace", integration.WithInstanceID(nfo.LatestInstance.ID)) + if err != nil { + t.Fatal(err) + } + defer rsa.Close() + + // get actual from workspace + git := common.Git(rsa) + actBranch, err := git.GetBranch(test.WorkspaceRoot) + if err != nil { + t.Fatal(err) + } + rsa.Close() + + expectedBranch := test.ExpectedBranch + if test.ExpectedBranchFunc != nil { + expectedBranch = test.ExpectedBranchFunc(it.Username()) + } + if actBranch != expectedBranch { + t.Fatalf("expected branch '%s', got '%s'!", test.ExpectedBranch, actBranch) + } + }) + } +} diff --git a/test/tests/workspace/example_test.go b/test/tests/workspace/example_test.go index 98e27bf1beee7c..700f8acc481d75 100644 --- a/test/tests/workspace/example_test.go +++ b/test/tests/workspace/example_test.go @@ -6,17 +6,18 @@ package workspace_test import ( "testing" + "time" "github.com/gitpod-io/gitpod/test/pkg/integration" agent "github.com/gitpod-io/gitpod/test/tests/workspace/workspace_agent/api" ) func TestWorkspaceInstrumentation(t *testing.T) { - it := integration.NewTest(t) + it, _ := integration.NewTest(t, 5*time.Minute) defer it.Done() - nfo := integration.LaunchWorkspaceFromContextURL(it, "github.com/gitpod-io/gitpod") - defer integration.DeleteWorkspace(it, nfo.LatestInstance.ID) + nfo, stopWs := integration.LaunchWorkspaceFromContextURL(it, "github.com/gitpod-io/gitpod") + defer stopWs(true) rsa, err := it.Instrument(integration.ComponentWorkspace, "workspace", integration.WithInstanceID(nfo.LatestInstance.ID)) if err != nil { @@ -37,7 +38,7 @@ func TestWorkspaceInstrumentation(t *testing.T) { } func TestLaunchWorkspaceDirectly(t *testing.T) { - it := integration.NewTest(t) + it, _ := integration.NewTest(t, 5*time.Minute) defer it.Done() nfo := integration.LaunchWorkspaceDirectly(it) diff --git a/test/tests/workspace/git_test.go b/test/tests/workspace/git_test.go new file mode 100644 index 00000000000000..39d9b5d2201360 --- /dev/null +++ b/test/tests/workspace/git_test.go @@ -0,0 +1,123 @@ +// 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 workspace_test + +import ( + "net/rpc" + "testing" + "time" + + "github.com/gitpod-io/gitpod/test/pkg/integration" + "github.com/gitpod-io/gitpod/test/tests/workspace/common" + agent "github.com/gitpod-io/gitpod/test/tests/workspace/workspace_agent/api" +) + +type GitTest struct { + Skip bool + Name string + ContextURL string + WorkspaceRoot string + Action GitFunc +} + +type GitFunc func(rsa *rpc.Client, git common.GitClient, workspaceRoot string) error + +func TestGitActions(t *testing.T) { + tests := []GitTest{ + { + Name: "create, add and commit", + ContextURL: "github.com/gitpod-io/gitpod-test-repo/tree/integration-test/commit-and-push", + WorkspaceRoot: "/workspace/gitpod-test-repo", + Action: func(rsa *rpc.Client, git common.GitClient, workspaceRoot string) (err error) { + + var resp agent.ExecResponse + err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: workspaceRoot, + Command: "bash", + Args: []string{ + "-c", + "echo \"another test run...\" >> file_to_commit.txt", + }, + }, &resp) + if err != nil { + return err + } + err = git.Add(workspaceRoot) + if err != nil { + return err + } + err = git.Commit(workspaceRoot, "automatic test commit", false) + if err != nil { + return err + } + return nil + }, + }, + { + Skip: true, + Name: "create, add and commit and PUSH", + ContextURL: "github.com/gitpod-io/gitpod-test-repo/tree/integration-test/commit-and-push", + WorkspaceRoot: "/workspace/gitpod-test-repo", + Action: func(rsa *rpc.Client, git common.GitClient, workspaceRoot string) (err error) { + + var resp agent.ExecResponse + err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{ + Dir: workspaceRoot, + Command: "bash", + Args: []string{ + "-c", + "echo \"another test run...\" >> file_to_commit.txt", + }, + }, &resp) + if err != nil { + return err + } + err = git.Add(workspaceRoot) + if err != nil { + return err + } + err = git.Commit(workspaceRoot, "automatic test commit", false) + if err != nil { + return err + } + err = git.Push(workspaceRoot, false) + if err != nil { + return err + } + return nil + }, + }, + } + runGitTests(t, tests) +} + +func runGitTests(t *testing.T, tests []GitTest) { + for _, test := range tests { + t.Run(test.ContextURL, func(t *testing.T) { + if test.Skip { + t.SkipNow() + } + + it, ctx := integration.NewTest(t, 5*time.Minute) + defer it.Done() + + nfo, stopWS := integration.LaunchWorkspaceFromContextURL(it, test.ContextURL) + defer stopWS(false) + + it.WaitForWorkspace(ctx, nfo.LatestInstance.ID) + + rsa, err := it.Instrument(integration.ComponentWorkspace, "workspace", integration.WithInstanceID(nfo.LatestInstance.ID)) + if err != nil { + t.Fatal(err) + } + defer rsa.Close() + + git := common.Git(rsa) + test.Action(rsa, git, test.WorkspaceRoot) + + rsa.Close() + }) + } +} diff --git a/test/tests/workspace/tasks_test.go b/test/tests/workspace/tasks_test.go index c29713539c3340..a82a57fcb8bfb0 100644 --- a/test/tests/workspace/tasks_test.go +++ b/test/tests/workspace/tasks_test.go @@ -44,7 +44,7 @@ func TestRegularWorkspaceTasks(t *testing.T) { t.Run(test.Name, func(t *testing.T) { t.Parallel() - it := integration.NewTest(t) + it, ctx := integration.NewTest(t, 5*time.Minute) defer it.Done() addInitTask := func(swr *wsmanapi.StartWorkspaceRequest) error { @@ -67,9 +67,9 @@ func TestRegularWorkspaceTasks(t *testing.T) { conn := it.API().Supervisor(nfo.Req.Id) statusService := supervisor.NewStatusServiceClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - resp, err := statusService.TasksStatus(ctx, &supervisor.TasksStatusRequest{Observe: false}) + tsctx, tscancel := context.WithTimeout(ctx, 60*time.Second) + defer tscancel() + resp, err := statusService.TasksStatus(tsctx, &supervisor.TasksStatusRequest{Observe: false}) if err != nil { t.Fatal(err) } diff --git a/test/tests/workspace/workspace_agent/api/api.go b/test/tests/workspace/workspace_agent/api/api.go index 116ccaa0e71c45..6e18009f2a40d5 100644 --- a/test/tests/workspace/workspace_agent/api/api.go +++ b/test/tests/workspace/workspace_agent/api/api.go @@ -26,3 +26,15 @@ type WriteFileRequest struct { // WriteFileResponse is the response for WriteFile type WriteFileResponse struct { } + +type ExecRequest struct { + Dir string + Command string + Args []string +} + +type ExecResponse struct { + ExitCode int + Stdout string + Stderr string +} diff --git a/test/tests/workspace/workspace_agent/main.go b/test/tests/workspace/workspace_agent/main.go index 1f858d0d7f890a..9d30361aa65de7 100644 --- a/test/tests/workspace/workspace_agent/main.go +++ b/test/tests/workspace/workspace_agent/main.go @@ -5,7 +5,11 @@ package main import ( + "bytes" + "fmt" "os" + "os/exec" + "strings" "github.com/gitpod-io/gitpod/test/pkg/integration" "github.com/gitpod-io/gitpod/test/tests/workspace/workspace_agent/api" @@ -43,3 +47,35 @@ func (*WorkspaceAgent) WriteFile(req *api.WriteFileRequest, resp *api.WriteFileR *resp = api.WriteFileResponse{} return } + +// Exec executes a command in the workspace +func (*WorkspaceAgent) Exec(req *api.ExecRequest, resp *api.ExecResponse) (err error) { + cmd := exec.Command(req.Command, req.Args...) + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + if req.Dir != "" { + cmd.Dir = req.Dir + } + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + + var rc int + if err != nil { + exitError, ok := err.(*exec.ExitError) + if !ok { + fullCommand := strings.Join(append([]string{req.Command}, req.Args...), " ") + return fmt.Errorf("%s: %w", fullCommand, err) + } + rc = exitError.ExitCode() + } + + *resp = api.ExecResponse{ + ExitCode: rc, + Stdout: stdout.String(), + Stderr: stderr.String(), + } + return +}