Skip to content

Commit

Permalink
pkg/karmadactl: unit test cordon
Browse files Browse the repository at this point in the history
In this commit, we unit test cordon on karmadactl client
making sure the cordon/uncordon working as expected on a given cluster.

Signed-off-by: Mohamed Awnallah <[email protected]>
  • Loading branch information
mohamedawnallah committed Nov 17, 2024
1 parent c7bc870 commit 3a9513f
Showing 1 changed file with 263 additions and 0 deletions.
263 changes: 263 additions & 0 deletions pkg/karmadactl/cordon/cordon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
Copyright 2024 The Karmada 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 cordon

import (
"bytes"
"context"
"fmt"
"os"
"reflect"
"strings"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cmdutil "k8s.io/kubectl/pkg/cmd/util"

clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
fakekarmadaclient "github.com/karmada-io/karmada/pkg/generated/clientset/versioned/fake"
"github.com/karmada-io/karmada/pkg/karmadactl/util"
)

type testFactory struct {
cmdutil.Factory
client karmadaclientset.Interface
}

func (t testFactory) KarmadaClientSet() (karmadaclientset.Interface, error) {
return t.client, nil
}

func (t testFactory) FactoryForMemberCluster(string) (cmdutil.Factory, error) {
panic("not implemented")
}

type fTaintCondition func(cluster *clusterv1alpha1.Cluster) error

var fTCordonCondition = func(cluster *clusterv1alpha1.Cluster) error {
if len(cluster.Spec.Taints) == 0 {
return fmt.Errorf("expected one noschedule taint for cluster %s, but got empty taints", cluster.GetName())
}
taintExpected := corev1.Taint{
Key: clusterv1alpha1.TaintClusterUnscheduler,
Effect: corev1.TaintEffectNoSchedule,
}
if !reflect.DeepEqual(cluster.Spec.Taints[0], taintExpected) {
return fmt.Errorf("expected taint %v, but got %v", taintExpected, cluster.Spec.Taints[0])
}
return nil
}

var fTUncordonCondition = func(cluster *clusterv1alpha1.Cluster) error {
if len(cluster.Spec.Taints) != 0 {
return fmt.Errorf("expected no taints for cluster %s, but got %d taints", cluster.GetName(), len(cluster.Spec.Taints))
}
return nil
}

func TestRunCordonOrUncordon(t *testing.T) {
clusterName := "test-cluster"
tests := []struct {
name string
desiredCordonStatus int
f util.Factory
opts *CommandCordonOption
prep func(f util.Factory, opts *CommandCordonOption, desiredCordonStatus int) error
verify func(util.Factory) error
wantErr bool
logMsg string
}{
{
name: "RunCordonOrUncordon_CordonUncordonedCluster_ClusterCordoned",
desiredCordonStatus: DesiredCordon,
f: testFactory{client: fakekarmadaclient.NewSimpleClientset()},
opts: &CommandCordonOption{ClusterName: clusterName},
prep: func(f util.Factory, _ *CommandCordonOption, _ int) error {
return prepClusterCreation(f, clusterName)
},
verify: func(f util.Factory) error {
return verifyClusterCordoned(f, clusterName, fTCordonCondition)
},
wantErr: false,
logMsg: fmt.Sprintf("%s cluster cordoned", clusterName),
},
{
name: "RunCordonOrUncordon_CordonCordonedCluster_ClusterAlreadyCordoned",
desiredCordonStatus: DesiredCordon,
f: testFactory{client: fakekarmadaclient.NewSimpleClientset()},
opts: &CommandCordonOption{ClusterName: clusterName},
prep: func(f util.Factory, opts *CommandCordonOption, desiredCordonStatus int) error {
if err := prepClusterCreation(f, clusterName); err != nil {
return err
}
if err := RunCordonOrUncordon(desiredCordonStatus, f, *opts); err != nil {
return fmt.Errorf("failed to cordon cluster %s, got error: %v", clusterName, err)
}
return nil
},
verify: func(util.Factory) error { return nil },
wantErr: false,
logMsg: fmt.Sprintf("%s cluster already cordoned", clusterName),
},
{
name: "RunCordonOrUncordon_UncordonCordonedCluster_ClusterUncordoned",
desiredCordonStatus: DesiredUnCordon,
f: testFactory{client: fakekarmadaclient.NewSimpleClientset()},
opts: &CommandCordonOption{ClusterName: clusterName},
prep: func(f util.Factory, opts *CommandCordonOption, _ int) error {
if err := prepClusterCreation(f, clusterName); err != nil {
return err
}
if err := RunCordonOrUncordon(DesiredCordon, f, *opts); err != nil {
return fmt.Errorf("failed to cordon cluster %s, got error: %v", clusterName, err)
}
return nil
},
verify: func(f util.Factory) error {
return verifyClusterCordoned(f, clusterName, fTUncordonCondition)
},
wantErr: false,
logMsg: fmt.Sprintf("%s cluster uncordoned", clusterName),
},
{
name: "RunCordonOrUncordon_UncordonUncordonedCluster_ClusterAlreadyUncordoned",
desiredCordonStatus: DesiredUnCordon,
f: testFactory{client: fakekarmadaclient.NewSimpleClientset()},
opts: &CommandCordonOption{ClusterName: clusterName},
prep: func(f util.Factory, _ *CommandCordonOption, _ int) error {
return prepClusterCreation(f, clusterName)
},
verify: func(util.Factory) error { return nil },
wantErr: false,
logMsg: fmt.Sprintf("%s cluster already uncordoned", clusterName),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.logMsg != "" {
r, w, oldStdout, err := prepLogMsgWatcher()
if err != nil {
t.Fatal(err)
}
defer func() {
if err := verifyMsg(r, w, oldStdout, test.logMsg); err != nil {
t.Error(err)
}
}()
}
if err := test.prep(test.f, test.opts, test.desiredCordonStatus); err != nil {
t.Fatalf("failed to prep test environment, got: %v", err)
}
err := RunCordonOrUncordon(test.desiredCordonStatus, test.f, *test.opts)
if err == nil && test.wantErr {
t.Fatal("expected an error, but got none")
}
if err != nil && !test.wantErr {
t.Errorf("unexpected error, got: %v", err)
}
if err := test.verify(test.f); err != nil {
t.Errorf("failed to verify the cordon/uncordon, got: %v", err)
}
})
}
}

