kubeadm/operator/controllers/operation_controller_test.go

635 lines
16 KiB
Go

/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/runtime/log"
operatorv1 "k8s.io/kubeadm/operator/api/v1alpha1"
)
func TestOperatorReconcilePhase(t *testing.T) {
tx := metav1.Now()
errMessage := "error"
tests := []struct {
name string
input *operatorv1.Operation
expected *operatorv1.Operation
}{
{
name: "Reconcile pending state",
input: &operatorv1.Operation{
Status: operatorv1.OperationStatus{},
},
expected: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
Phase: string(operatorv1.OperationPhasePending),
},
},
},
{
name: "Reconcile running state",
input: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
},
},
expected: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
Phase: string(operatorv1.RuntimeTaskPhaseRunning),
},
},
},
{
name: "Reconcile paused state",
input: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
Paused: true,
},
},
expected: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
Paused: true,
Phase: string(operatorv1.OperationPhasePaused),
},
},
},
{
name: "Reconcile succeeded state",
input: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
CompletionTime: &tx,
},
},
expected: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
CompletionTime: &tx,
Phase: string(operatorv1.OperationPhaseSucceeded),
},
},
},
{
name: "Reconcile failed state",
input: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
ErrorMessage: &errMessage,
},
},
expected: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: &tx,
ErrorMessage: &errMessage,
Phase: string(operatorv1.OperationPhaseFailed),
},
},
},
{
name: "Reconcile deleted state",
input: &operatorv1.Operation{
ObjectMeta: metav1.ObjectMeta{
DeletionTimestamp: &tx,
},
},
expected: &operatorv1.Operation{
ObjectMeta: metav1.ObjectMeta{
DeletionTimestamp: &tx,
},
Status: operatorv1.OperationStatus{
Phase: string(operatorv1.OperationPhaseDeleted),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &OperationReconciler{}
r.reconcilePhase(tt.input)
if !reflect.DeepEqual(tt.input, tt.expected) {
t.Errorf("expected %v, got %v", tt.expected, tt.input)
}
})
}
}
func TestOperatorReconcilePause(t *testing.T) {
type expected struct {
operation *operatorv1.Operation
events int
}
tests := []struct {
name string
input *operatorv1.Operation
expected expected
}{
{
name: "Reconcile an Operation not paused with Spec paused",
input: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: true,
},
Status: operatorv1.OperationStatus{
Paused: false,
},
},
expected: expected{
operation: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: true,
},
Status: operatorv1.OperationStatus{
Paused: true,
},
},
events: 1,
},
},
{
name: "Reconcile an Operation paused with Spec not paused",
input: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: false,
},
Status: operatorv1.OperationStatus{
Paused: true,
},
},
expected: expected{
operation: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: false,
},
Status: operatorv1.OperationStatus{
Paused: false,
},
},
events: 1,
},
},
{
name: "Reconcile an Operation paused with Spec paused",
input: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: true,
},
Status: operatorv1.OperationStatus{
Paused: true,
},
},
expected: expected{
operation: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: true,
},
Status: operatorv1.OperationStatus{
Paused: true,
},
},
events: 0,
},
},
{
name: "Reconcile an Operation not paused with Spec not paused",
input: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: false,
},
Status: operatorv1.OperationStatus{
Paused: false,
},
},
expected: expected{
operation: &operatorv1.Operation{
Spec: operatorv1.OperationSpec{
Paused: false,
},
Status: operatorv1.OperationStatus{
Paused: false,
},
},
events: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec := record.NewFakeRecorder(1)
r := &OperationReconciler{
recorder: rec,
}
r.reconcilePause(tt.input)
if !reflect.DeepEqual(tt.input, tt.expected.operation) {
t.Errorf("expected %v, got %v", tt.expected.operation, tt.input)
}
if tt.expected.events != len(rec.Events) {
t.Errorf("expected %v, got %v", tt.expected.events, len(rec.Events))
}
})
}
}
func TestOperationReconciler_Reconcile(t *testing.T) {
type fields struct {
Objs []runtime.Object
}
type args struct {
req ctrl.Request
}
tests := []struct {
name string
fields fields
args args
want ctrl.Result
wantErr bool
}{
{
name: "Reconcile does nothing if operation does not exist",
fields: fields{},
args: args{
req: ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "foo"}},
},
want: ctrl.Result{},
wantErr: false,
},
{
name: "Reconcile does nothing if the operation is already completed",
fields: fields{
Objs: []runtime.Object{
&operatorv1.Operation{
TypeMeta: metav1.TypeMeta{
Kind: "Operation",
APIVersion: operatorv1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo-operation",
},
Status: operatorv1.OperationStatus{
CompletionTime: timePtr(metav1.Now()),
},
},
},
},
args: args{
req: ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "foo-operation"}},
},
want: ctrl.Result{},
wantErr: false,
},
{
name: "Reconcile pass",
fields: fields{
Objs: []runtime.Object{
&operatorv1.Operation{
TypeMeta: metav1.TypeMeta{
Kind: "Operation",
APIVersion: operatorv1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo-operation",
},
Spec: operatorv1.OperationSpec{
OperatorDescriptor: operatorv1.OperatorDescriptor{
CustomOperation: &operatorv1.CustomOperationSpec{},
},
},
},
},
},
args: args{
req: ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "foo-operation"}},
},
want: ctrl.Result{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &OperationReconciler{
Client: fake.NewFakeClientWithScheme(setupScheme(), tt.fields.Objs...),
AgentImage: "some-image", //making reconcile operation pass
MetricsRBAC: false,
Log: log.Log,
recorder: record.NewFakeRecorder(1),
}
got, err := r.Reconcile(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("Reconcile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Reconcile() got = %v, want %v", got, tt.want)
}
})
}
}
func TestOperationReconciler_reconcileNormal(t *testing.T) {
type args struct {
operation *operatorv1.Operation
taskGroups *taskGroupReconcileList
}
type want struct {
operation *operatorv1.Operation
}
tests := []struct {
name string
args args
want want
wantTaskGroups int
wantErr bool
}{
{
name: "Reconcile sets error if a taskGroup is failed and no taskGroup is active",
args: args{
operation: &operatorv1.Operation{},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
},
failed: []*taskGroupReconcileItem{
{},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
ErrorMessage: stringPtr("error"), //using error as a marker for "whatever error was raised"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile sets error if a taskGroup is invalid and no taskGroup is active",
args: args{
operation: &operatorv1.Operation{},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
},
invalid: []*taskGroupReconcileItem{
{},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
ErrorMessage: stringPtr("error"), //using error as a marker for "whatever error was raised"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile set start time",
args: args{
operation: &operatorv1.Operation{},
taskGroups: &taskGroupReconcileList{},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile reset error if a taskGroup is active",
args: args{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
ErrorMessage: stringPtr("error"),
},
},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
{},
},
running: []*taskGroupReconcileItem{
{},
},
failed: []*taskGroupReconcileItem{ // failed should be ignored if a taskGroup is active
{},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile set completion time if no more taskGroup to create",
args: args{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Now()),
},
},
taskGroups: &taskGroupReconcileList{}, //empty list of nodes -> no more task to create
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
CompletionTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it completed"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile do nothing if paused",
args: args{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Now()),
Paused: true,
},
},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
},
tobeCreated: []*taskGroupReconcileItem{
{},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
Paused: true,
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
{
name: "Reconcile creates a taskGroup if nothing running",
args: args{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Now()),
},
},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
},
tobeCreated: []*taskGroupReconcileItem{
{
planned: &operatorv1.RuntimeTaskGroup{},
},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
},
},
},
wantTaskGroups: 1,
wantErr: false,
},
{
name: "Reconcile does not creates a taskGroup if something running",
args: args{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Now()),
},
},
taskGroups: &taskGroupReconcileList{
all: []*taskGroupReconcileItem{
{},
{},
},
tobeCreated: []*taskGroupReconcileItem{
{},
},
running: []*taskGroupReconcileItem{
{},
},
},
},
want: want{
operation: &operatorv1.Operation{
Status: operatorv1.OperationStatus{
StartTime: timePtr(metav1.Time{}), //using zero as a marker for "whatever time it started"
},
},
},
wantTaskGroups: 0,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := fake.NewFakeClientWithScheme(setupScheme())
r := &OperationReconciler{
Client: c,
recorder: record.NewFakeRecorder(1),
Log: log.Log,
}
if err := r.reconcileNormal(tt.args.operation, tt.args.taskGroups, r.Log); (err != nil) != tt.wantErr {
t.Errorf("reconcileNormal() error = %v, wantErr %v", err, tt.wantErr)
}
fixupWantOperation(tt.want.operation, tt.args.operation)
if !reflect.DeepEqual(tt.args.operation, tt.want.operation) {
t.Errorf("reconcileNormal() = %v, want %v", tt.args.operation, tt.want.operation)
}
taskGroups := &operatorv1.RuntimeTaskGroupList{}
if err := c.List(context.Background(), taskGroups); err != nil {
t.Fatalf("List() error = %v", err)
}
if len(taskGroups.Items) != tt.wantTaskGroups {
t.Errorf("reconcileNormal() = %v taskGroups, want %v taskGroups", len(taskGroups.Items), tt.wantTaskGroups)
}
})
}
}
func fixupWantOperation(want *operatorv1.Operation, got *operatorv1.Operation) {
// In case want.StartTime is a marker, replace it with the current CompletionTime
if want.CreationTimestamp.IsZero() {
want.CreationTimestamp = got.CreationTimestamp
}
// In case want.ErrorMessage is a marker, replace it with the current error
if want.Status.ErrorMessage != nil && *want.Status.ErrorMessage == "error" && got.Status.ErrorMessage != nil {
want.Status.ErrorMessage = got.Status.ErrorMessage
want.Status.ErrorReason = got.Status.ErrorReason
}
// In case want.StartTime is a marker, replace it with the current CompletionTime
if want.Status.StartTime != nil && want.Status.StartTime.IsZero() && got.Status.StartTime != nil {
want.Status.StartTime = got.Status.StartTime
}
// In case want.CompletionTime is a marker, replace it with the current CompletionTime
if want.Status.CompletionTime != nil && want.Status.CompletionTime.IsZero() && got.Status.CompletionTime != nil {
want.Status.CompletionTime = got.Status.CompletionTime
}
}