Skip to content

Commit

Permalink
neonvm-controller: introduce unit tests (#935)
Browse files Browse the repository at this point in the history
First PR, introducing unit tests. The old functional tests are moved to
a `functest` directory.

Part of #763

---------

Signed-off-by: Oleg Vasilev <[email protected]>
  • Loading branch information
Omrigan authored Jun 21, 2024
1 parent a77b07b commit 9de695e
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ testbin/*

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
cover.html

# Kubernetes Generated files - skip generated files, except for vendored files

Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ vet: ## Run go vet against code.
CGO_ENABLED=0 go vet ./...


TESTARGS ?= ./...
.PHONY: test
test: fmt vet envtest ## Run tests.
# chmodding KUBEBUILDER_ASSETS dir to make it deletable by owner,
Expand All @@ -107,7 +108,8 @@ test: fmt vet envtest ## Run tests.
export KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)"; \
find $(KUBEBUILDER_ASSETS) -type d -exec chmod 0755 {} \; ; \
CGO_ENABLED=0 \
go test ./... -coverprofile cover.out
go test $(TESTARGS) -coverprofile cover.out
go tool cover -html=cover.out -o cover.html

##@ Build

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ require (
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
go.etcd.io/etcd/api/v3 v3.5.6 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.6 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers
package functests

import (
"path/filepath"
Expand Down Expand Up @@ -51,7 +51,7 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers
package functests

import (
"context"
Expand All @@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/types"

vmv1 "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1"
"github.com/neondatabase/autoscaling/neonvm/controllers"
)

var _ = Describe("VirtualMachine controller", func() {
Expand Down Expand Up @@ -101,11 +102,11 @@ var _ = Describe("VirtualMachine controller", func() {
}, time.Minute, time.Second).Should(Succeed())

By("Reconciling the custom resource created")
virtualmachineReconciler := &VMReconciler{
virtualmachineReconciler := &controllers.VMReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Recorder: nil,
Config: &ReconcilerConfig{
Config: &controllers.ReconcilerConfig{
IsK3s: false,
UseContainerMgr: true,
MaxConcurrentReconciles: 1,
Expand Down
252 changes: 252 additions & 0 deletions neonvm/controllers/vm_controller_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package controllers

import (
"context"
"encoding/json"
"os"
"testing"
"time"

"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

vmv1 "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1"
)

type mockRecorder struct {
mock.Mock
}

func (m *mockRecorder) Event(object runtime.Object, eventtype, reason, message string) {
m.Called(object, eventtype, reason, message)
}

func (m *mockRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
m.Called(object, eventtype, reason, messageFmt, args)
}

func (m *mockRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) {
m.Called(object, annotations, eventtype, reason, messageFmt, args)
}

// defaultVm returns a VM which is similar to what we can reasonably
// expect from the control plane.
func defaultVm() *vmv1.VirtualMachine {
return &vmv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vm",
Namespace: "default",
},
Spec: vmv1.VirtualMachineSpec{
EnableSSH: lo.ToPtr(false),
EnableAcceleration: lo.ToPtr(true),
//nolint:exhaustruct // This is a test
Guest: vmv1.Guest{
KernelImage: lo.ToPtr("kernel-img"),
AppendKernelCmdline: nil,
CPUs: vmv1.CPUs{
Min: lo.ToPtr(vmv1.MilliCPU(1000)),

Check failure on line 61 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(vmv1.MilliCPU(1000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal

Check failure on line 61 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(vmv1.MilliCPU(1000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal

Check failure on line 61 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(vmv1.MilliCPU(1000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal
Max: lo.ToPtr(vmv1.MilliCPU(2000)),

Check failure on line 62 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(vmv1.MilliCPU(2000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal

Check failure on line 62 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(vmv1.MilliCPU(2000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal

Check failure on line 62 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(vmv1.MilliCPU(2000)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal
Use: lo.ToPtr(vmv1.MilliCPU(1500)),

Check failure on line 63 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(vmv1.MilliCPU(1500)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal

Check failure on line 63 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(vmv1.MilliCPU(1500)) (value of type *"github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU) as "github.com/neondatabase/autoscaling/neonvm/apis/neonvm/v1".MilliCPU value in struct literal
},
MemorySlots: vmv1.MemorySlots{
Min: lo.ToPtr(int32(1)),

Check failure on line 66 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(int32(1)) (value of type *int32) as int32 value in struct literal

Check failure on line 66 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(int32(1)) (value of type *int32) as int32 value in struct literal
Max: lo.ToPtr(int32(32)),

Check failure on line 67 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(int32(32)) (value of type *int32) as int32 value in struct literal

Check failure on line 67 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(int32(32)) (value of type *int32) as int32 value in struct literal
Use: lo.ToPtr(int32(2)),

Check failure on line 68 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / tests

cannot use lo.ToPtr(int32(2)) (value of type *int32) as int32 value in struct literal

Check failure on line 68 in neonvm/controllers/vm_controller_unit_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use lo.ToPtr(int32(2)) (value of type *int32) as int32 value in struct literal (typecheck)
},
MemorySlotSize: resource.MustParse("1Gi"),
},
},
//nolint:exhaustruct // Intentionally left empty
Status: vmv1.VirtualMachineStatus{},
}
}

type testParams struct {
t *testing.T
ctx context.Context
r *VMReconciler
client client.Client
origVM *vmv1.VirtualMachine
mockRecorder *mockRecorder
}

var reconcilerMetrics = MakeReconcilerMetrics()

func newTestParams(t *testing.T) *testParams {
os.Setenv("VM_RUNNER_IMAGE", "vm-runner-img")

logger := zap.New(zap.UseDevMode(true), zap.WriteTo(os.Stdout),
zap.Level(zapcore.DebugLevel))
ctx := log.IntoContext(context.Background(), logger)

scheme := runtime.NewScheme()
scheme.AddKnownTypes(vmv1.SchemeGroupVersion, &vmv1.VirtualMachine{})
scheme.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Pod{})

params := &testParams{
t: t,
ctx: ctx,
client: fake.NewClientBuilder().WithScheme(scheme).Build(),
//nolint:exhaustruct // This is a mock
mockRecorder: &mockRecorder{},
r: nil,
origVM: nil,
}

params.r = &VMReconciler{
Client: params.client,
Recorder: params.mockRecorder,
Scheme: scheme,
Config: &ReconcilerConfig{
IsK3s: false,
UseContainerMgr: false,
MaxConcurrentReconciles: 10,
QEMUDiskCacheSettings: "",
FailurePendingPeriod: time.Minute,
FailingRefreshInterval: time.Minute,
},
Metrics: reconcilerMetrics,
}

return params
}

// initVM initializes the VM in the fake client and returns the VM
func (p *testParams) initVM(vm *vmv1.VirtualMachine) *vmv1.VirtualMachine {
err := p.client.Create(p.ctx, vm)
require.NoError(p.t, err)
p.origVM = vm

// Do serialize/deserialize, to normalize resource.Quantity
return p.getVM()
}

func (p *testParams) getVM() *vmv1.VirtualMachine {
var obj vmv1.VirtualMachine
err := p.client.Get(p.ctx, client.ObjectKeyFromObject(p.origVM), &obj)
require.NoError(p.t, err)

return &obj
}

func TestReconcile(t *testing.T) {
params := newTestParams(t)
origVM := params.initVM(defaultVm())

req := reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(origVM),
}

// Round 1
res, err := params.r.Reconcile(params.ctx, req)
assert.NoError(t, err)

// Added finalizer
assert.Equal(t, reconcile.Result{
Requeue: true,
}, res)
assert.Contains(t, params.getVM().Finalizers, virtualmachineFinalizer)

// Round 2
res, err = params.r.Reconcile(params.ctx, req)
assert.NoError(t, err)
assert.Equal(t, false, res.Requeue)

// VM is pending
assert.Equal(t, vmv1.VmPending, params.getVM().Status.Phase)

// Round 3
params.mockRecorder.On("Event", mock.Anything, "Normal", "Created",
mock.Anything)
res, err = params.r.Reconcile(params.ctx, req)
assert.NoError(t, err)
assert.Equal(t, false, res.Requeue)

// We now have a pod
vm := params.getVM()
assert.NotEmpty(t, vm.Status.PodName)
// Spec is unchanged
assert.Equal(t, vm.Spec, origVM.Spec)

// Round 4
res, err = params.r.Reconcile(params.ctx, req)
assert.NoError(t, err)
assert.Equal(t, false, res.Requeue)

// Nothing is updating the pod status, so nothing changes in VM as well
assert.Equal(t, vm, params.getVM())
}

func prettyPrint(t *testing.T, obj any) {
s, _ := json.MarshalIndent(obj, "", "\t")
t.Logf("%s\n", s)
}

func TestRunningPod(t *testing.T) {
params := newTestParams(t)
origVM := defaultVm()
origVM.Finalizers = append(origVM.Finalizers, virtualmachineFinalizer)
origVM.Status.Phase = vmv1.VmPending

origVM = params.initVM(origVM)

req := reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(origVM),
}

// Round 1
params.mockRecorder.On("Event", mock.Anything, "Normal", "Created",
mock.Anything)
res, err := params.r.Reconcile(params.ctx, req)
require.NoError(t, err)
assert.Equal(t, false, res.Requeue)

// We now have a pod
podName := params.getVM().Status.PodName
podKey := client.ObjectKey{
Namespace: origVM.Namespace,
Name: podName,
}
var pod corev1.Pod
err = params.client.Get(params.ctx, podKey, &pod)
require.NoError(t, err)

assert.Len(t, pod.Spec.Containers, 1)
assert.Equal(t, "neonvm-runner", pod.Spec.Containers[0].Name)
assert.Equal(t, "vm-runner-img", pod.Spec.Containers[0].Image)
assert.Len(t, pod.Spec.InitContainers, 2)
assert.Equal(t, "init", pod.Spec.InitContainers[0].Name)
assert.Equal(t, "init-kernel", pod.Spec.InitContainers[1].Name)

prettyPrint(t, pod)

pod.Status.Phase = corev1.PodRunning
err = params.client.Update(params.ctx, &pod)
require.NoError(t, err)

// Round 2
res, err = params.r.Reconcile(params.ctx, req)
require.NoError(t, err)
assert.Equal(t, false, res.Requeue)

vm := params.getVM()

// VM is now running
assert.Equal(t, vmv1.VmRunning, vm.Status.Phase)
assert.Len(t, vm.Status.Conditions, 1)
assert.Equal(t, vm.Status.Conditions[0].Type, typeAvailableVirtualMachine)
}

0 comments on commit 9de695e

Please sign in to comment.