// verifyClusterCordoned verifies if the cluster with the given name is cordoned
// by checking its taint condition using the provided factory and condition function.
// It returns an error if the cluster retrieval fails or the condition check fails.
func verifyClusterCordoned(f util.Factory, clusterName string, fTCondition fTaintCondition) error {
client, err := f.KarmadaClientSet()
if err != nil {
return err
}
cluster, err := client.ClusterV1alpha1().Clusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get cluster %s, got error: %v", clusterName, err)
}
return fTCondition(cluster)
}

// verifyMsg checks if the expected message is in the captured stdout output.
// Restores os.Stderr after verification and returns an error if the message is missing.
func verifyMsg(r, w *os.File, oldStdOut *os.File, msgExpected string) error {
gotMsg, err := closeAndReadStdOut(r, w)
if err != nil {
return err
}
if !strings.Contains(gotMsg, msgExpected) {
return fmt.Errorf("expected message %s to be in %s", msgExpected, gotMsg)
}
os.Stdout = oldStdOut
return nil
}

// closeAndReadStdOut closes the writer and reads from the reader to capture stdout output.
// Returns the output as a string and an error if reading fails.
func closeAndReadStdOut(r *os.File, w *os.File) (string, error) {
// Close the writer to finish the capture.
w.Close()

// Read the captured output from the pipe.
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
return "", fmt.Errorf("failed to read from pipe: %v", err)
}

output := buf.String()
return output, err
}

// prepLogMsgWatcher redirects os.Stdout to a pipe for capturing messages.
// Returns the pipe's reader and writer, the original os.Stdout, and an error.
func prepLogMsgWatcher() (*os.File, *os.File, *os.File, error) {
r, w, err := watchStdOut()
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to watch stdError, got: %v", err)
}
oldStdout := os.Stdout
os.Stdout = w
return r, w, oldStdout, err
}

// watchStdOut creates a pipe to capture os.Stdout. Returns the pipe's reader, writer, and an error.
func watchStdOut() (*os.File, *os.File, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, nil, fmt.Errorf("failed to create pipe: %v", err)
}
return r, w, err
}

// prepClusterCreation creates a new cluster with the given name using the provided factory.
// It returns an error if the cluster creation fails.
func prepClusterCreation(f util.Factory, clusterName string) error {
client, err := f.KarmadaClientSet()
if err != nil {
return err
}
testCluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: clusterName},
}
_, err = client.ClusterV1alpha1().Clusters().Create(context.TODO(), testCluster, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create cluster %s, got: %v", testCluster, err)
}
return nil
}

0 comments on commit 3a9513f

Please sign in to comment.