blob: e51ebe5fe69d37c43d1f4ff70a14a0c54592b629 [file] [log] [blame] [edit]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
pb "go.chromium.org/chromiumos/vm_tools/tremplin_proto"
"context"
"errors"
"fmt"
"io"
"log"
"strings"
"testing"
"time"
lxd "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared/api"
"google.golang.org/grpc"
)
type lxdStub struct {
lxd.InstanceServer
operation *operationStub
execError error
osRelease string
lastExec *api.InstanceExecPost
}
type operationStub struct {
lxd.Operation
apiOperation api.Operation
waitTime time.Duration
waitError error
out io.WriteCloser
}
type listenerStub struct {
pb.TremplinListenerClient
validator func(*pb.UpgradeContainerProgress)
}
type containerFileServerStub struct {
lxd *lxdStub
}
func (s *lxdStub) ExecInstance(containerName string, exec api.InstanceExecPost, args *lxd.InstanceExecArgs) (lxd.Operation, error) {
s.operation.out = args.Stdout
s.lastExec = &exec
args.Stdout.Write([]byte("In-progress message\n"))
return s.operation, s.execError
}
func (s *lxdStub) UpdateInstanceState(containerName string, statePut api.InstanceStatePut, etag string) (lxd.Operation, error) {
return s.operation, s.execError
}
func (s *lxdStub) GetInstanceFile(containerName, path string) (io.ReadCloser, *lxd.InstanceFileResponse, error) {
reader := io.NopCloser(strings.NewReader(s.osRelease))
resp := lxd.InstanceFileResponse{}
return reader, &resp, nil
}
func (s *containerFileServerStub) GetInstanceFile(path string) (io.ReadCloser, *lxd.InstanceFileResponse, error) {
return s.lxd.GetInstanceFile("unused", path)
}
func (s *containerFileServerStub) CreateInstanceFile(path string, args lxd.InstanceFileArgs) (err error) {
log.Fatal("Not implemented")
return nil
}
func (s *containerFileServerStub) DeleteInstanceFile(path string) (err error) {
log.Fatal("Not implemented")
return nil
}
func (s operationStub) Wait() (err error) {
time.Sleep(s.waitTime)
s.out.Write([]byte("Last in-progress message\nDone message\n"))
return s.waitError
}
func (s operationStub) Get() api.Operation {
return s.apiOperation
}
func (s listenerStub) UpgradeContainerStatus(ctx context.Context, in *pb.UpgradeContainerProgress, opts ...grpc.CallOption) (*pb.EmptyMessage, error) {
if s.validator != nil {
s.validator(in)
}
return &pb.EmptyMessage{}, nil
}
func (s listenerStub) ContainerShutdown(ctx context.Context, in *pb.ContainerShutdownInfo, opts ...grpc.CallOption) (*pb.EmptyMessage, error) {
return &pb.EmptyMessage{}, nil
}
func makeOsRelease(codename string) string {
return fmt.Sprintf("ID=debian\nVERSION_CODENAME=%s", codename)
}
func makeStubs(returnCode float64, waitTime time.Duration, validator func(*pb.UpgradeContainerProgress)) (*tremplinServer, *lxdStub, *operationStub) {
metadata := map[string]interface{}{
"return": returnCode,
}
apiOp := api.Operation{
Metadata: metadata,
}
op := &operationStub{
apiOperation: apiOp,
waitTime: waitTime,
waitError: nil,
}
listener := listenerStub{
validator: validator,
}
lxd := &lxdStub{
operation: op,
osRelease: makeOsRelease("stretch"),
}
cfs := &containerFileServerStub{lxd: lxd}
OverrideInstanceFileServerForTesting(cfs)
server := &tremplinServer{
lxd: lxd,
upgradeStatus: *NewTransactionMap(),
listenerClient: listener,
upgradeClientUpdateInterval: 5 * time.Millisecond,
}
return server, lxd, op
}
func contains(arr []string, needle string) bool {
for _, line := range arr {
if line == needle {
return true
}
}
return false
}
func TestStartUpgradeContainerLimitOneInProgressPerContainer(t *testing.T) {
done := make(chan bool)
callback := func(status *pb.UpgradeContainerProgress) {
done <- true
}
// Our upgrade sleeps for a very long time in the background, we don't wait for or need it to finish so it's fine.
// This needs to be long enough that the second Start runs before the first finishes.
server, _, _ := makeStubs(0.0, 1*time.Minute, callback)
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to start, got status %v and message %s", status, msg)
}
status, msg = server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_ALREADY_RUNNING {
t.Fatalf("Failed to correctly fail when an upgrade is already running, got status %v and message %s", status, msg)
}
status, msg = server.startUpgradeContainer("test2", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to allow multiple upgrades on separate containers, got status %v and message %s", status, msg)
}
<-done
}
// If the upgrade fails we should be able to try upgrading again.
func TestStartUpgradeContainerCanRetryAfterFailure(t *testing.T) {
done := make(chan bool)
callback := func(status *pb.UpgradeContainerProgress) {
if status.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
done <- true
}
}
server, lxd, operation := makeStubs(0.0, 0*time.Millisecond, callback)
// Immediate failure.
lxd.execError = errors.New("I'm a test error :)")
status, _ := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_FAILED {
t.Fatal("StartUpgrade didn't fail when it should've failed")
}
// Eventual failure.
lxd.execError = nil
operation.apiOperation.Metadata["return"] = 1.0
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to start on retry after immediate failure, got status %v and message %s", status, msg)
}
// Wait until the above completes.
<-done
status, msg = server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to start on retry after eventual failure, got status %v and message %s", status, msg)
}
}
func TestUpgradeContainerVersionValidation(t *testing.T) {
server, _, _ := makeStubs(127.0, 0*time.Millisecond, nil)
tables := []struct {
codename string
to pb.UpgradeContainerRequest_Version
expected pb.UpgradeContainerResponse_Status
expectedArgs []string
}{
{"", pb.UpgradeContainerRequest_DEBIAN_BUSTER, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"bookworm/sid", pb.UpgradeContainerRequest_DEBIAN_BUSTER, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"stretch", pb.UpgradeContainerRequest_UNKNOWN, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"buster", pb.UpgradeContainerRequest_UNKNOWN, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"bullseye", pb.UpgradeContainerRequest_UNKNOWN, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"stretch", pb.UpgradeContainerRequest_DEBIAN_STRETCH, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"buster", pb.UpgradeContainerRequest_DEBIAN_STRETCH, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"bullseye", pb.UpgradeContainerRequest_DEBIAN_STRETCH, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"stretch", pb.UpgradeContainerRequest_DEBIAN_BUSTER, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath, "DEBIAN_BUSTER"}},
{"buster", pb.UpgradeContainerRequest_DEBIAN_BUSTER, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath}},
{"bullseye", pb.UpgradeContainerRequest_DEBIAN_BUSTER, pb.UpgradeContainerResponse_NOT_SUPPORTED, []string{}},
{"stretch", pb.UpgradeContainerRequest_DEBIAN_BULLSEYE, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath, "DEBIAN_BUSTER", "DEBIAN_BULLSEYE"}},
{"buster", pb.UpgradeContainerRequest_DEBIAN_BULLSEYE, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath, "DEBIAN_BULLSEYE"}},
{"bullseye", pb.UpgradeContainerRequest_DEBIAN_BULLSEYE, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath}},
{"buster", pb.UpgradeContainerRequest_DEBIAN_BOOKWORM, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath, "DEBIAN_BULLSEYE", "DEBIAN_BOOKWORM"}},
{"bullseye", pb.UpgradeContainerRequest_DEBIAN_BOOKWORM, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath, "DEBIAN_BOOKWORM"}},
{"bookworm", pb.UpgradeContainerRequest_DEBIAN_BOOKWORM, pb.UpgradeContainerResponse_STARTED, []string{upgradeScriptPath}},
}
for _, table := range tables {
server.lxd.(*lxdStub).lastExec = nil
server.lxd.(*lxdStub).osRelease = makeOsRelease(table.codename)
status, err := server.startUpgradeContainer(fmt.Sprintf("%s -> %s", table.codename, table.to.String()), table.to)
if status != table.expected {
t.Errorf("status of (%s -> %s) was incorrect, got: %v, want: %v.", table.codename, table.to.String(), status, table.expected)
t.Errorf("response message: %q", err)
}
var actualArgs []string
if server.lxd.(*lxdStub).lastExec != nil {
actualArgs = server.lxd.(*lxdStub).lastExec.Command
}
argsCorrect := true
var expectedArgs []string
if len(table.expectedArgs) > 0 {
cmdPrefix := []string{"setsid"}
expectedArgs = append(cmdPrefix, table.expectedArgs...)
} else {
expectedArgs = table.expectedArgs
}
for i := range expectedArgs {
if expectedArgs[i] != actualArgs[i] {
argsCorrect = false
break
}
}
if !argsCorrect || len(expectedArgs) != len(actualArgs) {
t.Errorf("(%s -> %d): Ran unexpected command %v and not %v", table.codename, table.to, actualArgs, expectedArgs)
}
}
}
func TestUpgradeContainerSendsInProgressMessages(t *testing.T) {
statusChannel := make(chan *pb.UpgradeContainerProgress)
callback := func(status *pb.UpgradeContainerProgress) {
statusChannel <- status
fmt.Println(status.ProgressMessages)
}
// Our upgrade sleeps for a very long time in the background, we don't wait for or need it to finish so it's fine.
// This needs to be long enough that we see an in-progress message get sent.
server, _, _ := makeStubs(0.0, 1*time.Minute, callback)
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("failed to start, got %v and message %s", status, msg)
}
s := <-statusChannel
if s.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
t.Fatalf("didn't get in-progress message, got %v", s)
}
if len(s.ProgressMessages) == 0 {
t.Fatal("didn't get any progress messages")
}
}
func TestUpgradeContainerSendsSuccessOnSuccessfulEnd(t *testing.T) {
ch := make(chan *pb.UpgradeContainerProgress)
callback := func(status *pb.UpgradeContainerProgress) {
if status.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
ch <- status
}
}
server, _, _ := makeStubs(0.0, 0*time.Millisecond, callback)
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("failed to start, got %v and message %s", status, msg)
}
s := <-ch
if s.Status != pb.UpgradeContainerProgress_SUCCEEDED {
t.Fatalf("didn't get success message, got %v", s)
}
if !contains(s.ProgressMessages, "Done message") {
t.Fatalf("didn't see expected end message, only saw: %v", s.ProgressMessages)
}
}
func TestUpgradeContainerSendsFailureOnUnsuccessfulEnd(t *testing.T) {
statusChannel := make(chan pb.UpgradeContainerProgress_Status)
callback := func(status *pb.UpgradeContainerProgress) {
if status.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
statusChannel <- status.Status
}
}
server, _, _ := makeStubs(127.0, 0*time.Millisecond, callback)
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to start, got %v and message %s", status, msg)
}
if s := <-statusChannel; s != pb.UpgradeContainerProgress_FAILED {
t.Fatalf("Didn't get failure message, got %v", s)
}
}
func TestUpgradeContainerSendsFailureOnLxdError(t *testing.T) {
statusChannel := make(chan pb.UpgradeContainerProgress_Status)
callback := func(status *pb.UpgradeContainerProgress) {
if status.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
statusChannel <- status.Status
}
}
server, _, op := makeStubs(0.0, 0*time.Millisecond, callback)
op.waitError = errors.New("I'm a test error :)")
status, msg := server.startUpgradeContainer("test1", pb.UpgradeContainerRequest_DEBIAN_BUSTER)
if status != pb.UpgradeContainerResponse_STARTED {
t.Fatalf("Failed to start, got %v and message %s", status, msg)
}
if s := <-statusChannel; s != pb.UpgradeContainerProgress_FAILED {
t.Fatalf("Didn't get failure message, got %v", s)
}
}
func TestCancelNotRunning(t *testing.T) {
ch := make(chan *pb.UpgradeContainerProgress)
callback := func(status *pb.UpgradeContainerProgress) {
if status.Status != pb.UpgradeContainerProgress_IN_PROGRESS {
ch <- status
}
}
server, _, _ := makeStubs(0.0, 0*time.Millisecond, callback)
status, _ := server.cancelUpgradeContainer("test")
if status != pb.CancelUpgradeContainerResponse_NOT_RUNNING {
t.Errorf("Unexpected status, got: %v, want: %v.", status, pb.CancelUpgradeContainerResponse_NOT_RUNNING)
}
}