2022-10-06 07:45:21 +02:00

427 lines
13 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"
"fmt"
"path/filepath"
"reflect"
"testing"
"time"
"sigs.k8s.io/e2e-framework/pkg/envconf"
"sigs.k8s.io/e2e-framework/pkg/features"
csapi "github.com/gitpod-io/gitpod/content-service/api"
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"
)
// TestBackup tests a basic start/modify/restart cycle
func TestBackup(t *testing.T) {
f := features.New("backup").
WithLabel("component", "ws-manager").
Assess("it should start a workspace, create a file and successfully create a backup", 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",
WorkspaceRoot: "/workspace/empty",
CheckoutLocation: "empty",
},
{
Name: "pvc",
ContextURL: "https://github.com/gitpod-io/empty",
WorkspaceRoot: "/workspace/empty",
CheckoutLocation: "empty",
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
},
{
Name: "pvc-non-gitpodified",
ContextURL: "https://github.com/gitpod-io/non-gitpodified-repo",
WorkspaceRoot: "/workspace/non-gitpodified-repo",
CheckoutLocation: "non-gitpodified-repo",
FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 20*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
ws1, stopWs1, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(w *wsmanapi.StartWorkspaceRequest) error {
w.Spec.FeatureFlags = test.FF
w.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
w.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatal(err)
}
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws1.Req.Id),
integration.WithContainer("workspace"),
integration.WithWorkspacekitLift(true),
)
if err != nil {
if _, err := stopWs1(true, api); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Fatal(err)
}
integration.DeferCloser(t, closer)
var resp agent.WriteFileResponse
err = rsa.Call("WorkspaceAgent.WriteFile", &agent.WriteFileRequest{
Path: fmt.Sprintf("%s/foobar.txt", test.WorkspaceRoot),
Content: []byte("hello world"),
Mode: 0644,
}, &resp)
rsa.Close()
if err != nil {
if _, err = stopWs1(true, api); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Fatal(err)
}
lastStatusWs1, err := stopWs1(true, api)
if err != nil {
t.Fatal(err)
}
ws2, stopWs2, err := integration.LaunchWorkspaceDirectly(t, ctx, api,
integration.WithRequestModifier(func(w *wsmanapi.StartWorkspaceRequest) error {
w.ServicePrefix = ws1.Req.ServicePrefix
w.Metadata.MetaId = ws1.Req.Metadata.MetaId
w.Metadata.Owner = ws1.Req.Metadata.Owner
w.Spec.FeatureFlags = ws1.Req.Spec.FeatureFlags
w.Spec.Initializer = ws1.Req.Spec.Initializer
w.Spec.WorkspaceLocation = ws1.Req.Spec.WorkspaceLocation
if !reflect.DeepEqual(w.Spec.FeatureFlags, []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM}) {
return nil
}
if lastStatusWs1 != nil && lastStatusWs1.Conditions != nil && lastStatusWs1.Conditions.VolumeSnapshot != nil {
w.Spec.VolumeSnapshot = lastStatusWs1.Conditions.VolumeSnapshot
}
return nil
}),
)
if err != nil {
t.Fatal(err)
}
defer func() {
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 = stopWs2(true, sapi)
if err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
}()
rsa, closer, err = integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws2.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)
}
// TestExistingWorkspaceEnablePVC tests enable PVC feature flag on the existing workspace
func TestExistingWorkspaceEnablePVC(t *testing.T) {
f := features.New("backup").
WithLabel("component", "ws-manager").
Assess("it should enable PVC feature flag to the existing workspace without data loss", func(_ context.Context, t *testing.T, cfg *envconf.Config) context.Context {
tests := []struct {
Name string
ContextURL string
WorkspaceRoot string
CheckoutLocation string
}{
{
Name: "pvc",
ContextURL: "http://github.com/gitpod-io/template-golang-cli",
WorkspaceRoot: "/workspace/template-golang-cli",
CheckoutLocation: "template-golang-cli",
},
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
for _, test := range tests {
api := integration.NewComponentAPI(ctx, cfg.Namespace(), kubeconfig, cfg.Client())
t.Cleanup(func() {
api.Done(t)
})
// Create a new workspace without the PVC feature flag
// 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
ws1, stopWs1, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(w *wsmanapi.StartWorkspaceRequest) error {
w.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Git{
Git: &csapi.GitInitializer{
RemoteUri: test.ContextURL,
CheckoutLocation: test.CheckoutLocation,
Config: &csapi.GitConfig{},
},
},
}
w.Spec.WorkspaceLocation = test.CheckoutLocation
return nil
}))
if err != nil {
t.Fatal(err)
}
rsa, closer, err := integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws1.Req.Id),
integration.WithContainer("workspace"),
integration.WithWorkspacekitLift(true),
)
if err != nil {
if _, err := stopWs1(true, api); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Fatal(err)
}
integration.DeferCloser(t, closer)
var resp agent.WriteFileResponse
err = rsa.Call("WorkspaceAgent.WriteFile", &agent.WriteFileRequest{
Path: fmt.Sprintf("%s/foobar.txt", test.WorkspaceRoot),
Content: []byte("hello world"),
Mode: 0644,
}, &resp)
rsa.Close()
if err != nil {
if _, err := stopWs1(true, api); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Fatal(err)
}
_, err = stopWs1(true, api)
if err != nil {
t.Fatal(err)
}
// Relaunch the workspace and enable the PVC feature flag
ws2, stopWs2, err := integration.LaunchWorkspaceDirectly(t, ctx, api,
integration.WithRequestModifier(func(w *wsmanapi.StartWorkspaceRequest) error {
w.ServicePrefix = ws1.Req.ServicePrefix
w.Metadata.MetaId = ws1.Req.Metadata.MetaId
w.Metadata.Owner = ws1.Req.Metadata.Owner
w.Spec.FeatureFlags = []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM}
w.Spec.Initializer = ws1.Req.Spec.Initializer
w.Spec.WorkspaceLocation = ws1.Req.Spec.WorkspaceLocation
return nil
}),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
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 = stopWs2(true, sapi)
if err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
})
rsa, closer, err = integration.Instrument(integration.ComponentWorkspace, "workspace", cfg.Namespace(), kubeconfig, cfg.Client(),
integration.WithInstanceID(ws2.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)
}
// TestMissingBackup ensures workspaces fail if they should have a backup but don't have one
func TestMissingBackup(t *testing.T) {
f := features.New("CreateWorkspace").
WithLabel("component", "ws-manager").
Assess("it ensures workspace fail if they should have a backup but don't have one", 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)
if err != nil {
t.Fatal(err)
}
wsm, err := api.WorkspaceManager()
if err != nil {
if _, err := stopWs(true, api); err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Fatal(err)
}
_, err = wsm.StopWorkspace(ctx, &wsmanapi.StopWorkspaceRequest{Id: ws.Req.Id})
if err != nil {
t.Fatal(err)
}
_, err = integration.WaitForWorkspaceStop(ctx, api, ws.Req.Id)
if err != nil {
t.Fatal(err)
}
contentSvc, err := api.ContentService()
if err != nil {
t.Fatal(err)
}
_, err = contentSvc.DeleteWorkspace(ctx, &csapi.DeleteWorkspaceRequest{
OwnerId: ws.Req.Metadata.Owner,
WorkspaceId: ws.Req.Metadata.MetaId,
})
if err != nil {
t.Fatal(err)
}
tests := []struct {
Name string
FF []wsmanapi.WorkspaceFeatureFlag
}{
{Name: "classic"},
{Name: "fwb", FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_FULL_WORKSPACE_BACKUP}},
{Name: "pvc", FF: []wsmanapi.WorkspaceFeatureFlag{wsmanapi.WorkspaceFeatureFlag_PERSISTENT_VOLUME_CLAIM}},
}
for _, test := range tests {
t.Run(test.Name+"_backup_init", func(t *testing.T) {
testws, stopWs, err := integration.LaunchWorkspaceDirectly(t, ctx, api, integration.WithRequestModifier(func(w *wsmanapi.StartWorkspaceRequest) error {
w.ServicePrefix = ws.Req.ServicePrefix
w.Metadata.MetaId = ws.Req.Metadata.MetaId
w.Metadata.Owner = ws.Req.Metadata.Owner
w.Spec.Initializer = &csapi.WorkspaceInitializer{
Spec: &csapi.WorkspaceInitializer_Backup{
Backup: &csapi.FromBackupInitializer{},
},
}
w.Spec.FeatureFlags = test.FF
return nil
}), integration.WithWaitWorkspaceForOpts(integration.WorkspaceCanFail))
if err != nil {
t.Fatal(err)
}
if testws.LastStatus == nil {
t.Fatal("did not receive a last status")
return
}
if testws.LastStatus.Conditions.Failed == "" {
_, err = stopWs(true, api)
if err != nil {
t.Errorf("cannot stop workspace: %q", err)
}
t.Errorf("restarted workspace did not fail despite missing backup, %v", testws)
}
})
}
return ctx
}).
Feature()
testEnv.Test(t, f)
}