Skip to content

Commit

Permalink
add functional test for instascale machineset test case in accordance…
Browse files Browse the repository at this point in the history
… with using envtest control plane or using existing-cluster
  • Loading branch information
abhijeet-dhumal committed Jan 24, 2024
1 parent 2798824 commit 9429154
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 89 deletions.
61 changes: 61 additions & 0 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set Go
uses: actions/setup-go@v3
with:
go-version: v1.19

- name: init directories
run: mkdir -p "$(pwd)/bin"

- name: Download kubebuilder
run: |
version=1.0.8
arch=amd64
curl -L -O "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz"
tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz
mv kubebuilder_${version}_linux_${arch} kubebuilder
echo "kubebuilder_${version}_linux_${arch} renamed to kubebuilder"
sudo mv kubebuilder /usr/local/
echo "kubebuilder moved to /usr/local/ path"
DIRECTORY=/usr/local/kubebuilder
if [ -d "$DIRECTORY" ]; then
echo "$DIRECTORY does exist."
else
echo "$DIRECTORY does not exist."
fi
echo "PATH=$PATH:$DIRECTORY/bin" >> $GITHUB_ENV
export PATH=$PATH:$DIRECTORY/bin
- name: Set up Kubebuilder and setup-envtest
run: |
make envtest
echo "Kubebuilder-envtest : $(./bin/setup-envtest use 1.23 -p path)"
echo "KUBEBUILDER_ASSETS=$(./bin/setup-envtest use 1.23 -p path)" >> $GITHUB_ENV
echo "USE_EXISTING_CLUSTER=false" >> $GITHUB_ENV
export KUBEBUILDER_ASSETS="$(./bin/setup-envtest use 1.23 -p path)"
export USE_EXISTING_CLUSTER=false
- name: Test
run: |
pwd
echo "KUBEBUILDER_ASSETS : $KUBEBUILDER_ASSETS"
echo "USE_EXISTING_CLUSTER : $USE_EXISTING_CLUSTER"
go test -v ./functional-tests
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
run: go build -o bin/manager main.go

- name: Test
run: go test -v ./...
run: go test -v ./controllers
250 changes: 250 additions & 0 deletions functional-tests/appwrapper_reconciler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package functional_tests

import (
"context"
"go/build"
"os"
"path/filepath"
"testing"
"time"

. "github.com/onsi/gomega"
gstruct "github.com/onsi/gomega/gstruct"
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
. "github.com/project-codeflare/codeflare-common/support"
"github.com/project-codeflare/instascale/controllers"
"github.com/project-codeflare/instascale/pkg/config"
mcadv1beta1 "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/apis/controller/v1beta1"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
log "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
cfg *rest.Config
k8sClient client.Client // You'll be using this client in your tests.
testEnv *envtest.Environment
err error
)

func logger() {
// Get log file path from environment variable or use a default path
logFilePath := os.Getenv("LOG_FILE_PATH")
if logFilePath == "" {
logFilePath = "./functional-test-logfile.log"
}
// Create a log file
logFile, err := os.Create(logFilePath)
if err != nil {
log.Log.Error(err, "Error creating log file: %v", err)
}
// Configure zap logger to write to the log file
logger := zap.New(zap.WriteTo(logFile), zap.UseDevMode(true))

// Set the logger for controller-runtime
ctrl.SetLogger(logger)

// This line prevents controller-runtime from complaining about log.SetLogger never being called
log.SetLogger(logger)
}

func startEnvTest(t *testing.T) {
// to redirect all functional test related logs to separate logfile ~ default (in functional-tests directory), can be changed using environment variable LOG_FILE_PATH=/path_to_logfile
logger()

//specify testEnv configuration
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join(build.Default.GOPATH, "pkg", "mod", "github.com", "project-codeflare", "[email protected]", "config", "crd", "bases"),
filepath.Join(build.Default.GOPATH, "pkg", "mod", "github.com", "openshift", "[email protected]", "machine", "v1beta1"),
},
ErrorIfCRDPathMissing: true,
}
test := WithConfig(t, testEnv.Config)
cfg, err = testEnv.Start()
test.Expect(err).NotTo(HaveOccurred())

t.Cleanup(func() {
teardownTestEnv(t, testEnv)
})

establishClient(t, testEnv) // setup client required for test to interact with kubernetes machine API
}

