Skip to content

Commit

Permalink
Merge pull request #73 from harshanarayana/harshanarayana/issue60
Browse files Browse the repository at this point in the history
GIT-60: enable helper function for conditional waits
  • Loading branch information
k8s-ci-robot authored Nov 19, 2021
2 parents a76189f + 335a796 commit 9169c87
Show file tree
Hide file tree
Showing 6 changed files with 605 additions and 66 deletions.
80 changes: 80 additions & 0 deletions klient/internal/testutil/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright 2021 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 testutil

import (
"log"
"time"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/e2e-framework/klient/conf"
"sigs.k8s.io/e2e-framework/support/kind"
)

type TestCluster struct {
KindCluster *kind.Cluster
Kubeconfig string
RESTConfig *rest.Config
Clientset kubernetes.Interface
}

func SetupTestCluster(path string) *TestCluster {
if path == "" {
path = conf.ResolveKubeConfigFile()
}

tc := &TestCluster{}
var err error
kc, err := setupKind()
if err != nil {
log.Fatalln("error while setting up the kind cluster", err)
}
tc.KindCluster = kc

cfg, err := conf.New(path)
if err != nil {
log.Fatalln("error while client connection trying to resolve kubeconfig", err)
}
tc.RESTConfig = cfg
clientSet, err := kubernetes.NewForConfig(cfg)
if err != nil {
log.Fatalln("failed to create new Client set for kind cluster", err)
}
tc.Clientset = clientSet
return tc
}

func (t *TestCluster) DestroyTestCluster() {
err := t.KindCluster.Destroy()
if err != nil {
log.Println("error while deleting the cluster", err)
return
}
}

func setupKind() (kc *kind.Cluster, err error) {
kc = kind.NewCluster("e2e-test-cluster")
if _, err = kc.Create(); err != nil {
return
}

waitPeriod := 10 * time.Second
log.Println("Waiting for kind pods to be initlaized...")
time.Sleep(waitPeriod)
return
}
74 changes: 8 additions & 66 deletions klient/k8s/resources/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,104 +18,46 @@ package resources

import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"testing"
"time"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/homedir"
"sigs.k8s.io/e2e-framework/klient/conf"
"sigs.k8s.io/e2e-framework/support/kind"

"sigs.k8s.io/e2e-framework/klient/internal/testutil"
)

var (
kubeconfig string
tc *testutil.TestCluster
dep *appsv1.Deployment
clientset *kubernetes.Clientset
clientset kubernetes.Interface
count uint64
replicaCount int32 = 2
ctx = context.TODO()
cfg *rest.Config
namespace *corev1.Namespace
kc *kind.Cluster
)

func TestMain(m *testing.M) {
setup()
tc = testutil.SetupTestCluster("")
clientset = tc.Clientset
cfg = tc.RESTConfig
initializeResObjects()
code := m.Run()
teardown()
os.Exit(code)
}

func setup() {
home := homedir.HomeDir()
path := filepath.Join(home, ".kube", "config")

// set up kind cluster
err := setupKindCluster()
if err != nil {
log.Println("error while setting up kind cluster", err)
return
}

flag.StringVar(&kubeconfig, "kubeconfig", "", "Paths to a kubeconfig. Only required if out-of-cluster.")

// set --kubeconfig flag
err = flag.Set("kubeconfig", path)
if err != nil {
log.Println("unexpected error while setting flag value", err)
return
}

flag.Parse()

cfg, err = conf.New(conf.ResolveKubeConfigFile())
if err != nil {
log.Println("error while client connection", err)
return
}

clientset, err = kubernetes.NewForConfig(cfg)
if err != nil {
log.Println("error while client set connection", err)
return
}
}

// setupKindCluster
func setupKindCluster() error {
kc = kind.NewCluster("e2e-test-cluster")
if _, err := kc.Create(); err != nil {
return err
}

// stall to wait for kind pods initialization
waitTime := time.Second * 10
log.Println("waiting for kind pods to initialize...", waitTime)
time.Sleep(waitTime)

return nil
}

func teardown() {
deleteDeployment(ctx, dep, namespace.Name)
deleteNamespace(ctx, namespace)

// delete kind cluster
err := kc.Destroy()
if err != nil {
log.Println("error while deleting the cluster", err)
return
}
tc.DestroyTestCluster()
}

