JenTing Hsiao f96cbe6087 test: check the /workspace UID:GID is gitpod:gitpod
Signed-off-by: JenTing Hsiao <hsiaoairplane@gmail.com>
2022-11-06 18:59:37 +01:00

1042 lines
38 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 wsmanager
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/rpc"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/e2e-framework/klient"
"sigs.k8s.io/e2e-framework/pkg/envconf"
"sigs.k8s.io/e2e-framework/pkg/features"
csapi "github.com/gitpod-io/gitpod/content-service/api"
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
agent "github.com/gitpod-io/gitpod/test/pkg/agent/workspace/api"
"github.com/gitpod-io/gitpod/test/pkg/integration"
wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
volumesnapshotclientv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned"
)
func TestPrebuildWorkspaceTaskSuccess(t *testing.T) {
f := features.New("prebuild").
WithLabel("component", "ws-manager").
Assess("it should create a prebuild and succeed the defined tasks", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
tests := []struct {
Name string
ContextURL string
WorkspaceRoot string
CheckoutLocation string
Task []gitpod.TasksItems
FF []wsmanapi.WorkspaceFeatureFlag
}{
{
Name: "classic",
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
Task: []gitpod.TasksItems{
{Init: "echo \"some output\" > someFile; sleep 10; exit 0;"},
},
},
{
Name: "pvc",
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
Task: []gitpod.TasksItems{
{Init: "echo \"some output\" > someFile; sleep 10; exit 0;"},
},
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(5*len(tests))*time.Minute)
defer cancel()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
// TODO: change to use server API to launch the workspace, so we could run the integration test as the user code flow
// which is client -> server -> ws-manager rather than client -> ws-manager directly
ws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Type = wsmanapi.WorkspaceType_PREBUILD
tasks, err := json.Marshal(test.Task)
if err != nil {
return err
}
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: string(tasks),
})
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
_, err = integration.WaitForWorkspace(ctx, api, ws.Req.Id, func(status *wsmanapi.WorkspaceStatus) bool {
if status.Phase != wsmanapi.WorkspacePhase_STOPPED {
return false
}
if status.Conditions.HeadlessTaskFailed != "" {
t.Logf("Conditions: %v", status.Conditions)
t.Fatal("unexpected HeadlessTaskFailed condition")
}
return true
})
if err != nil {
t.Fatalf("failed for wait workspace stop: %q", err)
}
})
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}
func TestPrebuildWorkspaceTaskFail(t *testing.T) {
f := features.New("prebuild").
WithLabel("component", "server").
Assess("it should create a prebuild and fail after running the defined tasks", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
ws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Type = wsmanapi.WorkspaceType_PREBUILD
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: `[{ "init": "echo \"some output\" > someFile; sleep 10; exit 1;" }]`,
})
return nil
}))
if err != nil {
t.Fatalf("cannot start workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
_, err = integration.WaitForWorkspace(ctx, api, ws.Req.Id, func(status *wsmanapi.WorkspaceStatus) bool {
if status.Phase != wsmanapi.WorkspacePhase_STOPPED {
return false
}
if status.Conditions.HeadlessTaskFailed == "" {
t.Logf("Conditions: %v", status.Conditions)
t.Fatal("expected HeadlessTaskFailed condition")
}
return true
})
if err != nil {
t.Fatalf("failed for wait workspace stop: %q", err)
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}
const (
prebuildLogPath string = "/workspace/.gitpod"
prebuildLog string = "'🤙 This task ran as a workspace prebuild'"
initTask string = "echo \"some output\" > someFile; sleep 10;"
regularPrefix string = "ws-"
)
// TestOpenWorkspaceFromPrebuild
// - create a prebuild
// - stop the prebuild workspace
// - if the PVC feature flag enables, restart the control plane components (ws-manager and snapshot-controller) after the volume snapshot is finished
// - open the regular workspace from prebuild
// - make sure the regular workspace PVC object should exist or not
// - make sure either one of the condition mets
// - the prebuild log message exists
// - the init task message exists
// - the init task generated file exists
//
// - make sure the .git/ folder with correct permission
// - write a new file foobar.txt
// - stop the regular workspace
// - if the PVC feature flag enables, restart the control plane components (ws-manager and snapshot-controller) during the volume snapshot is in progress
// - relaunch the regular workspace
// - make sure the regular workspace PVC object should exist or not
// - make sure the file foobar.txt exists
func TestOpenWorkspaceFromPrebuild(t *testing.T) {
var (
snapshotcontrollerDeployment string = "snapshot-controller"
snapshotcontrollerNamespace string = "kube-system"
wsmanagerDeployment string = "ws-manager"
wsmanagerNamespace string = "default"
)
f := features.New("prebuild").
WithLabel("component", "ws-manager").
Assess("it should open workspace from prebuild successfully", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
tests := []struct {
Name string
ContextURL string
WorkspaceRoot string
CheckoutLocation string
FF []wsmanapi.WorkspaceFeatureFlag
}{
{
Name: "classic",
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
},
{
Name: "pvc",
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10*len(tests))*time.Minute)
defer cancel()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
isPVCEnable := reflect.DeepEqual(test.FF, []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM})
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
// create a prebuild and stop workspace
// TODO: change to use server API to launch the workspace, so we could run the integration test as the user code flow
// which is client -> server -> ws-manager rather than client -> ws-manager directly
ws, prebuildStopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Type = wsmanapi.WorkspaceType_PREBUILD
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: fmt.Sprintf(`[{ "init": %q }]`, initTask),
})
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, prebuildStopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
prebuildSnapshot, prebuildVSInfo, err := watchStopWorkspaceAndFindSnapshot(ctx, ws.Req.Id, api)
if err != nil {
t.Fatalf("stop workspace and find snapshot error: %v", err)
}
t.Logf("prebuild snapshot: %s", prebuildSnapshot)
checkSnapshot(t, prebuildVSInfo, isPVCEnable)
// restart the ws-manager and volume snapshot after the volume snapshot is finished
if isPVCEnable {
t.Logf("rollout restart deployment %s/%s after the volume snapshot is finished", wsmanagerDeployment, wsmanagerNamespace)
if err := api.RestartDeployment(wsmanagerDeployment, wsmanagerNamespace, true); err != nil {
t.Errorf("cannot restart deployment %s/%s", wsmanagerDeployment, wsmanagerNamespace)
}
t.Logf("rollout restart deployment %s/%s is finished", wsmanagerDeployment, wsmanagerNamespace)
t.Logf("rollout restart deployment %s/%s after the volume snapshot is finished", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
if err := api.RestartDeployment(snapshotcontrollerDeployment, snapshotcontrollerNamespace, true); err != nil {
t.Errorf("cannot restart deployment %s/%s", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
}
t.Logf("rollout restart deployment %s/%s is finished", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
// recreate a new API because the ws-manager restarted
api1 := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
api = api1
t.Cleanup(func() {
api1.Done(t)
})
}
// launch the workspace from prebuild
// TODO: change to use server API to launch the workspace, so we could run the integration test as the user code flow
// which is client -> server -> ws-manager rather than client -> ws-manager directly
ws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Prebuild{
Prebuild: &csapi.PrebuildInitializer{
Prebuild: &csapi.SnapshotInitializer{
Snapshot: prebuildSnapshot,
FromVolumeSnapshot: isPVCEnable,
},
Git: []*csapi.GitInitializer{
{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
},
},
}
req.Spec.VolumeSnapshot = prebuildVSInfo
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
// check the PVC object should exist or not
checkPVCObject(t, api, isPVCEnable, regularPrefix+ws.Req.Id)
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws.Req.Id),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
rsa.Close()
})
integration.DeferCloser(t, closer)
// check prebuild log message exists
checkPrebuildLogExist(t, cfg, rsa, ws, test.WorkspaceRoot)
// check the folder permission is gitpod
checkFolderPermission(t, rsa, "/workspace")
// check the files/folders permission under .git/ is gitpod
checkGitFolderPermission(t, rsa, test.WorkspaceRoot)
// write file foobar.txt and stop the workspace
var writeFileResp agent.ExecResponse
err = rsa.Call("WorkspaceAgent.WriteFile", &agent.WriteFileRequest{
Path: fmt.Sprintf("%s/foobar.txt", test.WorkspaceRoot),
Content: []byte("hello world"),
Mode: 0644,
}, &writeFileResp)
if err != nil {
t.Fatalf("cannot write file %s", fmt.Sprintf("%s/foobar.txt", test.WorkspaceRoot))
}
sctx, scancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer scancel()
sapi := integration.NewComponentAPI(sctx, cfg.Namespace(), kubeconfig, cfg.Client())
defer sapi.Done(t)
// stop workspace without wait
_, err = stopWs(false, sapi)
if err != nil {
t.Fatal(err)
}
// restart the ws-manager and volume snapshot after the volume snapshot is in progress
if isPVCEnable {
t.Logf("rollout restart deployment %s/%s after the volume snapshot is in progress", wsmanagerDeployment, wsmanagerNamespace)
if err := api.RestartDeployment(wsmanagerDeployment, wsmanagerNamespace, true); err != nil {
t.Errorf("cannot restart deployment %s/%s", wsmanagerDeployment, wsmanagerNamespace)
}
t.Logf("rollout restart deployment %s/%s is in progress", wsmanagerDeployment, wsmanagerNamespace)
t.Logf("rollout restart deployment %s/%s after the volume snapshot is in progress", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
if err := api.RestartDeployment(snapshotcontrollerDeployment, snapshotcontrollerNamespace, true); err != nil {
t.Errorf("cannot restart deployment %s/%s", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
}
t.Logf("rollout restart deployment %s/%s is in progress", snapshotcontrollerDeployment, snapshotcontrollerNamespace)
// recreate a new API because the ws-manager restarted
sapi1 := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
sapi = sapi1
t.Cleanup(func() {
sapi.Done(t)
})
}
// find the volume snapshot by volume snapshot client
wsVSInfo, err := findVolumeSnapshot(ctx, isPVCEnable, ws.Req.Id, cfg.Namespace(), cfg.Client())
if err != nil {
t.Fatal(err)
}
checkSnapshot(t, wsVSInfo, isPVCEnable)
// reopen the workspace and make sure the file foobar.txt exists
ws1, stopWs1, err := integration.LaunchWorkspaceDirectly(t, ctx, sapi, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.ServicePrefix = ws.Req.ServicePrefix
req.Metadata.MetaId = ws.Req.Metadata.MetaId
req.Metadata.Owner = ws.Req.Metadata.Owner
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Backup{
Backup: &csapi.FromBackupInitializer{
CheckoutLocation: test.CheckoutLocation,
FromVolumeSnapshot: isPVCEnable,
},
},
}
req.Spec.VolumeSnapshot = wsVSInfo
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
// check the PVC object should exist or not
checkPVCObject(t, sapi, isPVCEnable, regularPrefix+ws1.Req.Id)
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs1); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
rsa, closer, err = integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws1.Req.Id),
)
if err != nil {
t.Fatal(err)
}
integration.DeferCloser(t, closer)
var ls agent.ListDirResponse
err = rsa.Call("WorkspaceAgent.ListDir", &agent.ListDirRequest{
Dir: test.WorkspaceRoot,
}, &ls)
if err != nil {
t.Fatal(err)
}
rsa.Close()
var found bool
for _, f := range ls.Files {
if filepath.Base(f) == "foobar.txt" {
found = true
break
}
}
if !found {
t.Fatal("did not find foobar.txt from previous workspace instance")
}
})
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}
// TestOpenWorkspaceFromOutdatedPrebuild
// - create a prebuild on older commit
// - open a workspace from a later commit with a prebuild initializer
// - make sure the workspace's init task ran (the init task will create a file `incremental.txt` see https://github.com/gitpod-io/test-incremental-workspace/blob/main/.gitpod.yml)
func TestOpenWorkspaceFromOutdatedPrebuild(t *testing.T) {
f := features.New("prebuild").
WithLabel("component", "ws-manager").
Assess("it should open a workspace from with an older prebuild initializer successfully and run the init task", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
tests := []struct {
Name string
RemoteUri string
CloneTargetForPrebuild string
CloneTargetForWorkspace string
WorkspaceRoot string
CheckoutLocation string
FF []wsmanapi.WorkspaceFeatureFlag
}{
{
Name: "classic",
RemoteUri: "https://github.com/gitpod-io/test-incremental-workspace",
CloneTargetForPrebuild: "prebuild",
CloneTargetForWorkspace: "main",
CheckoutLocation: "test-incremental-workspace",
WorkspaceRoot: "/workspace/test-incremental-workspace",
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10*len(tests))*time.Minute)
defer cancel()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
// create a prebuild
ws, prebuildStopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Type = wsmanapi.WorkspaceType_PREBUILD
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: `[{ "init": "./init.sh" }]`,
})
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.RemoteUri,
TargetMode: csapi.CloneTargetMode_REMOTE_BRANCH,
CloneTaget: test.CloneTargetForPrebuild,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, prebuildStopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
prebuildSnapshot, prebuildVSInfo, err := watchStopWorkspaceAndFindSnapshot(ctx, ws.Req.Id, api)
if err != nil {
t.Fatalf("stop workspace and find snapshot error: %v", err)
}
t.Logf("prebuild snapshot: %s", prebuildSnapshot)
// launch the workspace on a later commit using this prebuild
ws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: `[{ "init": "./init.sh" }]`,
})
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Prebuild{
Prebuild: &csapi.PrebuildInitializer{
Prebuild: &csapi.SnapshotInitializer{
Snapshot: prebuildSnapshot,
},
Git: []*csapi.GitInitializer{
{
RemoteUri: test.RemoteUri,
TargetMode: csapi.CloneTargetMode_REMOTE_BRANCH,
CloneTaget: test.CloneTargetForWorkspace,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
},
},
}
req.Spec.VolumeSnapshot = prebuildVSInfo
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws.Req.Id),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
rsa.Close()
})
integration.DeferCloser(t, closer)
var ls agent.ListDirResponse
err = rsa.Call("WorkspaceAgent.ListDir", &agent.ListDirRequest{
Dir: test.WorkspaceRoot,
}, &ls)
if err != nil {
t.Fatal(err)
}
rsa.Close()
var found bool
for _, f := range ls.Files {
t.Logf("file: %s", f)
if filepath.Base(f) == "incremental.txt" {
found = true
}
}
if !found {
t.Fatal("did not find incremental.txt")
}
})
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}
// TestPrebuildAndRegularWorkspaceDifferentWorkspaceClass
// - create a prebuild with small workspace class (20Gi disk)
// - create the workspace from prebulid with large workspace class (30Gi disk)
// - make sure either one of the condition mets
// - the prebuild log message exists
// - the init task message exists
// - the init task generated file exists
//
// - make sure the .git/ folder with correct permission
// - create a prebuild with large workspace class (30Gi disk) separately
// - create the workspace from prebuild with small workspace class (20Gi disk) separately
// - make sure the workspace can't start
func TestPrebuildAndRegularWorkspaceDifferentWorkspaceClass(t *testing.T) {
f := features.New("prebuild").
WithLabel("component", "ws-manager").
Assess("it should open workspace with different workspace class", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
tests := []struct {
Name string
PrebuildWorkspaceClass string
RegularWorkspaceClass string
ContextURL string
WorkspaceRoot string
CheckoutLocation string
FF []wsmanapi.WorkspaceFeatureFlag
ShouldFailToOpenRegularWorkspace bool
}{
{
Name: "prebuild-small-regular-large-workspace-class",
// TODO: do not use hard-code workspace class name to prevent if we change to different environment to run the test
PrebuildWorkspaceClass: "small",
RegularWorkspaceClass: "default",
//
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
},
{
Name: "prebuild-large-regular-small-workspace-class",
// TODO: do not use hard-code workspace class name to prevent if we change to different environment to run the test
PrebuildWorkspaceClass: "default",
RegularWorkspaceClass: "small",
//
ContextURL: "https://github.com/gitpod-io/empty",
CheckoutLocation: "empty",
WorkspaceRoot: "/workspace/empty",
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
ShouldFailToOpenRegularWorkspace: true,
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10*len(tests))*time.Minute)
defer cancel()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
isPVCEnable := reflect.DeepEqual(test.FF, []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM})
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
// create a prebuild and stop workspace
ws, prebuildStopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Type = wsmanapi.WorkspaceType_PREBUILD
req.Spec.Class = test.PrebuildWorkspaceClass
req.Spec.Envvars = append(req.Spec.Envvars, &wsmanapi.EnvironmentVariable{
Name: "GITPOD_TASKS",
Value: fmt.Sprintf(`[{ "init": %q }]`, initTask),
})
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatalf("cannot launch a workspace: %q", err)
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, prebuildStopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
prebuildSnapshot, vsInfo, err := watchStopWorkspaceAndFindSnapshot(ctx, ws.Req.Id, api)
if err != nil {
t.Fatalf("stop workspace and find snapshot error: %v", err)
}
t.Logf("prebuild snapshot: %s", prebuildSnapshot)
checkSnapshot(t, vsInfo, isPVCEnable)
// launch the workspace from prebuild
ws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(req *wsmanapi.StartWorkspaceRequest) error {
req.Spec.Class = test.RegularWorkspaceClass
req.Spec.FeatureFlags = test.FF
req.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Prebuild{
Prebuild: &csapi.PrebuildInitializer{
Prebuild: &csapi.SnapshotInitializer{
Snapshot: prebuildSnapshot,
FromVolumeSnapshot: true,
},
Git: []*csapi.GitInitializer{
{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
},
},
}
req.Spec.VolumeSnapshot = vsInfo
req.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
if test.ShouldFailToOpenRegularWorkspace {
return
}
t.Fatalf("cannot launch a workspace: %q", err)
}
if test.ShouldFailToOpenRegularWorkspace {
t.Fatal("should failed on launch a workspace")
return
}
defer func() {
// stop workspace in defer function to prevent we forget to stop the workspace
if err := stopWorkspace(t, cfg, stopWs); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws.Req.Id),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
rsa.Close()
})
integration.DeferCloser(t, closer)
// check prebuild log message exists
checkPrebuildLogExist(t, cfg, rsa, ws, test.WorkspaceRoot)
// check the folder permission is gitpod
checkFolderPermission(t, rsa, "/workspace")
// check the files/folders permission under .git/ is gitpod
checkGitFolderPermission(t, rsa, test.WorkspaceRoot)
})
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}
// checkSnapshot checks the volume snapshot information is valid or not
func checkSnapshot(t *testing.T, vsInfo *wsmanapi.VolumeSnapshotInfo, isPVCEnable bool) {
if !isPVCEnable {
if vsInfo == nil {
return
}
if vsInfo.VolumeSnapshotName == "" && vsInfo.VolumeSnapshotHandle == "" {
return
}
t.Fatalf("it should not contain volume snapshot name %s and volume snapshot handle %s without PVC", vsInfo.VolumeSnapshotName, vsInfo.VolumeSnapshotHandle)
} else {
if vsInfo == nil {
t.Fatal("it should contain volume snapshot info with PVC")
}
if vsInfo.VolumeSnapshotName != "" && vsInfo.VolumeSnapshotHandle != "" {
t.Logf("vsName: %s, vsHandle: %s", vsInfo.VolumeSnapshotName, vsInfo.VolumeSnapshotHandle)
return
}
t.Fatalf("it should contain volume snapshot name %s and volume snapshot handle %s without PVC", vsInfo.VolumeSnapshotName, vsInfo.VolumeSnapshotHandle)
}
}
// checkPVCObject checks the PVC object should exist or not
func checkPVCObject(t *testing.T, api *integration.ComponentAPI, isPVCEnable bool, pvcName string) {
if isPVCEnable {
// check the PVC object exist
if !api.IsPVCExist(pvcName) {
t.Fatal("prebuild PVC object should exist")
}
return
}
// check the PVC object not exist
if api.IsPVCExist(pvcName) {
t.Fatal("prebuild PVC object should not exist")
}
}
// checkPrebuildLogExist checks the prebuild log message exists
func checkPrebuildLogExist(t *testing.T, cfg *envconf.Config, rsa *rpc.Client, ws *integration.LaunchWorkspaceDirectlyResult, wsRoot string) {
// since the message '🤙 This task ran as a workspace prebuild' is generated by
// a prebuild workspace supervisor, so we add a retry mechanism to make sure that we
// won't check the message too earlier before the supervisor generated it.
var (
err error
grepResp agent.ExecResponse
checkPrebuildLog bool
)
err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Dir: prebuildLogPath,
Command: "bash",
Args: []string{
"-c",
fmt.Sprintf("grep %s *", prebuildLog),
},
}, &grepResp)
if err == nil && grepResp.ExitCode == 0 && strings.Trim(grepResp.Stdout, " \t\n") != "" {
checkPrebuildLog = true
}
if checkPrebuildLog {
return
}
t.Logf("cannot found the prebuild message %s in %s, err:%v, exitCode:%d, stdout:%s", prebuildLog, prebuildLogPath, err, grepResp.ExitCode, grepResp.Stdout)
// somehow, the prebuild log message '🤙 This task ran as a workspace prebuild' does not exists
// we fall back to check the the init task message within the /workspace/.gitpod/prebuild-log-* or not
var grepResp1 agent.ExecResponse
var checkInitTaskMsg bool
err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Dir: prebuildLogPath,
Command: "bash",
Args: []string{
"-c",
fmt.Sprintf("grep %q *", initTask),
},
}, &grepResp1)
if err == nil && grepResp1.ExitCode == 0 && strings.Trim(grepResp1.Stdout, " \t\n") != "" {
checkInitTaskMsg = true
}
if checkInitTaskMsg {
return
}
t.Logf("cannot found the init task message %s in %s, err:%v, exitCode:%d, stdout:%s", initTask, prebuildLogPath, err, grepResp.ExitCode, grepResp.Stdout)
// somehow, the init task message does not exist within the /workspace/.gitpod/prebuild-log-*
// we fall back to check the file exists or not
var ls agent.ListDirResponse
err = rsa.Call("WorkspaceAgent.ListDir", &agent.ListDirRequest{
Dir: wsRoot,
}, &ls)
if err != nil {
t.Fatal(err)
}
var found bool
for _, f := range ls.Files {
if filepath.Base(f) == "someFile" {
found = true
break
}
}
if found {
return
}
t.Fatal("did not find someFile from previous workspace instance")
}
// checkFolderPermission checks the folder UID and GID is gitpod
func checkFolderPermission(t *testing.T, rsa *rpc.Client, workspace string) {
var uid agent.ExecResponse
err := rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Command: "stat",
Args: []string{"--format", "%U", workspace},
}, &uid)
if err != nil || uid.ExitCode != 0 || strings.Trim(uid.Stdout, " \t\n") != "gitpod" {
t.Fatalf("folder %s UID %s is incorrect, err:%v, exitCode:%d", workspace, uid.Stdout, err, uid.ExitCode)
}
var gid agent.ExecResponse
err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Command: "stat",
Args: []string{"--format", "%G", workspace},
}, &gid)
if err != nil || uid.ExitCode != 0 || strings.Trim(gid.Stdout, " \t\n") != "gitpod" {
t.Fatalf("folder %s GID %s is incorrect, err:%v, exitCode:%d", workspace, gid.Stdout, err, uid.ExitCode)
}
}
// checkGitFolderPermission checks the files/folders UID and GID under .git/ is gitpod
func checkGitFolderPermission(t *testing.T, rsa *rpc.Client, workspaceRoot string) {
var findUserResp agent.ExecResponse
var gitDir string = fmt.Sprintf("%s/%s", workspaceRoot, ".git")
err := rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Dir: gitDir,
Command: "find",
Args: []string{"!", "-user", "gitpod"},
}, &findUserResp)
if err != nil || findUserResp.ExitCode != 0 || strings.Trim(findUserResp.Stdout, " \t\n") != "" {
t.Fatalf("incorrect UID under %s folder, err:%v, exitCode:%d, stdout:%s", gitDir, err, findUserResp.ExitCode, findUserResp.Stdout)
}
var findGroupResp agent.ExecResponse
err = rsa.Call("WorkspaceAgent.Exec", &agent.ExecRequest{
Dir: gitDir,
Command: "find",
Args: []string{"!", "-group", "gitpod"},
}, &findGroupResp)
if err != nil || findGroupResp.ExitCode != 0 || strings.Trim(findGroupResp.Stdout, " \t\n") != "" {
t.Fatalf("incorrect GID under %s folder, err:%v, exitCode:%d, stdout:%s", gitDir, err, findGroupResp.ExitCode, findGroupResp.Stdout)
}
}
func findVolumeSnapshot(ctx context.Context, isPVCEnable bool, instanceID, namespace string, client klient.Client) (*wsmanapi.VolumeSnapshotInfo, error) {
vsInfo := &wsmanapi.VolumeSnapshotInfo{}
if !isPVCEnable {
return vsInfo, nil
}
vsClient, err := volumesnapshotclientv1.NewForConfig(client.RESTConfig())
if err != nil {
return vsInfo, err
}
vsInfo.VolumeSnapshotName = instanceID
watcher, err := vsClient.SnapshotV1().VolumeSnapshots(namespace).Watch(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("metadata.name=%s", instanceID)})
if err != nil {
return vsInfo, err
}
defer func() {
// stop the volume snapshot watcher
watcher.Stop()
}()
for event := range watcher.ResultChan() {
vs, ok := event.Object.(*volumesnapshotv1.VolumeSnapshot)
if !ok {
return vsInfo, fmt.Errorf("unexpected type assertion %T", event.Object)
}
if vs != nil && vs.Status != nil && vs.Status.ReadyToUse != nil && *vs.Status.ReadyToUse && vs.Status.BoundVolumeSnapshotContentName != nil {
vsInfo.VolumeSnapshotHandle = *vs.Status.BoundVolumeSnapshotContentName
break
}
}
return vsInfo, nil
}
func watchStopWorkspaceAndFindSnapshot(ctx context.Context, instanceId string, api *integration.ComponentAPI) (string, *wsmanapi.VolumeSnapshotInfo, error) {
lastStatus, err := integration.WaitForWorkspaceStop(ctx, api, instanceId)
if err != nil {
return "", nil, err
}
if lastStatus.Conditions.HeadlessTaskFailed != "" {
return "", nil, errors.New("unexpected HeadlessTaskFailed condition")
}
if lastStatus == nil || lastStatus.Conditions == nil || lastStatus.Conditions.VolumeSnapshot == nil {
return "", nil, nil
}
return lastStatus.Conditions.Snapshot, lastStatus.Conditions.VolumeSnapshot, nil
}
func stopWorkspace(t *testing.T, cfg *envconf.Config, StopWorkspaceFunc integration.StopWorkspaceFunc) error {
sctx, scancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer scancel()
sapi := integration.NewComponentAPI(sctx, cfg.Namespace(), kubeconfig, cfg.Client())
defer sapi.Done(t)
_, err := StopWorkspaceFunc(true, sapi)
return err
}