func establishClient(t *testing.T, testEnv *envtest.Environment) {
// Create a test instance with the provided configuration.
test := WithConfig(t, cfg)

// Add custom resource schemes to the global scheme.
err = mcadv1beta1.AddToScheme(scheme.Scheme)
test.Expect(err).NotTo(HaveOccurred())

err = machinev1beta1.AddToScheme(scheme.Scheme)
test.Expect(err).NotTo(HaveOccurred())

// Create a Kubernetes client using the provided configuration.
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
test.Expect(err).NotTo(HaveOccurred())
test.Expect(k8sClient).NotTo(BeNil())

// Create a controller manager for managing controllers.
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
test.Expect(err).ToNot(HaveOccurred())

// Create an instance of the AppWrapperReconciler with configuration
instaScaleController := &controllers.AppWrapperReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
Config: config.InstaScaleConfiguration{
MachineSetsStrategy: "reuse",
MaxScaleoutAllowed: 5,
},
}
// Set up the AppWrapperReconciler with the manager.
err = instaScaleController.SetupWithManager(context.Background(), k8sManager)
test.Expect(err).ToNot(HaveOccurred())

// Start the controller manager in a goroutine.
go func() {
err = k8sManager.Start(ctrl.SetupSignalHandler())
test.Expect(err).ToNot(HaveOccurred())
}()

}

func teardownTestEnv(t *testing.T, testEnv *envtest.Environment) {
if err := testEnv.Stop(); err != nil {
t.Log("Error stopping test Environment\n", err)
}
}

func instascaleAppwrapper(namespace string) *mcadv1beta1.AppWrapper {
aw := &mcadv1beta1.AppWrapper{
ObjectMeta: metav1.ObjectMeta{
Name: "test-instascale",
Namespace: namespace,
Labels: map[string]string{
"orderedinstance": "test.instance1",
},
Finalizers: []string{"instascale.codeflare.dev/finalizer"},
},
Spec: mcadv1beta1.AppWrapperSpec{
AggrResources: mcadv1beta1.AppWrapperResourceList{
GenericItems: []mcadv1beta1.AppWrapperGenericResource{
{
DesiredAvailable: 1,
CustomPodResources: []mcadv1beta1.CustomPodResourceTemplate{
{
Replicas: 1,
Requests: apiv1.ResourceList{
apiv1.ResourceCPU: resource.MustParse("250m"),
apiv1.ResourceMemory: resource.MustParse("512Mi"),
},
Limits: apiv1.ResourceList{
apiv1.ResourceCPU: resource.MustParse("1"),
apiv1.ResourceMemory: resource.MustParse("1G"),
},
},
},
},
},
},
},
}
return aw
}

func MachineSet(test Test, namespace string, machineSetName string) func(g Gomega) *machinev1beta1.MachineSet {
return func(g Gomega) *machinev1beta1.MachineSet {
machineset, err := test.Client().Machine().MachineV1beta1().MachineSets(namespace).Get(test.Ctx(), machineSetName, metav1.GetOptions{})
test.Expect(err).ToNot(HaveOccurred())
return machineset
}
}

func MachineSetReplicas(machineSet *machinev1beta1.MachineSet) *int32 {
return machineSet.Spec.Replicas
}