func deleteDeployment(ctx context.Context, dep *appsv1.Deployment, ns string) {
Expand Down
141 changes: 141 additions & 0 deletions klient/wait/conditions/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Copyright 2021 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 conditions

import (
"context"

batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apimachinerywait "k8s.io/apimachinery/pkg/util/wait"

"sigs.k8s.io/e2e-framework/klient/k8s"
"sigs.k8s.io/e2e-framework/klient/k8s/resources"
)

type Condition struct {
resources *resources.Resources
}

// New is used to create a new Condition that can be used to perform a series of pre-defined wait checks
// against a resource in question
func New(r *resources.Resources) *Condition {
return &Condition{resources: r}
}

// ResourceScaled is a helper function used to check if the resource under question has a pre-defined number of
// replicas. This can be leveraged for checking cases such as scaling up and down a deployment or STS and any
// other scalable resources.
func (c *Condition) ResourceScaled(obj k8s.Object, scaleFetcher func(object k8s.Object) int32, replica int32) apimachinerywait.ConditionFunc {
return func() (done bool, err error) {
if err := c.resources.Get(context.TODO(), obj.GetName(), obj.GetNamespace(), obj); err != nil {
return false, nil
}
return scaleFetcher(obj) == replica, nil
}
}

// ResourceDeleted is a helper function used to check if a resource under question has been deleted. This will enable
// testing cases where the resource have a finalizer and the DELETE operation of such resource have been triggered and
// you want to wait until the resource has been deleted.
//
// This method can be leveraged against any Kubernetes resource to check the deletion workflow and it does so by
// checking the resource and waiting until it obtains a v1.StatusReasonNotFound error from the API
func (c *Condition) ResourceDeleted(obj k8s.Object) apimachinerywait.ConditionFunc {
return func() (done bool, err error) {
if err := c.resources.Get(context.Background(), obj.GetName(), obj.GetNamespace(), obj); err != nil {
if errors.IsNotFound(err) {
return true, nil
}
return false, err
}
return false, nil
}
}

// JobConditionMatch is a helper function that can be used to check the Job Completion or runtime status against a
// specific condition. This function accepts both conditionType and conditionState as argument and hence you can use this
// to match both positive or negative cases with suitable values passed to the arguments.
func (c *Condition) JobConditionMatch(job k8s.Object, conditionType batchv1.JobConditionType, conditionState v1.ConditionStatus) apimachinerywait.ConditionFunc {
return func() (done bool, err error) {
if err := c.resources.Get(context.TODO(), job.GetName(), job.GetNamespace(), job); err != nil {
return false, err
}
for _, cond := range job.(*batchv1.Job).Status.Conditions {
if cond.Type == conditionType && cond.Status == conditionState {
done = true
}
}
return
}
}

// PodConditionMatch is a helper function that can be used to check a specific condition match for the Pod in question.
// This is extended into a few simplified match helpers such as PodReady and ContainersReady as well.
func (c *Condition) PodConditionMatch(pod k8s.Object, conditionType v1.PodConditionType, conditionState v1.ConditionStatus) apimachinerywait.ConditionFunc {
return func() (done bool, err error) {
if err := c.resources.Get(context.TODO(), pod.GetName(), pod.GetNamespace(), pod); err != nil {
return false, err
}
for _, cond := range pod.(*v1.Pod).Status.Conditions {
if cond.Type == conditionType && cond.Status == conditionState {
done = true
}
}
return
}
}

// PodPhaseMatch is a helper function that is used to check and see if the Pod Has reached a specific Phase of the
// runtime. This can be combined with PodConditionMatch to check if a specific condition and phase has been met.
// This will enable validation such as checking against CLB of a POD.
func (c *Condition) PodPhaseMatch(pod k8s.Object, phase v1.PodPhase) apimachinerywait.ConditionFunc {
return func() (done bool, err error) {
if err := c.resources.Get(context.Background(), pod.GetName(), pod.GetNamespace(), pod); err != nil {
return false, err
}
return pod.(*v1.Pod).Status.Phase == phase, nil
}
}

// PodReady is a helper function used to check if the pod condition v1.PodReady has reached v1.ConditionTrue state
func (c *Condition) PodReady(pod k8s.Object) apimachinerywait.ConditionFunc {
return c.PodConditionMatch(pod, v1.PodReady, v1.ConditionTrue)
}

// ContainersReady is a helper function used to check if the pod condition v1.ContainersReady has reached v1.ConditionTrue
func (c *Condition) ContainersReady(pod k8s.Object) apimachinerywait.ConditionFunc {
return c.PodConditionMatch(pod, v1.ContainersReady, v1.ConditionTrue)
}

// PodRunning is a helper function used to check if the pod.Status.Phase attribute of the Pod has reached v1.PodRunning
func (c *Condition) PodRunning(pod k8s.Object) apimachinerywait.ConditionFunc {
return c.PodPhaseMatch(pod, v1.PodRunning)
}

// JobCompleted is a helper function used to check if the Job has been completed successfully by checking if the
// batchv1.JobCompleted has reached the v1.ConditionTrue state
func (c *Condition) JobCompleted(job k8s.Object) apimachinerywait.ConditionFunc {
return c.JobConditionMatch(job, batchv1.JobComplete, v1.ConditionTrue)
}

// JobFailed is a helper function used to check if the Job has failed by checking if the batchv1.JobFailed has reached
// v1.ConditionTrue state
func (c *Condition) JobFailed(job k8s.Object) apimachinerywait.ConditionFunc {
return c.JobConditionMatch(job, batchv1.JobFailed, v1.ConditionTrue)
}
Loading

0 comments on commit 9169c87

Please sign in to comment.