func TestReconciler(t *testing.T) {
// initiate setting up the EnvTest environment
startEnvTest(t)

test := WithConfig(t, cfg)

// initialize a variable with test client
client := test.Client()

//read machineset yaml from file `test_instascale_machineset.yml`
b, err := os.ReadFile("test_instascale_machineset.yml")
test.Expect(err).ToNot(HaveOccurred())

//deserialize kubernetes object
decode := scheme.Codecs.UniversalDeserializer().Decode
ms, _, err := decode(b, nil, nil) //decode machineset content of YAML file into kubernetes object
test.Expect(err).ToNot(HaveOccurred())
msa := ms.(*machinev1beta1.MachineSet) //asserts that decoded object is of type `*machinev1beta1.MachineSet`

//create machineset in default namespace
ms, err = client.Machine().MachineV1beta1().MachineSets("default").Create(test.Ctx(), msa, metav1.CreateOptions{})
test.Expect(err).ToNot(HaveOccurred())

//assert that the replicas in Machineset specification is 0, before appwrapper creation
test.Eventually(MachineSet(test, "default", "test-instascale")).Should(WithTransform(MachineSetReplicas, gstruct.PointTo(Equal(int32(0)))))

//create new test namespace
namespace := test.NewTestNamespace()

// initializes an appwrapper in the created namespace
aw := instascaleAppwrapper(namespace.Name)

// create appwrapper resource using mcadClient
aw, err = client.MCAD().WorkloadV1beta1().AppWrappers(namespace.Name).Create(test.Ctx(), aw, metav1.CreateOptions{})
test.Expect(err).ToNot(HaveOccurred())
time.Sleep(10 * time.Second)

//update appwrapper status to avoid appwrapper dispatch in case of Insufficient resources
aw.Status = mcadv1beta1.AppWrapperStatus{
Pending: 1,
State: mcadv1beta1.AppWrapperStateEnqueued,
Conditions: []mcadv1beta1.AppWrapperCondition{
{
Type: mcadv1beta1.AppWrapperCondBackoff,
Status: apiv1.ConditionTrue,
Reason: "AppWrapperNotRunnable",
Message: "Insufficient resources to dispatch AppWrapper.",
},
},
}
_, err = client.MCAD().WorkloadV1beta1().AppWrappers(namespace.Name).UpdateStatus(test.Ctx(), aw, metav1.UpdateOptions{})
test.Expect(err).ToNot(HaveOccurred())
time.Sleep(10 * time.Second)

// assert for machine replicas belonging to the machine set after appwrapper creation- there should be 1
test.Eventually(MachineSet(test, "default", "test-instascale")).Should(WithTransform(MachineSetReplicas, gstruct.PointTo(Equal(int32(1)))))

// delete appwrapper
err = client.MCAD().WorkloadV1beta1().AppWrappers(namespace.Name).Delete(test.Ctx(), aw.Name, metav1.DeleteOptions{})
test.Expect(err).ToNot(HaveOccurred())
time.Sleep(10 * time.Second)

// assert for machine belonging to the machine set after appwrapper deletion- there should be none
test.Eventually(MachineSet(test, "default", "test-instascale")).Should(WithTransform(MachineSetReplicas, gstruct.PointTo(Equal(int32(0)))))

// delete machineset
err = client.Machine().MachineV1beta1().MachineSets("default").Delete(test.Ctx(), "test-instascale", metav1.DeleteOptions{})
test.Expect(err).ToNot(HaveOccurred())

// delete test namespace
err = client.Core().CoreV1().Namespaces().Delete(context.TODO(), namespace.Name, metav1.DeleteOptions{})
test.Expect(err).ToNot(HaveOccurred())
}
63 changes: 63 additions & 0 deletions functional-tests/test_instascale_machineset.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: machine.openshift.io/v1beta1
kind: MachineSet
metadata:
name: test-instascale
labels:
machine.openshift.io/cluster-api-cluster: fionawaters-v2pm4
spec:
replicas: 0
selector:
matchLabels:
machine.openshift.io/cluster-api-cluster: fionawaters-v2pm4
machine.openshift.io/cluster-api-machineset: test-instascale
template:
metadata:
labels:
machine.openshift.io/cluster-api-cluster: fionawaters-v2pm4
machine.openshift.io/cluster-api-machine-role: worker
machine.openshift.io/cluster-api-machine-type: worker
machine.openshift.io/cluster-api-machineset: test-instascale
spec:
lifecycleHooks: {}
metadata:
labels:
node-role.kubernetes.io/<role>: ''
providerSpec:
value:
userDataSecret:
name: worker-user-data
placement:
availabilityZone: us-east-1a
region: us-east-1
credentialsSecret:
name: aws-cloud-credentials
instanceType: test.instance1
metadata:
creationTimestamp: null
blockDevices:
- ebs:
iops: 0
kmsKey: {}
volumeSize: 120
volumeType: gp2
securityGroups:
- filters:
- name: 'tag:Name'
values:
- fionawaters-v2pm4-worker-sg
kind: AWSMachineProviderConfig
metadataServiceOptions: {}
tags:
- name: kubernetes.io/cluster/fionawaters-v2pm4
value: owned
deviceIndex: 0
ami:
id: ami-0624891c612b5eaa0
subnet:
filters:
- name: 'tag:Name'
values:
- fionawaters-v2pm4-private-us-east-1a
apiVersion: awsproviderconfig.openshift.io/v1beta1
iamInstanceProfile:
id: fionawaters-v2pm4-worker-profile
Loading

0 comments on commit 9429154

Please sign in to comment.