diff --git a/.travis.yml b/.travis.yml index b36ea0a..6958814 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,13 @@ sudo: required dist: xenial env: + global: + - CHANGE_MINIKUBE_NONE_USER=true + - MINIKUBE_WANTUPDATENOTIFICATION=false + - MINIKUBE_WANTREPORTERRORPROMPT=false + - MINIKUBE_HOME=$HOME + - CHANGE_MINIKUBE_NONE_USER=true + - KUBECONFIG=$HOME/.kube/config services: - docker language: go @@ -10,12 +17,31 @@ go: addons: apt: update: true - +before_install: + - sudo apt-get update -qq + - sudo apt-get install -y zfsutils-linux + - truncate -s 100G /tmp/disk.img + - sudo zpool create -f zfspv-pool /tmp/disk.img install: - make bootstrap - make format + - curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.16.0/bin/linux/amd64/kubectl + && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ + - curl -Lo minikube https://storage.googleapis.com/minikube/releases/v1.4.0/minikube-linux-amd64 + && chmod +x minikube && sudo mv minikube /usr/local/bin/ + - mkdir -p $HOME/.kube $HOME/.minikube + - touch $KUBECONFIG + - sudo minikube start --vm-driver=none --kubernetes-version=v1.16.0 + - sudo chown -R $USER $HOME/.minikube + - sudo chown -R $USER $HOME/.kube + - JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; + until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do + sleep 1; done + - kubectl cluster-info +before_script: + - "./buildscripts/travis-build.sh" script: - - ./buildscripts/travis-build.sh + - make ci after_success: - make deploy-images - bash <(curl -s https://codecov.io/bash) diff --git a/Makefile b/Makefile index 93abe65..14d203a 100644 --- a/Makefile +++ b/Makefile @@ -36,10 +36,11 @@ CSI_DRIVER=zfs-driver BUILD_DATE = $(shell date +'%Y%m%d%H%M%S') .PHONY: all -all: test zfs-driver-image +all: zfs-driver-image .PHONY: clean clean: + @echo "--> Cleaning Directory" ; go clean -testcache rm -rf bin rm -rf ${GOPATH}/bin/${CSI_DRIVER} @@ -132,9 +133,9 @@ informer: --go-header-file ./buildscripts/custom-boilerplate.go.txt .PHONY: zfs-driver -zfs-driver: +zfs-driver: format @echo "--------------------------------" - @echo "+ Building ${CSI_DRIVER} " + @echo "--> Building ${CSI_DRIVER} " @echo "--------------------------------" @PNAME=${CSI_DRIVER} CTLNAME=${CSI_DRIVER} sh -c "'$(PWD)/buildscripts/build.sh'" @@ -147,6 +148,10 @@ zfs-driver-image: zfs-driver cd buildscripts/${CSI_DRIVER} && sudo docker build -t openebs/${CSI_DRIVER}:${IMAGE_TAG} --build-arg BUILD_DATE=${BUILD_DATE} . && sudo docker tag openebs/${CSI_DRIVER}:${IMAGE_TAG} quay.io/openebs/${CSI_DRIVER}:${IMAGE_TAG} @rm buildscripts/${CSI_DRIVER}/${CSI_DRIVER} +.PHONY: ci +ci: + @echo "--> Running ci test"; + $(PWD)/ci/ci-test.sh # Push images deploy-images: @DIMAGE="openebs/zfs-driver" ./buildscripts/push diff --git a/ci/ci-test.sh b/ci/ci-test.sh new file mode 100755 index 0000000..ab9875f --- /dev/null +++ b/ci/ci-test.sh @@ -0,0 +1,87 @@ +# Copyright 2019 The OpenEBS 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. + +#!/usr/bin/env bash + +set -e + +export OPENEBS_NAMESPACE="openebs" +export NodeID=$HOSTNAME + +ZFS_OPERATOR=deploy/zfs-operator.yaml +TEST_DIR="tests" + +# Prepare env for runnging BDD tests +# Minikube is already running +kubectl apply -f $ZFS_OPERATOR + +dumpAgentLogs() { + NR=$1 + AgentPOD=$(kubectl get pods -l app=openebs-zfs-node -o jsonpath='{.items[0].metadata.name}' -n kube-system) + kubectl describe po $AgentPOD -n kube-system + printf "\n\n" + kubectl logs --tail=${NR} $AgentPOD -n kube-system -c openebs-zfs-plugin + printf "\n\n" +} + +dumpControllerLogs() { + NR=$1 + ControllerPOD=$(kubectl get pods -l app=openebs-zfs-controller -o jsonpath='{.items[0].metadata.name}' -n kube-system) + kubectl describe po $ControllerPOD -n kube-system + printf "\n\n" + kubectl logs --tail=${NR} $ControllerPOD -n kube-system -c openebs-zfs-plugin + printf "\n\n" +} + +# wait for zfs driver to be UP +sleep 20 + +cd $TEST_DIR + +kubectl get po -n kube-system + +set +e + +echo "running ginkgo test case" + +ginkgo -v + +if [ $? -ne 0 ]; then + +sudo zpool status + +sudo zfs list -t all + +echo "******************** ZFS Controller logs***************************** " +dumpControllerLogs 1000 + +echo "********************* ZFS Agent logs *********************************" +dumpAgentLogs 1000 + +echo "get all the pods" +kubectl get pods -owide --all-namespaces + +echo "get pvc and pv details" +kubectl get pvc,pv --all-namespaces + +echo "get sc details" +kubectl get sc --all-namespaces -oyaml + +echo "get zfs volume details" +kubectl get zfsvolumes.openebs.io -n openebs -oyaml + +exit 1 +fi + +echo "\n\n######### All test cases passed #########\n\n" diff --git a/pkg/common/kubernetes/client/client.go b/pkg/common/kubernetes/client/client.go index 9366e8b..c401564 100644 --- a/pkg/common/kubernetes/client/client.go +++ b/pkg/common/kubernetes/client/client.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "strings" + "sync" env "github.com/openebs/zfs-localpv/pkg/common/env" "github.com/pkg/errors" @@ -140,6 +141,21 @@ func New(opts ...OptionFunc) *Client { return c } +var ( + instance *Client + once sync.Once +) + +// Instance returns a singleton instance of +// this client +func Instance(opts ...OptionFunc) *Client { + once.Do(func() { + instance = New(opts...) + }) + + return instance +} + func withDefaults(c *Client) { if c.getInClusterConfig == nil { c.getInClusterConfig = rest.InClusterConfig diff --git a/tests/container/container.go b/tests/container/container.go new file mode 100644 index 0000000..f707287 --- /dev/null +++ b/tests/container/container.go @@ -0,0 +1,420 @@ +/* +Copyright 2019 The OpenEBS 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 container + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + corev1 "k8s.io/api/core/v1" +) + +type container struct { + corev1.Container // kubernetes container type +} + +// OptionFunc is a typed function that abstracts anykind of operation +// against the provided container instance +// +// This is the basic building block to create functional operations +// against the container instance +type OptionFunc func(*container) + +// Predicate abstracts conditional logic w.r.t the container instance +// +// NOTE: +// Predicate is a functional approach versus traditional approach to mix +// conditions such as *if-else* within blocks of business logic +// +// NOTE: +// Predicate approach enables clear separation of conditionals from +// imperatives i.e. actions that form the business logic +type Predicate func(*container) (nameOrMsg string, ok bool) + +// predicateFailedError returns the provided predicate as an error +func predicateFailedError(message string) error { + return errors.Errorf("predicatefailed: %s", message) +} + +var ( + errorvalidationFailed = errors.New("container validation failed") +) + +// asContainer transforms this container instance into corresponding kubernetes +// container type +func (c *container) asContainer() corev1.Container { + return corev1.Container{ + Name: c.Name, + Image: c.Image, + Command: c.Command, + Args: c.Args, + WorkingDir: c.WorkingDir, + Ports: c.Ports, + EnvFrom: c.EnvFrom, + Env: c.Env, + Resources: c.Resources, + VolumeMounts: c.VolumeMounts, + VolumeDevices: c.VolumeDevices, + LivenessProbe: c.LivenessProbe, + ReadinessProbe: c.ReadinessProbe, + Lifecycle: c.Lifecycle, + TerminationMessagePath: c.TerminationMessagePath, + TerminationMessagePolicy: c.TerminationMessagePolicy, + ImagePullPolicy: c.ImagePullPolicy, + SecurityContext: c.SecurityContext, + Stdin: c.Stdin, + StdinOnce: c.StdinOnce, + TTY: c.TTY, + } +} + +// New returns a new kubernetes container +func New(opts ...OptionFunc) corev1.Container { + c := &container{} + for _, o := range opts { + o(c) + } + return c.asContainer() +} + +// Builder provides utilities required to build a kubernetes container type +type Builder struct { + con *container // container instance + checks []Predicate // validations to be done while building the container instance + errors []error // errors found while building the container instance +} + +// NewBuilder returns a new instance of builder +func NewBuilder() *Builder { + return &Builder{ + con: &container{}, + } +} + +// validate will run checks against container instance +func (b *Builder) validate() error { + for _, c := range b.checks { + if m, ok := c(b.con); !ok { + b.errors = append(b.errors, predicateFailedError(m)) + } + } + if len(b.errors) == 0 { + return nil + } + return errorvalidationFailed +} + +// Build returns the final kubernetes container +func (b *Builder) Build() (corev1.Container, error) { + err := b.validate() + if err != nil { + return corev1.Container{}, err + } + return b.con.asContainer(), nil +} + +// AddCheck adds the predicate as a condition to be validated against the +// container instance +func (b *Builder) AddCheck(p Predicate) *Builder { + b.checks = append(b.checks, p) + return b +} + +// AddChecks adds the provided predicates as conditions to be validated against +// the container instance +func (b *Builder) AddChecks(p []Predicate) *Builder { + for _, check := range p { + b.AddCheck(check) + } + return b +} + +// WithName sets the name of the container +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing name"), + ) + return b + } + WithName(name)(b.con) + return b +} + +// WithName sets the name of the container +func WithName(name string) OptionFunc { + return func(c *container) { + c.Name = name + } +} + +// WithImage sets the image of the container +func (b *Builder) WithImage(img string) *Builder { + if len(img) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing image"), + ) + return b + } + WithImage(img)(b.con) + return b +} + +// WithImage sets the image of the container +func WithImage(img string) OptionFunc { + return func(c *container) { + c.Image = img + } +} + +// WithCommandNew sets the command of the container +func (b *Builder) WithCommandNew(cmd []string) *Builder { + if cmd == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil command"), + ) + return b + } + + if len(cmd) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing command"), + ) + return b + } + + newcmd := []string{} + newcmd = append(newcmd, cmd...) + + b.con.Command = newcmd + return b +} + +// WithArgumentsNew sets the command arguments of the container +func (b *Builder) WithArgumentsNew(args []string) *Builder { + if args == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil arguments"), + ) + return b + } + + if len(args) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing arguments"), + ) + return b + } + + newargs := []string{} + newargs = append(newargs, args...) + + b.con.Args = newargs + return b +} + +// WithVolumeMountsNew sets the command arguments of the container +func (b *Builder) WithVolumeMountsNew(volumeMounts []corev1.VolumeMount) *Builder { + if volumeMounts == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil volumemounts"), + ) + return b + } + + if len(volumeMounts) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing volumemounts"), + ) + return b + } + newvolumeMounts := []corev1.VolumeMount{} + newvolumeMounts = append(newvolumeMounts, volumeMounts...) + b.con.VolumeMounts = newvolumeMounts + return b +} + +// WithImagePullPolicy sets the image pull policy of the container +func (b *Builder) WithImagePullPolicy(policy corev1.PullPolicy) *Builder { + if len(policy) == 0 { + b.errors = append( + b.errors, + errors.New( + "failed to build container object: missing imagepullpolicy", + ), + ) + return b + } + + b.con.ImagePullPolicy = policy + return b +} + +// WithPrivilegedSecurityContext sets securitycontext of the container +func (b *Builder) WithPrivilegedSecurityContext(privileged *bool) *Builder { + if privileged == nil { + b.errors = append( + b.errors, + errors.New( + "failed to build container object: missing securitycontext", + ), + ) + return b + } + + newprivileged := *privileged + newsecuritycontext := &corev1.SecurityContext{ + Privileged: &newprivileged, + } + + b.con.SecurityContext = newsecuritycontext + return b +} + +// WithResources sets resources of the container +func (b *Builder) WithResources( + resources *corev1.ResourceRequirements, +) *Builder { + if resources == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing resources"), + ) + return b + } + + newresources := *resources + b.con.Resources = newresources + return b +} + +// WithResourcesByValue sets resources of the container +func (b *Builder) WithResourcesByValue(resources corev1.ResourceRequirements) *Builder { + b.con.Resources = resources + return b +} + +// WithPortsNew sets ports of the container +func (b *Builder) WithPortsNew(ports []corev1.ContainerPort) *Builder { + if ports == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil ports"), + ) + return b + } + + if len(ports) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing ports"), + ) + return b + } + + newports := []corev1.ContainerPort{} + newports = append(newports, ports...) + + b.con.Ports = newports + return b +} + +// WithEnvsNew sets the envs of the container +func (b *Builder) WithEnvsNew(envs []corev1.EnvVar) *Builder { + if envs == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil envs"), + ) + return b + } + + if len(envs) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing envs"), + ) + return b + } + + newenvs := []corev1.EnvVar{} + newenvs = append(newenvs, envs...) + + b.con.Env = newenvs + return b +} + +// WithEnvs sets the envs of the container +func (b *Builder) WithEnvs(envs []corev1.EnvVar) *Builder { + if envs == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil envs"), + ) + return b + } + + if len(envs) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing envs"), + ) + return b + } + + if b.con.Env == nil { + b.WithEnvsNew(envs) + return b + } + + b.con.Env = append(b.con.Env, envs...) + return b +} + +// WithLivenessProbe sets the liveness probe of the container +func (b *Builder) WithLivenessProbe(liveness *corev1.Probe) *Builder { + if liveness == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil liveness probe"), + ) + return b + } + + b.con.LivenessProbe = liveness + return b +} + +// WithLifeCycle sets the life cycle of the container +func (b *Builder) WithLifeCycle(lc *corev1.Lifecycle) *Builder { + if lc == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil lifecycle"), + ) + return b + } + + b.con.Lifecycle = lc + return b +} diff --git a/tests/deploy/deployment.go b/tests/deploy/deployment.go new file mode 100644 index 0000000..7e81482 --- /dev/null +++ b/tests/deploy/deployment.go @@ -0,0 +1,556 @@ +/* +Copyright 2019 The OpenEBS 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 deploy + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + templatespec "github.com/openebs/zfs-localpv/tests/pts" + "github.com/openebs/zfs-localpv/tests/stringer" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Predicate abstracts conditional logic w.r.t the deployment instance +// +// NOTE: +// predicate is a functional approach versus traditional approach to mix +// conditions such as *if-else* within blocks of business logic +// +// NOTE: +// predicate approach enables clear separation of conditionals from +// imperatives i.e. actions that form the business logic +type Predicate func(*Deploy) bool + +// Deploy is the wrapper over k8s deployment Object +type Deploy struct { + // kubernetes deployment instance + object *appsv1.Deployment +} + +// Builder enables building an instance of +// deployment +type Builder struct { + deployment *Deploy // kubernetes deployment instance + checks []Predicate // predicate list for deploy + errors []error +} + +// PredicateName type is wrapper over string. +// It is used to refer predicate and status msg. +type PredicateName string + +const ( + // PredicateProgressDeadlineExceeded refer to + // predicate IsProgressDeadlineExceeded. + PredicateProgressDeadlineExceeded PredicateName = "ProgressDeadlineExceeded" + // PredicateNotSpecSynced refer to predicate IsNotSpecSynced + PredicateNotSpecSynced PredicateName = "NotSpecSynced" + // PredicateOlderReplicaActive refer to predicate IsOlderReplicaActive + PredicateOlderReplicaActive PredicateName = "OlderReplicaActive" + // PredicateTerminationInProgress refer to predicate IsTerminationInProgress + PredicateTerminationInProgress PredicateName = "TerminationInProgress" + // PredicateUpdateInProgress refer to predicate IsUpdateInProgress. + PredicateUpdateInProgress PredicateName = "UpdateInProgress" +) + +// String implements the stringer interface +func (d *Deploy) String() string { + return stringer.Yaml("deployment", d.object) +} + +// GoString implements the goStringer interface +func (d *Deploy) GoString() string { + return d.String() +} + +// NewBuilder returns a new instance of builder meant for deployment +func NewBuilder() *Builder { + return &Builder{ + deployment: &Deploy{ + object: &appsv1.Deployment{}, + }, + } +} + +// WithName sets the Name field of deployment with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment: missing name"), + ) + return b + } + b.deployment.object.Name = name + return b +} + +// WithNamespace sets the Namespace field of deployment with provided value. +func (b *Builder) WithNamespace(namespace string) *Builder { + if len(namespace) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment: missing namespace"), + ) + return b + } + b.deployment.object.Namespace = namespace + return b +} + +// WithAnnotations merges existing annotations if any +// with the ones that are provided here +func (b *Builder) WithAnnotations(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: missing annotations"), + ) + return b + } + + if b.deployment.object.Annotations == nil { + return b.WithAnnotationsNew(annotations) + } + + for key, value := range annotations { + b.deployment.object.Annotations[key] = value + } + return b +} + +// WithAnnotationsNew resets existing annotaions if any with +// ones that are provided here +func (b *Builder) WithAnnotationsNew(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no new annotations"), + ) + return b + } + + // copy of original map + newannotations := map[string]string{} + for key, value := range annotations { + newannotations[key] = value + } + + // override + b.deployment.object.Annotations = newannotations + return b +} + +// WithNodeSelector Sets the node selector with the provided argument. +func (b *Builder) WithNodeSelector(selector map[string]string) *Builder { + if len(selector) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no node selector"), + ) + return b + } + if b.deployment.object.Spec.Template.Spec.NodeSelector == nil { + return b.WithNodeSelectorNew(selector) + } + + for key, value := range selector { + b.deployment.object.Spec.Template.Spec.NodeSelector[key] = value + } + return b +} + +// WithNodeSelector Sets the node selector with the provided argument. +func (b *Builder) WithNodeSelectorNew(selector map[string]string) *Builder { + if len(selector) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no new node selector"), + ) + return b + } + + b.deployment.object.Spec.Template.Spec.NodeSelector = selector + return b +} + +// WithOwnerReferenceNew sets ownerreference if any with +// ones that are provided here +func (b *Builder) WithOwnerReferenceNew(ownerRefernce []metav1.OwnerReference) *Builder { + if len(ownerRefernce) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no new ownerRefernce"), + ) + return b + } + + b.deployment.object.OwnerReferences = ownerRefernce + return b +} + +// WithLabels merges existing labels if any +// with the ones that are provided here +func (b *Builder) WithLabels(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: missing labels"), + ) + return b + } + + if b.deployment.object.Labels == nil { + return b.WithLabelsNew(labels) + } + + for key, value := range labels { + b.deployment.object.Labels[key] = value + } + return b +} + +// WithLabelsNew resets existing labels if any with +// ones that are provided here +func (b *Builder) WithLabelsNew(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no new labels"), + ) + return b + } + + // copy of original map + newlbls := map[string]string{} + for key, value := range labels { + newlbls[key] = value + } + + // override + b.deployment.object.Labels = newlbls + return b +} + +// WithSelectorMatchLabels merges existing matchlabels if any +// with the ones that are provided here +func (b *Builder) WithSelectorMatchLabels(matchlabels map[string]string) *Builder { + if len(matchlabels) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: missing matchlabels"), + ) + return b + } + + if b.deployment.object.Spec.Selector == nil { + return b.WithSelectorMatchLabelsNew(matchlabels) + } + + for key, value := range matchlabels { + b.deployment.object.Spec.Selector.MatchLabels[key] = value + } + return b +} + +// WithSelectorMatchLabelsNew resets existing matchlabels if any with +// ones that are provided here +func (b *Builder) WithSelectorMatchLabelsNew(matchlabels map[string]string) *Builder { + if len(matchlabels) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: no new matchlabels"), + ) + return b + } + + // copy of original map + newmatchlabels := map[string]string{} + for key, value := range matchlabels { + newmatchlabels[key] = value + } + + newselector := &metav1.LabelSelector{ + MatchLabels: newmatchlabels, + } + + // override + b.deployment.object.Spec.Selector = newselector + return b +} + +// WithReplicas sets the replica field of deployment +func (b *Builder) WithReplicas(replicas *int32) *Builder { + + if replicas == nil { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: nil replicas"), + ) + return b + } + + newreplicas := *replicas + + if newreplicas < 0 { + b.errors = append( + b.errors, + errors.Errorf( + "failed to build deployment object: invalid replicas {%d}", + newreplicas, + ), + ) + return b + } + + b.deployment.object.Spec.Replicas = &newreplicas + return b +} + +//WithStrategyType sets the strategy field of the deployment +func (b *Builder) WithStrategyType( + strategytype appsv1.DeploymentStrategyType, +) *Builder { + if len(strategytype) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build deployment object: missing strategytype"), + ) + return b + } + + b.deployment.object.Spec.Strategy.Type = strategytype + return b +} + +// WithPodTemplateSpecBuilder sets the template field of the deployment +func (b *Builder) WithPodTemplateSpecBuilder( + tmplbuilder *templatespec.Builder, +) *Builder { + if tmplbuilder == nil { + b.errors = append( + b.errors, + errors.New("failed to build deployment: nil templatespecbuilder"), + ) + return b + } + + templatespecObj, err := tmplbuilder.Build() + + if err != nil { + b.errors = append( + b.errors, + errors.Wrap( + err, + "failed to build deployment", + ), + ) + return b + } + + b.deployment.object.Spec.Template = *templatespecObj.Object + return b +} + +type deployBuildOption func(*Deploy) + +// NewForAPIObject returns a new instance of builder +// for a given deployment Object +func NewForAPIObject( + obj *appsv1.Deployment, + opts ...deployBuildOption, +) *Deploy { + d := &Deploy{object: obj} + for _, o := range opts { + o(d) + } + return d +} + +// Build returns a deployment instance +func (b *Builder) Build() (*appsv1.Deployment, error) { + err := b.validate() + // TODO: err in Wrapf is not logged. Fix is required + if err != nil { + return nil, errors.Wrapf(err, + "failed to build a deployment: %s", + b.deployment.object.Name) + } + return b.deployment.object, nil +} + +func (b *Builder) validate() error { + if len(b.errors) != 0 { + return errors.Errorf( + "failed to validate: build errors were found: %+v", + b.errors, + ) + } + return nil +} + +// IsRollout range over rolloutChecks map and check status of each predicate +// also it generates status message from rolloutStatuses using predicate key +func (d *Deploy) IsRollout() (PredicateName, bool) { + for pk, p := range rolloutChecks { + if p(d) { + return pk, false + } + } + return "", true +} + +// FailedRollout returns rollout status message for fail condition +func (d *Deploy) FailedRollout(name PredicateName) *RolloutOutput { + return &RolloutOutput{ + Message: rolloutStatuses[name](d), + IsRolledout: false, + } +} + +// SuccessRollout returns rollout status message for success condition +func (d *Deploy) SuccessRollout() *RolloutOutput { + return &RolloutOutput{ + Message: "deployment successfully rolled out", + IsRolledout: true, + } +} + +// RolloutStatus returns rollout message of deployment instance +func (d *Deploy) RolloutStatus() (op *RolloutOutput, err error) { + pk, ok := d.IsRollout() + if ok { + return d.SuccessRollout(), nil + } + return d.FailedRollout(pk), nil +} + +// RolloutStatusRaw returns rollout message of deployment instance +// in byte format +func (d *Deploy) RolloutStatusRaw() (op []byte, err error) { + message, err := d.RolloutStatus() + if err != nil { + return nil, err + } + return NewRollout( + withOutputObject(message)). + Raw() +} + +// AddCheck adds the predicate as a condition to be validated +// against the deployment instance +func (b *Builder) AddCheck(p Predicate) *Builder { + b.checks = append(b.checks, p) + return b +} + +// AddChecks adds the provided predicates as conditions to be +// validated against the deployment instance +func (b *Builder) AddChecks(p []Predicate) *Builder { + for _, check := range p { + b.AddCheck(check) + } + return b +} + +// IsProgressDeadlineExceeded is used to check update is timed out or not. +// If `Progressing` condition's reason is `ProgressDeadlineExceeded` then +// it is not rolled out. +func IsProgressDeadlineExceeded() Predicate { + return func(d *Deploy) bool { + return d.IsProgressDeadlineExceeded() + } +} + +// IsProgressDeadlineExceeded is used to check update is timed out or not. +// If `Progressing` condition's reason is `ProgressDeadlineExceeded` then +// it is not rolled out. +func (d *Deploy) IsProgressDeadlineExceeded() bool { + for _, cond := range d.object.Status.Conditions { + if cond.Type == appsv1.DeploymentProgressing && + cond.Reason == "ProgressDeadlineExceeded" { + return true + } + } + return false +} + +// IsOlderReplicaActive check if older replica's are still active or not if +// Status.UpdatedReplicas < *Spec.Replicas then some of the replicas are +// updated and some of them are not. +func IsOlderReplicaActive() Predicate { + return func(d *Deploy) bool { + return d.IsOlderReplicaActive() + } +} + +// IsOlderReplicaActive check if older replica's are still active or not if +// Status.UpdatedReplicas < *Spec.Replicas then some of the replicas are +// updated and some of them are not. +func (d *Deploy) IsOlderReplicaActive() bool { + return d.object.Spec.Replicas != nil && + d.object.Status.UpdatedReplicas < *d.object.Spec.Replicas +} + +// IsTerminationInProgress checks for older replicas are waiting to +// terminate or not. If Status.Replicas > Status.UpdatedReplicas then +// some of the older replicas are in running state because newer +// replicas are not in running state. It waits for newer replica to +// come into running state then terminate. +func IsTerminationInProgress() Predicate { + return func(d *Deploy) bool { + return d.IsTerminationInProgress() + } +} + +// IsTerminationInProgress checks for older replicas are waiting to +// terminate or not. If Status.Replicas > Status.UpdatedReplicas then +// some of the older replicas are in running state because newer +// replicas are not in running state. It waits for newer replica to +// come into running state then terminate. +func (d *Deploy) IsTerminationInProgress() bool { + return d.object.Status.Replicas > d.object.Status.UpdatedReplicas +} + +// IsUpdateInProgress Checks if all the replicas are updated or not. +// If Status.AvailableReplicas < Status.UpdatedReplicas then all the +//older replicas are not there but there are less number of availableReplicas +func IsUpdateInProgress() Predicate { + return func(d *Deploy) bool { + return d.IsUpdateInProgress() + } +} + +// IsUpdateInProgress Checks if all the replicas are updated or not. +// If Status.AvailableReplicas < Status.UpdatedReplicas then all the +// older replicas are not there but there are less number of availableReplicas +func (d *Deploy) IsUpdateInProgress() bool { + return d.object.Status.AvailableReplicas < d.object.Status.UpdatedReplicas +} + +// IsNotSyncSpec compare generation in status and spec and check if +// deployment spec is synced or not. If Generation <= Status.ObservedGeneration +// then deployment spec is not updated yet. +func IsNotSyncSpec() Predicate { + return func(d *Deploy) bool { + return d.IsNotSyncSpec() + } +} + +// IsNotSyncSpec compare generation in status and spec and check if +// deployment spec is synced or not. If Generation <= Status.ObservedGeneration +// then deployment spec is not updated yet. +func (d *Deploy) IsNotSyncSpec() bool { + return d.object.Generation > d.object.Status.ObservedGeneration +} diff --git a/tests/deploy/kubernetes.go b/tests/deploy/kubernetes.go new file mode 100644 index 0000000..6263e30 --- /dev/null +++ b/tests/deploy/kubernetes.go @@ -0,0 +1,417 @@ +/* +Copyright 2019 The OpenEBS 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 deploy + +import ( + "encoding/json" + "strings" + + client "github.com/openebs/zfs-localpv/pkg/common/kubernetes/client" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" +) + +// getClientsetFn is a typed function +// that abstracts fetching of kubernetes clientset +type getClientsetFn func() (*kubernetes.Clientset, error) + +// getClientsetFromPathFn is a typed function that +// abstracts fetching of clientset from kubeconfig path +type getClientsetForPathFn func(path string) (*kubernetes.Clientset, error) + +// getFn is a typed function that abstracts fetching a +// deployment instance from kubernetes cluster +type getFn func( + cli *kubernetes.Clientset, + name string, + namespace string, + opts *metav1.GetOptions, +) (*appsv1.Deployment, error) + +// listFn is a typed function that abstracts listing +// deployment instances from kubernetes cluster +type listFn func( + cli *kubernetes.Clientset, + namespace string, + opts *metav1.ListOptions, +) (*appsv1.DeploymentList, error) + +// createFn is a typed function that abstracts +// creating a deployment instance in kubernetes cluster +type createFn func( + cli *kubernetes.Clientset, + namespace string, + deploy *appsv1.Deployment, +) (*appsv1.Deployment, error) + +// deleteFn is a typed function that abstracts +// deleting a deployment from kubernetes cluster +type deleteFn func( + cli *kubernetes.Clientset, + namespace string, + name string, + opts *metav1.DeleteOptions, +) error + +// patchFn is a typed function that abstracts +// patching a deployment from kubernetes cluster +type patchFn func( + cli *kubernetes.Clientset, + name, namespace string, + pt types.PatchType, + data []byte, + subresources ...string, +) (*appsv1.Deployment, error) + +// rolloutStatusFn is a typed function that abstracts +// fetching rollout status of a deployment instance from +// kubernetes cluster +type rolloutStatusFn func(d *appsv1.Deployment) (*RolloutOutput, error) + +// rolloutStatusfFn is a typed function that abstracts +// fetching rollout status of a deployment instance from +// kubernetes cluster +type rolloutStatusfFn func(d *appsv1.Deployment) ([]byte, error) + +// defaultGetClientset is the default implementation to +// get kubernetes clientset instance +func defaultGetClientset() (*kubernetes.Clientset, error) { + return client.Instance().Clientset() +} + +// defaultGetClientsetForPath is the default implementation to +// get kubernetes clientset instance based on the given +// kubeconfig path +func defaultGetClientsetForPath(path string) (*kubernetes.Clientset, error) { + return client.New(client.WithKubeConfigPath(path)).Clientset() +} + +// defaultGet is the default implementation to get a +// deployment instance from kubernetes cluster +func defaultGet( + cli *kubernetes.Clientset, + name string, + namespace string, + opts *metav1.GetOptions, +) (*appsv1.Deployment, error) { + + return cli.AppsV1().Deployments(namespace).Get(name, *opts) +} + +// defaultList is the default implementation to list +// deployment instances from kubernetes cluster +func defaultList( + cli *kubernetes.Clientset, + namespace string, + opts *metav1.ListOptions, +) (*appsv1.DeploymentList, error) { + + return cli.AppsV1().Deployments(namespace).List(*opts) +} + +// defaultCreate is the default implementation to create +// a deployment instance in kubernetes cluster +func defaultCreate( + cli *kubernetes.Clientset, + namespace string, + deploy *appsv1.Deployment, +) (*appsv1.Deployment, error) { + + return cli.AppsV1().Deployments(namespace).Create(deploy) +} + +// defaultDel is the default implementation to delete a +// deployment instance in kubernetes cluster +func defaultDel( + cli *kubernetes.Clientset, + namespace string, + name string, + opts *metav1.DeleteOptions, +) error { + + return cli.AppsV1().Deployments(namespace).Delete(name, opts) +} + +func defaultPatch( + cli *kubernetes.Clientset, + name, namespace string, + pt types.PatchType, + data []byte, + subresources ...string, +) (*appsv1.Deployment, error) { + return cli.AppsV1().Deployments(namespace).Patch(name, pt, data, subresources...) +} + +// defaultRolloutStatus is the default implementation to +// fetch rollout status of a deployment instance from kubernetes +// cluster +func defaultRolloutStatus(d *appsv1.Deployment) (*RolloutOutput, error) { + b := NewForAPIObject(d) + return b.RolloutStatus() +} + +// defaultRolloutStatusf is the default implementation to fetch +// rollout status of a deployment instance from kubernetes cluster +func defaultRolloutStatusf(d *appsv1.Deployment) ([]byte, error) { + b := NewForAPIObject(d) + return b.RolloutStatusRaw() +} + +// Kubeclient enables kubernetes API operations on deployment instance +type Kubeclient struct { + // clientset refers to kubernetes clientset + // + // It enables CRUD operations of a deployment instance + // against a kubernetes cluster + clientset *kubernetes.Clientset + + namespace string + + // kubeconfig path to get kubernetes clientset + kubeConfigPath string + + // functions useful during mocking + getClientset getClientsetFn + getClientsetForPath getClientsetForPathFn + get getFn + list listFn + create createFn + del deleteFn + patch patchFn + rolloutStatus rolloutStatusFn + rolloutStatusf rolloutStatusfFn +} + +// KubeclientBuildOption defines the abstraction to build a +// kubeclient instance +type KubeclientBuildOption func(*Kubeclient) + +// withDefaults sets kubeclient instance's fields with defaults +// if these fields are not set +func (k *Kubeclient) withDefaults() { + + if k.getClientset == nil { + k.getClientset = defaultGetClientset + } + if k.getClientsetForPath == nil { + k.getClientsetForPath = defaultGetClientsetForPath + } + if k.get == nil { + k.get = defaultGet + } + if k.list == nil { + k.list = defaultList + } + if k.create == nil { + k.create = defaultCreate + } + if k.del == nil { + k.del = defaultDel + } + if k.patch == nil { + k.patch = defaultPatch + } + if k.rolloutStatus == nil { + k.rolloutStatus = defaultRolloutStatus + } + if k.rolloutStatusf == nil { + k.rolloutStatusf = defaultRolloutStatusf + } +} + +// WithClientset sets the kubernetes client against the kubeclient instance +func WithClientset(c *kubernetes.Clientset) KubeclientBuildOption { + return func(k *Kubeclient) { + k.clientset = c + } +} + +// WithNamespace sets the kubernetes client against +// the provided namespace +func (k *Kubeclient) WithNamespace(namespace string) *Kubeclient { + k.namespace = namespace + return k +} + +// WithKubeConfigPath sets the kubeConfig path +// against client instance +func WithKubeConfigPath(path string) KubeclientBuildOption { + return func(k *Kubeclient) { + k.kubeConfigPath = path + } +} + +// WithNamespace set namespace in kubeclient object +func WithNamespace(namespace string) KubeclientBuildOption { + return func(k *Kubeclient) { + k.namespace = namespace + } +} + +// NewKubeClient returns a new instance of kubeclient meant for deployment. +// caller can configure it with different kubeclientBuildOption +func NewKubeClient(opts ...KubeclientBuildOption) *Kubeclient { + k := &Kubeclient{} + for _, o := range opts { + o(k) + } + + k.withDefaults() + return k +} + +func (k *Kubeclient) getClientsetForPathOrDirect() (*kubernetes.Clientset, error) { + if k.kubeConfigPath != "" { + return k.getClientsetForPath(k.kubeConfigPath) + } + + return k.getClientset() +} + +// getClientOrCached returns either a new instance +// of kubernetes client or its cached copy +func (k *Kubeclient) getClientOrCached() (*kubernetes.Clientset, error) { + if k.clientset != nil { + return k.clientset, nil + } + + c, err := k.getClientsetForPathOrDirect() + if err != nil { + return nil, err + } + + k.clientset = c + return k.clientset, nil +} + +// Get returns deployment object for given name +func (k *Kubeclient) Get(name string) (*appsv1.Deployment, error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + + return k.get(cli, name, k.namespace, &metav1.GetOptions{}) +} + +// List returns deployment object for given name +func (k *Kubeclient) List(opts *metav1.ListOptions) (*appsv1.DeploymentList, error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + return k.list(cli, k.namespace, opts) +} + +// Patch patches deployment object for given name +func (k *Kubeclient) Patch( + name string, + pt types.PatchType, + data []byte, + subresources ...string, +) (*appsv1.Deployment, error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + + return k.patch(cli, name, k.namespace, pt, data, subresources...) +} + +// GetRaw returns deployment object for given name +func (k *Kubeclient) GetRaw(name string) ([]byte, error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + + d, err := k.get(cli, name, k.namespace, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return json.Marshal(d) +} + +// Delete deletes a deployment instance from the +// kubernetes cluster +func (k *Kubeclient) Delete(name string, opts *metav1.DeleteOptions) error { + + if strings.TrimSpace(name) == "" { + return errors.New("failed to delete deployment: missing deployment name") + } + + cli, err := k.getClientOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete deployment {%s}", name) + } + + return k.del(cli, k.namespace, name, opts) +} + +// Create creates a deployment in specified namespace in kubernetes cluster +func (k *Kubeclient) Create(deployment *appsv1.Deployment) (*appsv1.Deployment, error) { + + if deployment == nil { + return nil, errors.New("failed to create deployment: nil deployment object") + } + + cli, err := k.getClientOrCached() + if err != nil { + return nil, errors.Wrapf( + err, + "failed to create deployment {%s} in namespace {%s}", + deployment.Name, + deployment.Namespace, + ) + } + + return k.create(cli, k.namespace, deployment) +} + +// RolloutStatusf returns deployment's rollout status for given name +// in raw bytes +func (k *Kubeclient) RolloutStatusf(name string) (op []byte, err error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + + d, err := k.get(cli, name, k.namespace, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return k.rolloutStatusf(d) +} + +// RolloutStatus returns deployment's rollout status for given name +func (k *Kubeclient) RolloutStatus(name string) (*RolloutOutput, error) { + cli, err := k.getClientOrCached() + if err != nil { + return nil, err + } + + d, err := k.get(cli, name, k.namespace, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return k.rolloutStatus(d) +} diff --git a/tests/deploy/rollout_status.go b/tests/deploy/rollout_status.go new file mode 100644 index 0000000..5bb2def --- /dev/null +++ b/tests/deploy/rollout_status.go @@ -0,0 +1,135 @@ +/* +Copyright 2019 The OpenEBS 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 deploy + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +// rolloutStatus is a typed function that +// abstracts status message formation logic +type rolloutStatus func(*Deploy) string + +// rolloutStatuses contains a group of status message for +// each predicate checks. It uses predicateName as key. +var rolloutStatuses = map[PredicateName]rolloutStatus{ + // PredicateProgressDeadlineExceeded refer to rolloutStatus + // for predicate IsProgressDeadlineExceeded. + PredicateProgressDeadlineExceeded: func(d *Deploy) string { + return "deployment exceeded its progress deadline" + }, + // PredicateOlderReplicaActive refer to rolloutStatus for + // predicate IsOlderReplicaActive. + PredicateOlderReplicaActive: func(d *Deploy) string { + if d.object.Spec.Replicas == nil { + return "replica update in-progress: some older replicas were updated" + } + return fmt.Sprintf( + "replica update in-progress: %d of %d new replicas were updated", + d.object.Status.UpdatedReplicas, *d.object.Spec.Replicas) + }, + // PredicateTerminationInProgress refer rolloutStatus + // for predicate IsTerminationInProgress. + PredicateTerminationInProgress: func(d *Deploy) string { + return fmt.Sprintf( + "replica termination in-progress: %d old replicas are pending termination", + d.object.Status.Replicas-d.object.Status.UpdatedReplicas) + }, + // PredicateUpdateInProgress refer to rolloutStatus for predicate IsUpdateInProgress. + PredicateUpdateInProgress: func(d *Deploy) string { + return fmt.Sprintf( + "replica update in-progress: %d of %d updated replicas are available", + d.object.Status.AvailableReplicas, d.object.Status.UpdatedReplicas) + }, + // PredicateNotSpecSynced refer to status rolloutStatus for predicate IsNotSyncSpec. + PredicateNotSpecSynced: func(d *Deploy) string { + return "deployment rollout in-progress: waiting for deployment spec update" + }, +} + +// rolloutChecks contains a group of predicate it uses predicateName as key. +var rolloutChecks = map[PredicateName]Predicate{ + // PredicateProgressDeadlineExceeded refer to predicate IsProgressDeadlineExceeded. + PredicateProgressDeadlineExceeded: IsProgressDeadlineExceeded(), + // PredicateOlderReplicaActive refer to predicate IsOlderReplicaActive. + PredicateOlderReplicaActive: IsOlderReplicaActive(), + // PredicateTerminationInProgress refer to predicate IsTerminationInProgress. + PredicateTerminationInProgress: IsTerminationInProgress(), + // PredicateUpdateInProgress refer to predicate IsUpdateInProgress. + PredicateUpdateInProgress: IsUpdateInProgress(), + // PredicateNotSpecSynced refer to predicate IsSyncSpec. + PredicateNotSpecSynced: IsNotSyncSpec(), +} + +// RolloutOutput struct contains message and boolean value to show rolloutstatus +type RolloutOutput struct { + IsRolledout bool `json:"isRolledout"` + Message string `json:"message"` +} + +// rawFn is a typed function that abstracts +// conversion of rolloutOutput struct to raw byte +type rawFn func(r *RolloutOutput) ([]byte, error) + +// Rollout enables getting various output format of rolloutOutput +type Rollout struct { + output *RolloutOutput + raw rawFn +} + +// rolloutBuildOption defines the +// abstraction to build a rollout instance +type rolloutBuildOption func(*Rollout) + +// NewRollout returns new instance of rollout meant for +// rolloutOutput. caller can configure it with different +// rolloutOutputBuildOption +func NewRollout(opts ...rolloutBuildOption) *Rollout { + r := &Rollout{} + for _, o := range opts { + o(r) + } + r.withDefaults() + return r +} + +// withOutputObject sets rolloutOutput in rollout instance +func withOutputObject(o *RolloutOutput) rolloutBuildOption { + return func(r *Rollout) { + r.output = o + } +} + +// withDefaults sets the default options of rolloutBuilder instance +func (r *Rollout) withDefaults() { + if r.raw == nil { + r.raw = func(o *RolloutOutput) ([]byte, error) { + return json.Marshal(o) + } + } +} + +// Raw returns raw bytes outpot of rollout +func (r *Rollout) Raw() ([]byte, error) { + if r.output == nil { + return nil, errors.New("unable to get rollout status output") + } + return r.raw(r.output) +} diff --git a/tests/k8svolume/build.go b/tests/k8svolume/build.go new file mode 100644 index 0000000..4cff6b5 --- /dev/null +++ b/tests/k8svolume/build.go @@ -0,0 +1,139 @@ +/* +Copyright 2019 The OpenEBS 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 k8svolume + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + corev1 "k8s.io/api/core/v1" +) + +// Builder is the builder object for Volume +type Builder struct { + volume *Volume + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{volume: &Volume{object: &corev1.Volume{}}} +} + +// WithName sets the Name field of Volume with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Volume object: missing Volume name"), + ) + return b + } + b.volume.object.Name = name + return b +} + +// WithHostDirectory sets the VolumeSource field of Volume with provided hostpath +// as type directory. +func (b *Builder) WithHostDirectory(path string) *Builder { + if len(path) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build volume object: missing volume path"), + ) + return b + } + volumeSource := corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: path, + }, + } + + b.volume.object.VolumeSource = volumeSource + return b +} + +// WithHostPathAndType sets the VolumeSource field of Volume with provided +// hostpath as directory path and type as directory type +func (b *Builder) WithHostPathAndType( + dirpath string, + dirtype *corev1.HostPathType, +) *Builder { + if dirtype == nil { + b.errs = append( + b.errs, + errors.New("failed to build volume object: nil volume type"), + ) + return b + } + if len(dirpath) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build volume object: missing volume path"), + ) + return b + } + newdirtype := *dirtype + volumeSource := corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: dirpath, + Type: &newdirtype, + }, + } + + b.volume.object.VolumeSource = volumeSource + return b +} + +// WithPVCSource sets the Volume field of Volume with provided pvc +func (b *Builder) WithPVCSource(pvcName string) *Builder { + if len(pvcName) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build volume object: missing pvc name"), + ) + return b + } + volumeSource := corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + }, + } + b.volume.object.VolumeSource = volumeSource + return b +} + +// WithEmptyDir sets the EmptyDir field of the Volume with provided dir +func (b *Builder) WithEmptyDir(dir *corev1.EmptyDirVolumeSource) *Builder { + if dir == nil { + b.errs = append( + b.errs, + errors.New("failed to build volume object: nil dir"), + ) + return b + } + + newdir := *dir + b.volume.object.EmptyDir = &newdir + return b +} + +// Build returns the Volume API instance +func (b *Builder) Build() (*corev1.Volume, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("%+v", b.errs) + } + return b.volume.object, nil +} diff --git a/tests/k8svolume/volume.go b/tests/k8svolume/volume.go new file mode 100644 index 0000000..eea3673 --- /dev/null +++ b/tests/k8svolume/volume.go @@ -0,0 +1,71 @@ +// Copyright 2019 The OpenEBS 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 k8svolume + +import ( + corev1 "k8s.io/api/core/v1" +) + +// Volume is a wrapper over named volume api object, used +// within Pods. It provides build, validations and other common +// logic to be used by various feature specific callers. +type Volume struct { + object *corev1.Volume +} + +type volumeBuildOption func(*Volume) + +// NewForAPIObject returns a new instance of Volume +func NewForAPIObject(obj *corev1.Volume, opts ...volumeBuildOption) *Volume { + v := &Volume{object: obj} + for _, o := range opts { + o(v) + } + return v +} + +// Predicate defines an abstraction +// to determine conditional checks +// against the provided volume instance +type Predicate func(*Volume) bool + +// IsNil returns true if the Volume instance +// is nil +func (v *Volume) IsNil() bool { + return v.object == nil +} + +// IsNil is predicate to filter out nil Volume +// instances +func IsNil() Predicate { + return func(v *Volume) bool { + return v.IsNil() + } +} + +// PredicateList holds a list of predicate +type PredicateList []Predicate + +// all returns true if all the predicates +// succeed against the provided pvc +// instance +func (l PredicateList) all(v *Volume) bool { + for _, pred := range l { + if !pred(v) { + return false + } + } + return true +} diff --git a/tests/pod/build.go b/tests/pod/build.go new file mode 100644 index 0000000..244105d --- /dev/null +++ b/tests/pod/build.go @@ -0,0 +1,184 @@ +/* +Copyright 2019 The OpenEBS 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 pod + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + "github.com/openebs/zfs-localpv/tests/container" + volume "github.com/openebs/zfs-localpv/tests/k8svolume" + corev1 "k8s.io/api/core/v1" +) + +const ( + // k8sNodeLabelKeyHostname is the label key used by Kubernetes + // to store the hostname on the node resource. + k8sNodeLabelKeyHostname = "kubernetes.io/hostname" +) + +// Builder is the builder object for Pod +type Builder struct { + pod *Pod + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{pod: &Pod{object: &corev1.Pod{}}} +} + +// WithName sets the Name field of Pod with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing Pod name"), + ) + return b + } + b.pod.object.Name = name + return b +} + +// WithNamespace sets the Namespace field of Pod with provided value. +func (b *Builder) WithNamespace(namespace string) *Builder { + if len(namespace) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing namespace"), + ) + return b + } + b.pod.object.Namespace = namespace + return b +} + +// WithContainerBuilder adds a container to this pod object. +// +// NOTE: +// container details are present in the provided container +// builder object +func (b *Builder) WithContainerBuilder( + containerBuilder *container.Builder, +) *Builder { + containerObj, err := containerBuilder.Build() + if err != nil { + b.errs = append(b.errs, errors.Wrap(err, "failed to build pod")) + return b + } + b.pod.object.Spec.Containers = append( + b.pod.object.Spec.Containers, + containerObj, + ) + return b +} + +// WithVolumeBuilder sets Volumes field of deployment. +func (b *Builder) WithVolumeBuilder(volumeBuilder *volume.Builder) *Builder { + vol, err := volumeBuilder.Build() + if err != nil { + b.errs = append(b.errs, errors.Wrap(err, "failed to build deployment")) + return b + } + b.pod.object.Spec.Volumes = append( + b.pod.object.Spec.Volumes, + *vol, + ) + return b +} + +// WithRestartPolicy sets the RestartPolicy field in Pod with provided arguments +func (b *Builder) WithRestartPolicy( + restartPolicy corev1.RestartPolicy, +) *Builder { + b.pod.object.Spec.RestartPolicy = restartPolicy + return b +} + +// WithNodeName sets the NodeName field of Pod with provided value. +func (b *Builder) WithNodeName(nodeName string) *Builder { + if len(nodeName) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing Pod node name"), + ) + return b + } + b.pod.object.Spec.NodeName = nodeName + return b +} + +// WithNodeSelectorHostnameNew sets the Pod NodeSelector to the provided hostname value +// This function replaces (resets) the NodeSelector to use only hostname selector +func (b *Builder) WithNodeSelectorHostnameNew(hostname string) *Builder { + if len(hostname) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing Pod hostname"), + ) + return b + } + + b.pod.object.Spec.NodeSelector = map[string]string{ + k8sNodeLabelKeyHostname: hostname, + } + + return b +} + +// WithContainers sets the Containers field in Pod with provided arguments +func (b *Builder) WithContainers(containers []corev1.Container) *Builder { + if len(containers) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing containers"), + ) + return b + } + b.pod.object.Spec.Containers = containers + return b +} + +// WithContainer sets the Containers field in Pod with provided arguments +func (b *Builder) WithContainer(container corev1.Container) *Builder { + return b.WithContainers([]corev1.Container{container}) +} + +// WithVolumes sets the Volumes field in Pod with provided arguments +func (b *Builder) WithVolumes(volumes []corev1.Volume) *Builder { + if len(volumes) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing volumes"), + ) + return b + } + b.pod.object.Spec.Volumes = volumes + return b +} + +// WithVolume sets the Volumes field in Pod with provided arguments +func (b *Builder) WithVolume(volume corev1.Volume) *Builder { + return b.WithVolumes([]corev1.Volume{volume}) +} + +// Build returns the Pod API instance +func (b *Builder) Build() (*corev1.Pod, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("%+v", b.errs) + } + return b.pod.object, nil +} diff --git a/tests/pod/buildlist.go b/tests/pod/buildlist.go new file mode 100644 index 0000000..03d76ce --- /dev/null +++ b/tests/pod/buildlist.go @@ -0,0 +1,82 @@ +/* +Copyright 2019 The OpenEBS 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 pod + +import ( + corev1 "k8s.io/api/core/v1" +) + +// ListBuilder enables building an instance of +// Podlist +type ListBuilder struct { + list *PodList + filters predicateList +} + +// NewListBuilder returns a instance of ListBuilder +func NewListBuilder() *ListBuilder { + return &ListBuilder{list: &PodList{items: []*Pod{}}} +} + +// ListBuilderForAPIList returns a instance of ListBuilder from API PodList +func ListBuilderForAPIList(pods *corev1.PodList) *ListBuilder { + b := &ListBuilder{list: &PodList{}} + if pods == nil { + return b + } + for _, p := range pods.Items { + p := p + b.list.items = append(b.list.items, &Pod{object: &p}) + } + return b +} + +// ListBuilderForObjectList returns a instance of ListBuilder from API Pods +func ListBuilderForObjectList(pods ...*Pod) *ListBuilder { + b := &ListBuilder{list: &PodList{}} + if pods == nil { + return b + } + for _, p := range pods { + p := p + b.list.items = append(b.list.items, p) + } + return b +} + +// List returns the list of pod +// instances that was built by this +// builder +func (b *ListBuilder) List() *PodList { + if b.filters == nil || len(b.filters) == 0 { + return b.list + } + filtered := &PodList{} + for _, pod := range b.list.items { + if b.filters.all(pod) { + filtered.items = append(filtered.items, pod) + } + } + return filtered +} + +// WithFilter add filters on which the pod +// has to be filtered +func (b *ListBuilder) WithFilter(pred ...Predicate) *ListBuilder { + b.filters = append(b.filters, pred...) + return b +} diff --git a/tests/pod/kubernetes.go b/tests/pod/kubernetes.go new file mode 100644 index 0000000..f77b54e --- /dev/null +++ b/tests/pod/kubernetes.go @@ -0,0 +1,406 @@ +// Copyright 2019 The OpenEBS 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 pod + +import ( + "bytes" + "encoding/json" + + "github.com/openebs/zfs-localpv/pkg/common/errors" + client "github.com/openebs/zfs-localpv/pkg/common/kubernetes/client" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" +) + +// getClientsetFn is a typed function that +// abstracts fetching of clientset +type getClientsetFn func() (*clientset.Clientset, error) + +// getClientsetFromPathFn is a typed function that +// abstracts fetching of clientset from kubeConfigPath +type getClientsetForPathFn func(kubeConfigPath string) (*clientset.Clientset, error) + +// getKubeConfigFn is a typed function that +// abstracts fetching of config +type getKubeConfigFn func() (*rest.Config, error) + +// getKubeConfigForPathFn is a typed function that +// abstracts fetching of config from kubeConfigPath +type getKubeConfigForPathFn func(kubeConfigPath string) (*rest.Config, error) + +// createFn is a typed function that abstracts +// creation of pod +type createFn func(cli *clientset.Clientset, namespace string, pod *corev1.Pod) (*corev1.Pod, error) + +// listFn is a typed function that abstracts +// listing of pods +type listFn func(cli *clientset.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PodList, error) + +// deleteFn is a typed function that abstracts +// deleting of pod +type deleteFn func(cli *clientset.Clientset, namespace, name string, opts *metav1.DeleteOptions) error + +// deleteFn is a typed function that abstracts +// deletion of pod's collection +type deleteCollectionFn func(cli *clientset.Clientset, namespace string, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error + +// getFn is a typed function that abstracts +// to get pod +type getFn func(cli *clientset.Clientset, namespace, name string, opts metav1.GetOptions) (*corev1.Pod, error) + +// execFn is a typed function that abstracts +// pod exec +type execFn func(cli *clientset.Clientset, config *rest.Config, name, namespace string, opts *corev1.PodExecOptions) (*ExecOutput, error) + +// defaultExec is the default implementation of execFn +func defaultExec( + cli *clientset.Clientset, + config *rest.Config, + name string, + namespace string, + opts *corev1.PodExecOptions, +) (*ExecOutput, error) { + var stdout, stderr bytes.Buffer + + req := cli.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(name). + Namespace(namespace). + SubResource("exec"). + VersionedParams(opts, scheme.ParameterCodec) + + // create exec executor which is an interface + // for transporting shell-style streams + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return nil, err + } + + // Stream initiates transport of standard shell streams + // It will transport any non-nil stream to a remote system, + // and return an error if a problem occurs + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + Tty: opts.TTY, + }) + if err != nil { + return nil, err + } + + execOutput := &ExecOutput{ + Stdout: stdout.String(), + Stderr: stderr.String(), + } + return execOutput, nil +} + +// KubeClient enables kubernetes API operations +// on pod instance +type KubeClient struct { + // clientset refers to pod clientset + // that will be responsible to + // make kubernetes API calls + clientset *clientset.Clientset + + // namespace holds the namespace on which + // KubeClient has to operate + namespace string + + // kubeConfig represents kubernetes config + kubeConfig *rest.Config + + // kubeconfig path to get kubernetes clientset + kubeConfigPath string + + // functions useful during mocking + getKubeConfig getKubeConfigFn + getKubeConfigForPath getKubeConfigForPathFn + getClientset getClientsetFn + getClientsetForPath getClientsetForPathFn + create createFn + list listFn + del deleteFn + delCollection deleteCollectionFn + get getFn + exec execFn +} + +// ExecOutput struct contains stdout and stderr +type ExecOutput struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// KubeClientBuildOption defines the abstraction +// to build a KubeClient instance +type KubeClientBuildOption func(*KubeClient) + +// withDefaults sets the default options +// of KubeClient instance +func (k *KubeClient) withDefaults() { + if k.getKubeConfig == nil { + k.getKubeConfig = func() (config *rest.Config, err error) { + return client.New().Config() + } + } + if k.getKubeConfigForPath == nil { + k.getKubeConfigForPath = func(kubeConfigPath string) ( + config *rest.Config, err error) { + return client.New(client.WithKubeConfigPath(kubeConfigPath)). + GetConfigForPathOrDirect() + } + } + if k.getClientset == nil { + k.getClientset = func() (clients *clientset.Clientset, err error) { + return client.New().Clientset() + } + } + if k.getClientsetForPath == nil { + k.getClientsetForPath = func(kubeConfigPath string) ( + clients *clientset.Clientset, err error) { + return client.New(client.WithKubeConfigPath(kubeConfigPath)).Clientset() + } + } + if k.create == nil { + k.create = func(cli *clientset.Clientset, + namespace string, pod *corev1.Pod) (*corev1.Pod, error) { + return cli.CoreV1().Pods(namespace).Create(pod) + } + } + if k.list == nil { + k.list = func(cli *clientset.Clientset, + namespace string, opts metav1.ListOptions) (*corev1.PodList, error) { + return cli.CoreV1().Pods(namespace).List(opts) + } + } + if k.del == nil { + k.del = func(cli *clientset.Clientset, namespace, + name string, opts *metav1.DeleteOptions) error { + return cli.CoreV1().Pods(namespace).Delete(name, opts) + } + } + if k.get == nil { + k.get = func(cli *clientset.Clientset, namespace, + name string, opts metav1.GetOptions) (*corev1.Pod, error) { + return cli.CoreV1().Pods(namespace).Get(name, opts) + } + } + if k.delCollection == nil { + k.delCollection = func(cli *clientset.Clientset, namespace string, + listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + return cli.CoreV1().Pods(namespace).DeleteCollection(deleteOpts, listOpts) + } + } + if k.exec == nil { + k.exec = defaultExec + } +} + +// WithClientSet sets the kubernetes client against +// the KubeClient instance +func WithClientSet(c *clientset.Clientset) KubeClientBuildOption { + return func(k *KubeClient) { + k.clientset = c + } +} + +// WithKubeConfigPath sets the kubeConfig path +// against client instance +func WithKubeConfigPath(path string) KubeClientBuildOption { + return func(k *KubeClient) { + k.kubeConfigPath = path + } +} + +// NewKubeClient returns a new instance of KubeClient meant for +// zfs volume replica operations +func NewKubeClient(opts ...KubeClientBuildOption) *KubeClient { + k := &KubeClient{} + for _, o := range opts { + o(k) + } + k.withDefaults() + return k +} + +// WithNamespace sets the kubernetes namespace against +// the provided namespace +func (k *KubeClient) WithNamespace(namespace string) *KubeClient { + k.namespace = namespace + return k +} + +// WithKubeConfig sets the kubernetes config against +// the KubeClient instance +func (k *KubeClient) WithKubeConfig(config *rest.Config) *KubeClient { + k.kubeConfig = config + return k +} + +func (k *KubeClient) getClientsetForPathOrDirect() ( + *clientset.Clientset, error) { + if k.kubeConfigPath != "" { + return k.getClientsetForPath(k.kubeConfigPath) + } + return k.getClientset() +} + +// getClientsetOrCached returns either a new instance +// of kubernetes client or its cached copy +func (k *KubeClient) getClientsetOrCached() (*clientset.Clientset, error) { + if k.clientset != nil { + return k.clientset, nil + } + + cs, err := k.getClientsetForPathOrDirect() + if err != nil { + return nil, errors.Wrapf(err, "failed to get clientset") + } + k.clientset = cs + return k.clientset, nil +} + +func (k *KubeClient) getKubeConfigForPathOrDirect() (*rest.Config, error) { + if k.kubeConfigPath != "" { + return k.getKubeConfigForPath(k.kubeConfigPath) + } + return k.getKubeConfig() +} + +// getKubeConfigOrCached returns either a new instance +// of kubernetes config or its cached copy +func (k *KubeClient) getKubeConfigOrCached() (*rest.Config, error) { + if k.kubeConfig != nil { + return k.kubeConfig, nil + } + + kc, err := k.getKubeConfigForPathOrDirect() + if err != nil { + return nil, errors.Wrapf(err, "failed to get kube config") + } + k.kubeConfig = kc + return k.kubeConfig, nil +} + +// List returns a list of pod +// instances present in kubernetes cluster +func (k *KubeClient) List(opts metav1.ListOptions) (*corev1.PodList, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to list pods") + } + return k.list(cli, k.namespace, opts) +} + +// Delete deletes a pod instance present in kubernetes cluster +func (k *KubeClient) Delete(name string, opts *metav1.DeleteOptions) error { + if len(name) == 0 { + return errors.New("failed to delete pod: missing pod name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf( + err, + "failed to delete pod {%s}: failed to get clientset", + name, + ) + } + return k.del(cli, k.namespace, name, opts) +} + +// Create creates a pod in specified namespace in kubernetes cluster +func (k *KubeClient) Create(pod *corev1.Pod) (*corev1.Pod, error) { + if pod == nil { + return nil, errors.New("failed to create pod: nil pod object") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf( + err, + "failed to create pod {%s} in namespace {%s}", + pod.Name, + pod.Namespace, + ) + } + return k.create(cli, k.namespace, pod) +} + +// Get gets a pod object present in kubernetes cluster +func (k *KubeClient) Get(name string, + opts metav1.GetOptions) (*corev1.Pod, error) { + if len(name) == 0 { + return nil, errors.New("failed to get pod: missing pod name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf( + err, + "failed to get pod {%s}: failed to get clientset", + name, + ) + } + return k.get(cli, k.namespace, name, opts) +} + +// GetRaw gets pod object for a given name and namespace present +// in kubernetes cluster and returns result in raw byte. +func (k *KubeClient) GetRaw(name string, + opts metav1.GetOptions) ([]byte, error) { + p, err := k.Get(name, opts) + if err != nil { + return nil, err + } + return json.Marshal(p) +} + +// Exec runs a command remotely in a container of a pod +func (k *KubeClient) Exec(name string, + opts *corev1.PodExecOptions) (*ExecOutput, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, err + } + config, err := k.getKubeConfigOrCached() + if err != nil { + return nil, err + } + return k.exec(cli, config, name, k.namespace, opts) +} + +// ExecRaw runs a command remotely in a container of a pod +// and returns raw output +func (k *KubeClient) ExecRaw(name string, + opts *corev1.PodExecOptions) ([]byte, error) { + execOutput, err := k.Exec(name, opts) + if err != nil { + return nil, err + } + return json.Marshal(execOutput) +} + +// DeleteCollection deletes a collection of pod objects. +func (k *KubeClient) DeleteCollection(listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete the collection of pods") + } + return k.delCollection(cli, k.namespace, listOpts, deleteOpts) +} diff --git a/tests/pod/pod.go b/tests/pod/pod.go new file mode 100644 index 0000000..c42f69e --- /dev/null +++ b/tests/pod/pod.go @@ -0,0 +1,183 @@ +// Copyright 2019 The OpenEBS 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 pod + +import ( + corev1 "k8s.io/api/core/v1" +) + +// Pod holds the api's pod objects +type Pod struct { + object *corev1.Pod +} + +// PodList holds the list of API pod instances +type PodList struct { + items []*Pod +} + +// PredicateList holds a list of predicate +type predicateList []Predicate + +// Predicate defines an abstraction +// to determine conditional checks +// against the provided pod instance +type Predicate func(*Pod) bool + +// ToAPIList converts PodList to API PodList +func (pl *PodList) ToAPIList() *corev1.PodList { + plist := &corev1.PodList{} + for _, pod := range pl.items { + plist.Items = append(plist.Items, *pod.object) + } + return plist +} + +type podBuildOption func(*Pod) + +// NewForAPIObject returns a new instance of Pod +func NewForAPIObject(obj *corev1.Pod, opts ...podBuildOption) *Pod { + p := &Pod{object: obj} + for _, o := range opts { + o(p) + } + return p +} + +// Len returns the number of items present in the PodList +func (pl *PodList) Len() int { + return len(pl.items) +} + +// all returns true if all the predicates +// succeed against the provided pod +// instance +func (l predicateList) all(p *Pod) bool { + for _, pred := range l { + if !pred(p) { + return false + } + } + return true +} + +// IsRunning retuns true if the pod is in running +// state +func (p *Pod) IsRunning() bool { + return p.object.Status.Phase == "Running" +} + +// IsRunning is a predicate to filter out pods +// which in running state +func IsRunning() Predicate { + return func(p *Pod) bool { + return p.IsRunning() + } +} + +// IsCompleted retuns true if the pod is in completed +// state +func (p *Pod) IsCompleted() bool { + return p.object.Status.Phase == "Succeeded" +} + +// IsCompleted is a predicate to filter out pods +// which in completed state +func IsCompleted() Predicate { + return func(p *Pod) bool { + return p.IsCompleted() + } +} + +// HasLabels returns true if provided labels +// map[key]value are present in the provided PodList +// instance +func HasLabels(keyValuePair map[string]string) Predicate { + return func(p *Pod) bool { + // objKeyValues := p.object.GetLabels() + for key, value := range keyValuePair { + if !p.HasLabel(key, value) { + return false + } + } + return true + } +} + +// HasLabel return true if provided lable +// key and value are present in the the provided PodList +// instance +func (p *Pod) HasLabel(key, value string) bool { + val, ok := p.object.GetLabels()[key] + if ok { + return val == value + } + return false +} + +// HasLabel is predicate to filter out labeled +// pod instances +func HasLabel(key, value string) Predicate { + return func(p *Pod) bool { + return p.HasLabel(key, value) + } +} + +// IsNil returns true if the pod instance +// is nil +func (p *Pod) IsNil() bool { + return p.object == nil +} + +// IsNil is predicate to filter out nil pod +// instances +func IsNil() Predicate { + return func(p *Pod) bool { + return p.IsNil() + } +} + +// GetAPIObject returns a API's Pod +func (p *Pod) GetAPIObject() *corev1.Pod { + return p.object +} + +// FromList created a PodList with provided api podlist +func FromList(pods *corev1.PodList) *PodList { + pl := ListBuilderForAPIList(pods). + List() + return pl +} + +// GetScheduledNodes returns the nodes on which pods are scheduled +func (pl *PodList) GetScheduledNodes() map[string]int { + nodeNames := make(map[string]int) + for _, p := range pl.items { + p := p // pin it + nodeNames[p.object.Spec.NodeName]++ + } + return nodeNames +} + +// IsMatchNodeAny checks the PodList is running on the provided nodes +func (pl *PodList) IsMatchNodeAny(nodes map[string]int) bool { + for _, p := range pl.items { + p := p // pin it + if nodes[p.object.Spec.NodeName] == 0 { + return false + } + } + return true +} diff --git a/tests/provision_test.go b/tests/provision_test.go new file mode 100644 index 0000000..5e021f1 --- /dev/null +++ b/tests/provision_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 The OpenEBS 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 tests + +import ( + . "github.com/onsi/ginkgo" +) + +var _ = Describe("[zfspv] TEST VOLUME PROVISIONING", func() { + Context("App is deployed with zfs driver", func() { + It("Running zfs volume Creation Test", volumeCreationTest) + }) +}) + +func datasetCreationTest() { + By("Creating zfs storage class", createZfsStorageClass) + By("creating and verifying PVC bound status", createAndVerifyPVC) + By("Creating and deploying app pod", createDeployVerifyApp) + By("verifying ZFSVolume object", VerifyZFSVolume) + By("Deleting application deployment", deleteAppDeployment) + By("Deleting pvc", deletePVC) + By("Deleting storage class", deleteStorageClass) +} + +func zvolCreationTest() { + By("Creating ext4 storage class", createExt4StorageClass) + By("creating and verifying PVC bound status", createAndVerifyPVC) + + /* + * commenting app deployment as provisioning is taking time + * since we are creating a zfs pool on a sparse file and mkfs + * is taking forever for zvol. + * Should create the zfs pool on the disk. Need to check if travis + * has that functionality. + */ + //By("Creating and deploying app pod", createDeployVerifyApp) + By("verifying ZFSVolume object", VerifyZFSVolume) + //By("Deleting application deployment", deleteAppDeployment) + By("Deleting pvc", deletePVC) + By("Deleting storage class", deleteStorageClass) +} + +func volumeCreationTest() { + By("Running dataset creation test", datasetCreationTest) + By("Running zvol creation test", zvolCreationTest) +} diff --git a/tests/pts/pts.go b/tests/pts/pts.go new file mode 100644 index 0000000..4821277 --- /dev/null +++ b/tests/pts/pts.go @@ -0,0 +1,473 @@ +// Copyright 2019 The OpenEBS 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 pts + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + "github.com/openebs/zfs-localpv/tests/container" + volume "github.com/openebs/zfs-localpv/tests/k8svolume" + corev1 "k8s.io/api/core/v1" +) + +// PodTemplateSpec holds the api's podtemplatespec objects +type PodTemplateSpec struct { + Object *corev1.PodTemplateSpec +} + +// Builder is the builder object for Pod +type Builder struct { + podtemplatespec *PodTemplateSpec + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{ + podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }, + } +} + +// WithName sets the Name field of podtemplatespec with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec object: missing name"), + ) + return b + } + b.podtemplatespec.Object.Name = name + return b +} + +// WithNamespace sets the Namespace field of PodTemplateSpec with provided value. +func (b *Builder) WithNamespace(namespace string) *Builder { + if len(namespace) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing namespace", + ), + ) + return b + } + b.podtemplatespec.Object.Namespace = namespace + return b +} + +// WithAnnotations merges existing annotations if any +// with the ones that are provided here +func (b *Builder) WithAnnotations(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build deployment object: missing annotations"), + ) + return b + } + + if b.podtemplatespec.Object.Annotations == nil { + return b.WithAnnotationsNew(annotations) + } + + for key, value := range annotations { + b.podtemplatespec.Object.Annotations[key] = value + } + return b +} + +// WithAnnotationsNew resets the annotation field of podtemplatespec +// with provided arguments +func (b *Builder) WithAnnotationsNew(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing annotations", + ), + ) + return b + } + + // copy of original map + newannotations := map[string]string{} + for key, value := range annotations { + newannotations[key] = value + } + + // override + b.podtemplatespec.Object.Annotations = newannotations + return b +} + +// WithLabels merges existing labels if any +// with the ones that are provided here +func (b *Builder) WithLabels(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing labels", + ), + ) + return b + } + + if b.podtemplatespec.Object.Labels == nil { + return b.WithLabelsNew(labels) + } + + for key, value := range labels { + b.podtemplatespec.Object.Labels[key] = value + } + return b +} + +// WithLabelsNew resets the labels field of podtemplatespec +// with provided arguments +func (b *Builder) WithLabelsNew(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing labels", + ), + ) + return b + } + + // copy of original map + newlbls := map[string]string{} + for key, value := range labels { + newlbls[key] = value + } + + // override + b.podtemplatespec.Object.Labels = newlbls + return b +} + +// WithNodeSelector merges the nodeselectors if present +// with the provided arguments +func (b *Builder) WithNodeSelector(nodeselectors map[string]string) *Builder { + if len(nodeselectors) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing nodeselectors", + ), + ) + return b + } + + if b.podtemplatespec.Object.Spec.NodeSelector == nil { + return b.WithNodeSelectorNew(nodeselectors) + } + + for key, value := range nodeselectors { + b.podtemplatespec.Object.Spec.NodeSelector[key] = value + } + return b +} + +// WithNodeSelectorNew resets the nodeselector field of podtemplatespec +// with provided arguments +func (b *Builder) WithNodeSelectorNew(nodeselectors map[string]string) *Builder { + if len(nodeselectors) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing nodeselectors", + ), + ) + return b + } + + // copy of original map + newnodeselectors := map[string]string{} + for key, value := range nodeselectors { + newnodeselectors[key] = value + } + + // override + b.podtemplatespec.Object.Spec.NodeSelector = newnodeselectors + return b +} + +// WithServiceAccountName sets the ServiceAccountnNme field of podtemplatespec +func (b *Builder) WithServiceAccountName(serviceAccountnNme string) *Builder { + if len(serviceAccountnNme) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing serviceaccountname", + ), + ) + return b + } + + b.podtemplatespec.Object.Spec.ServiceAccountName = serviceAccountnNme + return b +} + +// WithAffinity sets the affinity field of podtemplatespec +func (b *Builder) WithAffinity(affinity *corev1.Affinity) *Builder { + if affinity == nil { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing affinity", + ), + ) + return b + } + + // copy of original pointer + newaffinitylist := *affinity + + b.podtemplatespec.Object.Spec.Affinity = &newaffinitylist + return b +} + +// WithTolerations merges the existing tolerations +// with the provided arguments +func (b *Builder) WithTolerations(tolerations ...corev1.Toleration) *Builder { + if tolerations == nil { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: nil tolerations", + ), + ) + return b + } + if len(tolerations) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing tolerations", + ), + ) + return b + } + + if len(b.podtemplatespec.Object.Spec.Tolerations) == 0 { + return b.WithTolerationsNew(tolerations...) + } + + b.podtemplatespec.Object.Spec.Tolerations = append( + b.podtemplatespec.Object.Spec.Tolerations, + tolerations..., + ) + + return b +} + +// WithTolerationsNew sets the tolerations field of podtemplatespec +func (b *Builder) WithTolerationsNew(tolerations ...corev1.Toleration) *Builder { + if tolerations == nil { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: nil tolerations", + ), + ) + return b + } + if len(tolerations) == 0 { + b.errs = append( + b.errs, + errors.New( + "failed to build podtemplatespec object: missing tolerations", + ), + ) + return b + } + + // copy of original slice + newtolerations := []corev1.Toleration{} + newtolerations = append(newtolerations, tolerations...) + + b.podtemplatespec.Object.Spec.Tolerations = newtolerations + + return b +} + +// WithContainerBuilders builds the list of containerbuilder +// provided and merges it to the containers field of the podtemplatespec +func (b *Builder) WithContainerBuilders( + containerBuilderList ...*container.Builder, +) *Builder { + if containerBuilderList == nil { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: nil containerbuilder"), + ) + return b + } + for _, containerBuilder := range containerBuilderList { + containerObj, err := containerBuilder.Build() + if err != nil { + b.errs = append( + b.errs, + errors.Wrap( + err, + "failed to build podtemplatespec", + ), + ) + return b + } + b.podtemplatespec.Object.Spec.Containers = append( + b.podtemplatespec.Object.Spec.Containers, + containerObj, + ) + } + return b +} + +// WithVolumeBuilders builds the list of volumebuilders provided +// and merges it to the volumes field of podtemplatespec. +func (b *Builder) WithVolumeBuilders( + volumeBuilderList ...*volume.Builder, +) *Builder { + if volumeBuilderList == nil { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: nil volumeBuilderList"), + ) + return b + } + for _, volumeBuilder := range volumeBuilderList { + vol, err := volumeBuilder.Build() + if err != nil { + b.errs = append( + b.errs, + errors.Wrap(err, "failed to build podtemplatespec"), + ) + return b + } + newvol := *vol + b.podtemplatespec.Object.Spec.Volumes = append( + b.podtemplatespec.Object.Spec.Volumes, + newvol, + ) + } + return b +} + +// WithContainerBuildersNew builds the list of containerbuilder +// provided and sets the containers field of the podtemplatespec +func (b *Builder) WithContainerBuildersNew( + containerBuilderList ...*container.Builder, +) *Builder { + if containerBuilderList == nil { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: nil containerbuilder"), + ) + return b + } + if len(containerBuilderList) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: missing containerbuilder"), + ) + return b + } + containerList := []corev1.Container{} + for _, containerBuilder := range containerBuilderList { + containerObj, err := containerBuilder.Build() + if err != nil { + b.errs = append( + b.errs, + errors.Wrap( + err, + "failed to build podtemplatespec", + ), + ) + return b + } + containerList = append( + containerList, + containerObj, + ) + } + b.podtemplatespec.Object.Spec.Containers = containerList + return b +} + +// WithVolumeBuildersNew builds the list of volumebuilders provided +// and sets Volumes field of podtemplatespec. +func (b *Builder) WithVolumeBuildersNew( + volumeBuilderList ...*volume.Builder, +) *Builder { + if volumeBuilderList == nil { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: nil volumeBuilderList"), + ) + return b + } + if len(volumeBuilderList) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build podtemplatespec: missing volumeBuilderList"), + ) + return b + } + volList := []corev1.Volume{} + for _, volumeBuilder := range volumeBuilderList { + vol, err := volumeBuilder.Build() + if err != nil { + b.errs = append( + b.errs, + errors.Wrap(err, "failed to build podtemplatespec"), + ) + return b + } + newvol := *vol + volList = append( + volList, + newvol, + ) + } + b.podtemplatespec.Object.Spec.Volumes = volList + return b +} + +// Build returns a deployment instance +func (b *Builder) Build() (*PodTemplateSpec, error) { + err := b.validate() + if err != nil { + return nil, errors.Wrapf( + err, + "failed to build a podtemplatespec: %s", + b.podtemplatespec.Object, + ) + } + return b.podtemplatespec, nil +} + +func (b *Builder) validate() error { + if len(b.errs) != 0 { + return errors.Errorf( + "failed to validate: build errors were found: %v", + b.errs, + ) + } + return nil +} diff --git a/tests/pvc/build.go b/tests/pvc/build.go new file mode 100644 index 0000000..1d4cee0 --- /dev/null +++ b/tests/pvc/build.go @@ -0,0 +1,181 @@ +/* +Copyright 2019 The OpenEBS 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 pvc + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// Builder is the builder object for PVC +type Builder struct { + pvc *PVC + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{pvc: &PVC{object: &corev1.PersistentVolumeClaim{}}} +} + +// BuildFrom returns new instance of Builder +// from the provided api instance +func BuildFrom(pvc *corev1.PersistentVolumeClaim) *Builder { + if pvc == nil { + b := NewBuilder() + b.errs = append( + b.errs, + errors.New("failed to build pvc object: nil pvc"), + ) + return b + } + return &Builder{ + pvc: &PVC{ + object: pvc, + }, + } +} + +// WithName sets the Name field of PVC with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append(b.errs, errors.New("failed to build PVC object: missing PVC name")) + return b + } + b.pvc.object.Name = name + return b +} + +// WithGenerateName sets the GenerateName field of +// PVC with provided value +func (b *Builder) WithGenerateName(name string) *Builder { + if len(name) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build PVC object: missing PVC generateName"), + ) + return b + } + + b.pvc.object.GenerateName = name + return b +} + +// WithNamespace sets the Namespace field of PVC provided arguments +func (b *Builder) WithNamespace(namespace string) *Builder { + if len(namespace) == 0 { + namespace = "default" + } + b.pvc.object.Namespace = namespace + return b +} + +// WithAnnotations sets the Annotations field of PVC with provided arguments +func (b *Builder) WithAnnotations(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errs = append(b.errs, errors.New("failed to build PVC object: missing annotations")) + return b + } + b.pvc.object.Annotations = annotations + return b +} + +// WithLabels merges existing labels if any +// with the ones that are provided here +func (b *Builder) WithLabels(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build PVC object: missing labels"), + ) + return b + } + + if b.pvc.object.Labels == nil { + b.pvc.object.Labels = map[string]string{} + } + + for key, value := range labels { + b.pvc.object.Labels[key] = value + } + return b +} + +// WithLabelsNew resets existing labels if any with +// ones that are provided here +func (b *Builder) WithLabelsNew(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build PVC object: missing labels"), + ) + return b + } + + // copy of original map + newlbls := map[string]string{} + for key, value := range labels { + newlbls[key] = value + } + + // override + b.pvc.object.Labels = newlbls + return b +} + +// WithStorageClass sets the StorageClass field of PVC with provided arguments +func (b *Builder) WithStorageClass(scName string) *Builder { + if len(scName) == 0 { + b.errs = append(b.errs, errors.New("failed to build PVC object: missing storageclass name")) + return b + } + b.pvc.object.Spec.StorageClassName = &scName + return b +} + +// WithAccessModes sets the AccessMode field in PVC with provided arguments +func (b *Builder) WithAccessModes(accessMode []corev1.PersistentVolumeAccessMode) *Builder { + if len(accessMode) == 0 { + b.errs = append(b.errs, errors.New("failed to build PVC object: missing accessmodes")) + return b + } + b.pvc.object.Spec.AccessModes = accessMode + return b +} + +// WithCapacity sets the Capacity field in PVC with provided arguments +func (b *Builder) WithCapacity(capacity string) *Builder { + resCapacity, err := resource.ParseQuantity(capacity) + if err != nil { + b.errs = append(b.errs, errors.Wrapf(err, "failed to build PVC object: failed to parse capacity {%s}", capacity)) + return b + } + resourceList := corev1.ResourceList{ + corev1.ResourceName(corev1.ResourceStorage): resCapacity, + } + b.pvc.object.Spec.Resources.Requests = resourceList + return b +} + +// Build returns the PVC API instance +func (b *Builder) Build() (*corev1.PersistentVolumeClaim, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("%+v", b.errs) + } + return b.pvc.object, nil +} diff --git a/tests/pvc/buildlist.go b/tests/pvc/buildlist.go new file mode 100644 index 0000000..7cc320e --- /dev/null +++ b/tests/pvc/buildlist.go @@ -0,0 +1,165 @@ +/* +Copyright 2019 The OpenEBS 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 pvc + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + corev1 "k8s.io/api/core/v1" +) + +// ListBuilder enables building an instance of +// PVCList +type ListBuilder struct { + // template to build a list of pvcs + template *corev1.PersistentVolumeClaim + + // count determines the number of + // pvcs to be built using the provided + // template + count int + + list *PVCList + filters PredicateList + errs []error +} + +// NewListBuilder returns an instance of ListBuilder +func NewListBuilder() *ListBuilder { + return &ListBuilder{list: &PVCList{}} +} + +// ListBuilderFromTemplate returns a new instance of +// ListBuilder based on the provided pvc template +func ListBuilderFromTemplate(pvc *corev1.PersistentVolumeClaim) *ListBuilder { + b := NewListBuilder() + if pvc == nil { + b.errs = append( + b.errs, + errors.New("failed to build pvc list: nil pvc template"), + ) + return b + } + + b.template = pvc + b.count = 1 + return b +} + +// ListBuilderForAPIObjects returns a new instance of +// ListBuilder based on provided api pvc list +func ListBuilderForAPIObjects(pvcs *corev1.PersistentVolumeClaimList) *ListBuilder { + b := &ListBuilder{list: &PVCList{}} + + if pvcs == nil { + b.errs = append( + b.errs, + errors.New("failed to build pvc list: missing api list"), + ) + return b + } + + for _, pvc := range pvcs.Items { + pvc := pvc + b.list.items = append(b.list.items, &PVC{object: &pvc}) + } + + return b +} + +// ListBuilderForObjects returns a new instance of +// ListBuilder based on provided pvc list +func ListBuilderForObjects(pvcs *PVCList) *ListBuilder { + b := &ListBuilder{} + if pvcs == nil { + b.errs = append( + b.errs, + errors.New("failed to build pvc list: missing object list"), + ) + return b + } + + b.list = pvcs + return b +} + +// WithFilter adds filters on which the pvcs +// are filtered +func (b *ListBuilder) WithFilter(pred ...Predicate) *ListBuilder { + b.filters = append(b.filters, pred...) + return b +} + +// WithCount sets the count that determines +// the number of pvcs to be built +func (b *ListBuilder) WithCount(count int) *ListBuilder { + b.count = count + return b +} + +func (b *ListBuilder) buildFromTemplateIfNilList() { + if len(b.list.items) != 0 || b.template == nil { + return + } + + for i := 0; i < b.count; i++ { + b.list.items = append(b.list.items, &PVC{object: b.template}) + } +} + +// List returns the list of pvc instances +// that was built by this builder +func (b *ListBuilder) List() (*PVCList, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("failed to build pvc list: %+v", b.errs) + } + + b.buildFromTemplateIfNilList() + + if b.filters == nil || len(b.filters) == 0 { + return b.list, nil + } + + filteredList := &PVCList{} + for _, pvc := range b.list.items { + if b.filters.all(pvc) { + filteredList.items = append(filteredList.items, pvc) + } + } + + return filteredList, nil +} + +// Len returns the number of items present +// in the PVCList of a builder +func (b *ListBuilder) Len() (int, error) { + l, err := b.List() + if err != nil { + return 0, err + } + + return l.Len(), nil +} + +// APIList builds core API PVC list using listbuilder +func (b *ListBuilder) APIList() (*corev1.PersistentVolumeClaimList, error) { + l, err := b.List() + if err != nil { + return nil, err + } + + return l.ToAPIList(), nil +} diff --git a/tests/pvc/kubernetes.go b/tests/pvc/kubernetes.go new file mode 100644 index 0000000..df82416 --- /dev/null +++ b/tests/pvc/kubernetes.go @@ -0,0 +1,288 @@ +// Copyright 2019 The OpenEBS 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 pvc + +import ( + "strings" + + "github.com/openebs/zfs-localpv/pkg/common/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + client "github.com/openebs/zfs-localpv/pkg/common/kubernetes/client" + "k8s.io/client-go/kubernetes" +) + +// getClientsetFn is a typed function that +// abstracts fetching of clientset +type getClientsetFn func() (clientset *kubernetes.Clientset, err error) + +// getClientsetFromPathFn is a typed function that +// abstracts fetching of clientset from kubeConfigPath +type getClientsetForPathFn func(kubeConfigPath string) (clientset *kubernetes.Clientset, err error) + +// getpvcFn is a typed function that +// abstracts fetching of pvc +type getFn func(cli *kubernetes.Clientset, name string, namespace string, opts metav1.GetOptions) (*corev1.PersistentVolumeClaim, error) + +// listFn is a typed function that abstracts +// listing of pvcs +type listFn func(cli *kubernetes.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PersistentVolumeClaimList, error) + +// deleteFn is a typed function that abstracts +// deletion of pvcs +type deleteFn func(cli *kubernetes.Clientset, namespace string, name string, deleteOpts *metav1.DeleteOptions) error + +// deleteFn is a typed function that abstracts +// deletion of pvc's collection +type deleteCollectionFn func(cli *kubernetes.Clientset, namespace string, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error + +// createFn is a typed function that abstracts +// creation of pvc +type createFn func(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) + +// updateFn is a typed function that abstracts +// updation of pvc +type updateFn func(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) + +// Kubeclient enables kubernetes API operations +// on pvc instance +type Kubeclient struct { + // clientset refers to pvc clientset + // that will be responsible to + // make kubernetes API calls + clientset *kubernetes.Clientset + + // namespace holds the namespace on which + // kubeclient has to operate + namespace string + + // kubeconfig path to get kubernetes clientset + kubeConfigPath string + + // functions useful during mocking + getClientset getClientsetFn + getClientsetForPath getClientsetForPathFn + list listFn + get getFn + create createFn + update updateFn + del deleteFn + delCollection deleteCollectionFn +} + +// KubeclientBuildOption abstracts creating an +// instance of kubeclient +type KubeclientBuildOption func(*Kubeclient) + +// withDefaults sets the default options +// of kubeclient instance +func (k *Kubeclient) withDefaults() { + if k.getClientset == nil { + k.getClientset = func() (clients *kubernetes.Clientset, err error) { + return client.New().Clientset() + } + } + + if k.getClientsetForPath == nil { + k.getClientsetForPath = func(kubeConfigPath string) (clients *kubernetes.Clientset, err error) { + return client.New(client.WithKubeConfigPath(kubeConfigPath)).Clientset() + } + } + + if k.get == nil { + k.get = func(cli *kubernetes.Clientset, name string, namespace string, opts metav1.GetOptions) (*corev1.PersistentVolumeClaim, error) { + return cli.CoreV1().PersistentVolumeClaims(namespace).Get(name, opts) + } + } + + if k.list == nil { + k.list = func(cli *kubernetes.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PersistentVolumeClaimList, error) { + return cli.CoreV1().PersistentVolumeClaims(namespace).List(opts) + } + } + + if k.del == nil { + k.del = func(cli *kubernetes.Clientset, namespace string, name string, deleteOpts *metav1.DeleteOptions) error { + return cli.CoreV1().PersistentVolumeClaims(namespace).Delete(name, deleteOpts) + } + } + + if k.delCollection == nil { + k.delCollection = func(cli *kubernetes.Clientset, namespace string, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + return cli.CoreV1().PersistentVolumeClaims(namespace).DeleteCollection(deleteOpts, listOpts) + } + } + + if k.create == nil { + k.create = func(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + return cli.CoreV1().PersistentVolumeClaims(namespace).Create(pvc) + } + } + + if k.update == nil { + k.update = func(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + return cli.CoreV1().PersistentVolumeClaims(namespace).Update(pvc) + } + } +} + +// WithClientSet sets the kubernetes client against +// the kubeclient instance +func WithClientSet(c *kubernetes.Clientset) KubeclientBuildOption { + return func(k *Kubeclient) { + k.clientset = c + } +} + +// WithKubeConfigPath sets the kubeConfig path +// against client instance +func WithKubeConfigPath(path string) KubeclientBuildOption { + return func(k *Kubeclient) { + k.kubeConfigPath = path + } +} + +// NewKubeClient returns a new instance of kubeclient meant for +// pvc operations +func NewKubeClient(opts ...KubeclientBuildOption) *Kubeclient { + k := &Kubeclient{} + for _, o := range opts { + o(k) + } + k.withDefaults() + return k +} + +// WithNamespace sets the kubernetes client against +// the provided namespace +func (k *Kubeclient) WithNamespace(namespace string) *Kubeclient { + k.namespace = namespace + return k +} + +func (k *Kubeclient) getClientsetForPathOrDirect() (*kubernetes.Clientset, error) { + if k.kubeConfigPath != "" { + return k.getClientsetForPath(k.kubeConfigPath) + } + return k.getClientset() +} + +// getClientsetOrCached returns either a new instance +// of kubernetes client or its cached copy +func (k *Kubeclient) getClientsetOrCached() (*kubernetes.Clientset, error) { + if k.clientset != nil { + return k.clientset, nil + } + + cs, err := k.getClientsetForPathOrDirect() + if err != nil { + return nil, errors.Wrapf(err, "failed to get clientset") + } + k.clientset = cs + return k.clientset, nil +} + +// Get returns a pvc resource +// instances present in kubernetes cluster +func (k *Kubeclient) Get(name string, opts metav1.GetOptions) (*corev1.PersistentVolumeClaim, error) { + if strings.TrimSpace(name) == "" { + return nil, errors.New("failed to get pvc: missing pvc name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to get pvc {%s}", name) + } + return k.get(cli, name, k.namespace, opts) +} + +// List returns a list of pvc +// instances present in kubernetes cluster +func (k *Kubeclient) List(opts metav1.ListOptions) (*corev1.PersistentVolumeClaimList, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to list pvc listoptions: '%v'", opts) + } + return k.list(cli, k.namespace, opts) +} + +// Delete deletes a pvc instance from the +// kubecrnetes cluster +func (k *Kubeclient) Delete(name string, deleteOpts *metav1.DeleteOptions) error { + if strings.TrimSpace(name) == "" { + return errors.New("failed to delete pvc: missing pvc name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete pvc {%s}", name) + } + return k.del(cli, k.namespace, name, deleteOpts) +} + +// Create creates a pvc in specified namespace in kubernetes cluster +func (k *Kubeclient) Create(pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + if pvc == nil { + return nil, errors.New("failed to create pvc: nil pvc object") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to create pvc {%s} in namespace {%s}", pvc.Name, pvc.Namespace) + } + return k.create(cli, k.namespace, pvc) +} + +// Update updates a pvc in specified namespace in kubernetes cluster +func (k *Kubeclient) Update(pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + if pvc == nil { + return nil, errors.New("failed to update pvc: nil pvc object") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to update pvc {%s} in namespace {%s}", pvc.Name, pvc.Namespace) + } + return k.update(cli, k.namespace, pvc) +} + +// CreateCollection creates a list of pvcs +// in specified namespace in kubernetes cluster +func (k *Kubeclient) CreateCollection( + list *corev1.PersistentVolumeClaimList, +) (*corev1.PersistentVolumeClaimList, error) { + if list == nil || len(list.Items) == 0 { + return nil, errors.New("failed to create list of pvcs: nil pvc list provided") + } + + newlist := &corev1.PersistentVolumeClaimList{} + for _, item := range list.Items { + item := item + obj, err := k.Create(&item) + if err != nil { + return nil, err + } + + newlist.Items = append(newlist.Items, *obj) + } + + return newlist, nil +} + +// DeleteCollection deletes a collection of pvc objects. +func (k *Kubeclient) DeleteCollection(listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete the collection of pvcs") + } + return k.delCollection(cli, k.namespace, listOpts, deleteOpts) +} diff --git a/tests/pvc/persistentvolumeclaim.go b/tests/pvc/persistentvolumeclaim.go new file mode 100644 index 0000000..4848e21 --- /dev/null +++ b/tests/pvc/persistentvolumeclaim.go @@ -0,0 +1,116 @@ +// Copyright 2019 The OpenEBS 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 pvc + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" +) + +// PVC is a wrapper over persistentvolumeclaim api +// object. It provides build, validations and other common +// logic to be used by various feature specific callers. +type PVC struct { + object *corev1.PersistentVolumeClaim +} + +// PVCList is a wrapper over persistentvolumeclaim api +// object. It provides build, validations and other common +// logic to be used by various feature specific callers. +type PVCList struct { + items []*PVC +} + +// Len returns the number of items present +// in the PVCList +func (p *PVCList) Len() int { + return len(p.items) +} + +// ToAPIList converts PVCList to API PVCList +func (p *PVCList) ToAPIList() *corev1.PersistentVolumeClaimList { + plist := &corev1.PersistentVolumeClaimList{} + for _, pvc := range p.items { + plist.Items = append(plist.Items, *pvc.object) + } + return plist +} + +type pvcBuildOption func(*PVC) + +// NewForAPIObject returns a new instance of PVC +func NewForAPIObject(obj *corev1.PersistentVolumeClaim, opts ...pvcBuildOption) *PVC { + p := &PVC{object: obj} + for _, o := range opts { + o(p) + } + return p +} + +// Predicate defines an abstraction +// to determine conditional checks +// against the provided pvc instance +type Predicate func(*PVC) bool + +// IsBound returns true if the pvc is bounded +func (p *PVC) IsBound() bool { + return p.object.Status.Phase == corev1.ClaimBound +} + +// IsBound is a predicate to filter out pvcs +// which is bounded +func IsBound() Predicate { + return func(p *PVC) bool { + return p.IsBound() + } +} + +// IsNil returns true if the PVC instance +// is nil +func (p *PVC) IsNil() bool { + return p.object == nil +} + +// IsNil is predicate to filter out nil PVC +// instances +func IsNil() Predicate { + return func(p *PVC) bool { + return p.IsNil() + } +} + +// ContainsName is filter function to filter pvc's +// based on the name +func ContainsName(name string) Predicate { + return func(p *PVC) bool { + return strings.Contains(p.object.GetName(), name) + } +} + +// PredicateList holds a list of predicate +type PredicateList []Predicate + +// all returns true if all the predicates +// succeed against the provided pvc +// instance +func (l PredicateList) all(p *PVC) bool { + for _, pred := range l { + if !pred(p) { + return false + } + } + return true +} diff --git a/tests/sc/build.go b/tests/sc/build.go new file mode 100644 index 0000000..cbd6dce --- /dev/null +++ b/tests/sc/build.go @@ -0,0 +1,115 @@ +/* +Copyright 2019 The OpenEBS 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 sc + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + storagev1 "k8s.io/api/storage/v1" +) + +// Builder enables building an instance of StorageClass +type Builder struct { + sc *StorageClass + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{sc: &StorageClass{object: &storagev1.StorageClass{}}} +} + +// WithName sets the Name field of storageclass with provided argument. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append(b.errs, errors.New("failed to build storageclass: missing storageclass name")) + return b + } + b.sc.object.Name = name + return b +} + +// WithGenerateName appends a random string after the name +func (b *Builder) WithGenerateName(name string) *Builder { + b.sc.object.GenerateName = name + "-" + return b +} + +// WithAnnotations sets the Annotations field of storageclass with provided value. +func (b *Builder) WithAnnotations(annotations map[string]string) *Builder { + if len(annotations) == 0 { + b.errs = append(b.errs, errors.New("failed to build storageclass: missing annotations")) + } + b.sc.object.Annotations = annotations + return b +} + +// WithParametersNew resets existing parameters if any with +// ones that are provided here +func (b *Builder) WithParametersNew(parameters map[string]string) *Builder { + if len(parameters) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build storageclass object: no new parameters"), + ) + return b + } + + // copy of original map + newparameters := map[string]string{} + for key, value := range parameters { + newparameters[key] = value + } + + // override + b.sc.object.Parameters = newparameters + return b +} + +// WithProvisioner sets the Provisioner field of storageclass with provided argument. +func (b *Builder) WithProvisioner(provisioner string) *Builder { + if len(provisioner) == 0 { + b.errs = append(b.errs, errors.New("failed to build storageclass: missing provisioner name")) + return b + } + b.sc.object.Provisioner = provisioner + return b +} + +// WithVolumeExpansion sets the AllowedVolumeExpansion field of storageclass with provided argument. +func (b *Builder) WithVolumeExpansion(expansionAllowed bool) *Builder { + b.sc.object.AllowVolumeExpansion = &expansionAllowed + return b +} + +// Build returns the StorageClass API instance +func (b *Builder) Build() (*storagev1.StorageClass, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("%+v", b.errs) + } + return b.sc.object, nil +} + +// WithVolumeBindingMode sets the volume binding mode of storageclass with +// provided argument. +func (b *Builder) WithVolumeBindingMode(bindingMode storagev1.VolumeBindingMode) *Builder { + if bindingMode == "" { + b.errs = append(b.errs, errors.New("failed to build storageclass: missing volume binding mode")) + return b + } + b.sc.object.VolumeBindingMode = &bindingMode + return b +} diff --git a/tests/sc/buildlist.go b/tests/sc/buildlist.go new file mode 100644 index 0000000..af439fe --- /dev/null +++ b/tests/sc/buildlist.go @@ -0,0 +1,102 @@ +/* +Copyright 2019 The OpenEBS 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 sc + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + storagev1 "k8s.io/api/storage/v1" +) + +// ListBuilder enables building an instance of StorageClassList +type ListBuilder struct { + list *StorageClassList + filters predicateList + errs []error +} + +// NewListBuilder returns a instance of ListBuilder +func NewListBuilder() *ListBuilder { + return &ListBuilder{list: &StorageClassList{items: []*StorageClass{}}} +} + +// ListBuilderForAPIList builds the ListBuilder object based on SC API list +func ListBuilderForAPIList(scl *storagev1.StorageClassList) *ListBuilder { + b := &ListBuilder{list: &StorageClassList{}} + if scl == nil { + b.errs = append(b.errs, errors.New("failed to build storageclass list: missing api list")) + return b + } + for _, sc := range scl.Items { + sc := sc + b.list.items = append(b.list.items, &StorageClass{object: &sc}) + } + return b +} + +// ListBuilderForObjects returns a instance of ListBuilder from SC instances +func ListBuilderForObjects(scl *StorageClassList) *ListBuilder { + b := &ListBuilder{list: &StorageClassList{}} + if scl == nil { + b.errs = append(b.errs, errors.New("failed to build storageclass list: missing object list")) + return b + } + b.list = scl + return b +} + +// List returns the list of StorageClass instances that was built by this builder +func (b *ListBuilder) List() (*StorageClassList, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("failed to list storageclass: %+v", b.errs) + } + if b.filters == nil || len(b.filters) == 0 { + return b.list, nil + } + filtered := &StorageClassList{} + for _, sc := range b.list.items { + if b.filters.all(sc) { + sc := sc // Pin it + filtered.items = append(filtered.items, sc) + } + } + return filtered, nil +} + +// WithFilter add filters on which the StorageClass has to be filtered +func (b *ListBuilder) WithFilter(pred ...Predicate) *ListBuilder { + b.filters = append(b.filters, pred...) + return b +} + +// APIList builds core API PVC list using listbuilder +func (b *ListBuilder) APIList() (*storagev1.StorageClassList, error) { + l, err := b.List() + if err != nil { + return nil, err + } + return l.ToAPIList(), nil +} + +// Len returns the number of items present +// in the PVCList of a builder +func (b *ListBuilder) Len() (int, error) { + l, err := b.List() + if err != nil { + return 0, err + } + return l.Len(), nil +} diff --git a/tests/sc/kubernetes.go b/tests/sc/kubernetes.go new file mode 100644 index 0000000..efcffd2 --- /dev/null +++ b/tests/sc/kubernetes.go @@ -0,0 +1,193 @@ +/* +Copyright 2019 The OpenEBS 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 sc + +import ( + "github.com/openebs/zfs-localpv/pkg/common/errors" + client "github.com/openebs/zfs-localpv/pkg/common/kubernetes/client" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// getClientsetFn is a typed function that abstracts +// fetching an instance of kubernetes clientset +type getClientsetFn func() (clientset *kubernetes.Clientset, err error) + +// getClientsetFromPathFn is a typed function that +// abstracts fetching of clientset from kubeConfigPath +type getClientsetForPathFn func(kubeConfigPath string) (clientset *kubernetes.Clientset, err error) + +// listFn is a typed function that abstracts +// listing of storageclasses +type listFn func(cli *kubernetes.Clientset, opts metav1.ListOptions) (*storagev1.StorageClassList, error) + +// getFn is a typed function that abstracts +// fetching an instance of storageclass +type getFn func(cli *kubernetes.Clientset, name string, opts metav1.GetOptions) (*storagev1.StorageClass, error) + +// createFn is a typed function that abstracts +// to create storage class +type createFn func(cli *kubernetes.Clientset, sc *storagev1.StorageClass) (*storagev1.StorageClass, error) + +// deleteFn is a typed function that abstracts +// to delete storageclass +type deleteFn func(cli *kubernetes.Clientset, name string, opts *metav1.DeleteOptions) error + +// Kubeclient enables kubernetes API operations on storageclass instance +type Kubeclient struct { + // clientset refers to storageclass clientset + // that will be responsible to + // make kubernetes API calls + clientset *kubernetes.Clientset + + // kubeconfig path to get kubernetes clientset + kubeConfigPath string + + // functions useful during mocking + getClientset getClientsetFn + getClientsetForPath getClientsetForPathFn + list listFn + get getFn + create createFn + del deleteFn +} + +// KubeClientBuildOption defines the abstraction +// to build a kubeclient instance +type KubeClientBuildOption func(*Kubeclient) + +func (k *Kubeclient) withDefaults() { + if k.getClientset == nil { + k.getClientset = func() (clients *kubernetes.Clientset, err error) { + return client.New().Clientset() + } + } + if k.getClientsetForPath == nil { + k.getClientsetForPath = func(kubeConfigPath string) (clients *kubernetes.Clientset, err error) { + return client.New(client.WithKubeConfigPath(kubeConfigPath)).Clientset() + } + } + if k.list == nil { + k.list = func(cli *kubernetes.Clientset, opts metav1.ListOptions) (*storagev1.StorageClassList, error) { + return cli.StorageV1().StorageClasses().List(opts) + } + } + if k.get == nil { + k.get = func(cli *kubernetes.Clientset, name string, opts metav1.GetOptions) (*storagev1.StorageClass, error) { + return cli.StorageV1().StorageClasses().Get(name, opts) + } + } + if k.create == nil { + k.create = func(cli *kubernetes.Clientset, sc *storagev1.StorageClass) (*storagev1.StorageClass, error) { + return cli.StorageV1().StorageClasses().Create(sc) + } + } + if k.del == nil { + k.del = func(cli *kubernetes.Clientset, name string, opts *metav1.DeleteOptions) error { + return cli.StorageV1().StorageClasses().Delete(name, opts) + } + } +} + +// NewKubeClient returns a new instance of kubeclient meant for storageclass +func NewKubeClient(opts ...KubeClientBuildOption) *Kubeclient { + k := &Kubeclient{} + for _, o := range opts { + o(k) + } + k.withDefaults() + return k +} + +// WithClientSet sets the kubernetes client against +// the kubeclient instance +func WithClientSet(c *kubernetes.Clientset) KubeClientBuildOption { + return func(k *Kubeclient) { + k.clientset = c + } +} + +// WithKubeConfigPath sets the kubeConfig path +// against client instance +func WithKubeConfigPath(path string) KubeClientBuildOption { + return func(k *Kubeclient) { + k.kubeConfigPath = path + } +} + +func (k *Kubeclient) getClientsetForPathOrDirect() (*kubernetes.Clientset, error) { + if k.kubeConfigPath != "" { + return k.getClientsetForPath(k.kubeConfigPath) + } + return k.getClientset() +} + +// getClientsetOrCached returns either a new +// instance of kubernetes clientset or its +// cached copy cached copy +func (k *Kubeclient) getClientsetOrCached() (*kubernetes.Clientset, error) { + if k.clientset != nil { + return k.clientset, nil + } + + cs, err := k.getClientsetForPathOrDirect() + if err != nil { + return nil, errors.Wrapf(err, "failed to get clientset") + } + k.clientset = cs + return k.clientset, nil +} + +// List returns a list of storageclass instances present in kubernetes cluster +func (k *Kubeclient) List(opts metav1.ListOptions) (*storagev1.StorageClassList, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to list storageclasses") + } + return k.list(cli, opts) +} + +// Get return a storageclass instance present in kubernetes cluster +func (k *Kubeclient) Get(name string, opts metav1.GetOptions) (*storagev1.StorageClass, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to get storageclass {%s}", name) + } + return k.get(cli, name, opts) +} + +// Create creates and returns a storageclass instance +func (k *Kubeclient) Create(sc *storagev1.StorageClass) (*storagev1.StorageClass, error) { + if sc == nil { + return nil, errors.New("failed to create storageclass: nil storageclass object") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to create storageclass") + } + return k.create(cli, sc) +} + +// Delete deletes the storageclass if present in kubernetes cluster +func (k *Kubeclient) Delete(name string, opts *metav1.DeleteOptions) error { + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete storageclass: {%s}", name) + } + return k.del(cli, name, opts) +} diff --git a/tests/sc/storageclass.go b/tests/sc/storageclass.go new file mode 100644 index 0000000..8cc0b01 --- /dev/null +++ b/tests/sc/storageclass.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 The OpenEBS 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 sc + +import ( + storagev1 "k8s.io/api/storage/v1" +) + +// StorageClass is a wrapper over API based +// storage class instance +type StorageClass struct { + object *storagev1.StorageClass +} + +// StorageClassList holds the list of StorageClass instances +type StorageClassList struct { + items []*StorageClass +} + +// Predicate defines an abstraction +// to determine conditional checks +// against the provided StorageClass instance +type Predicate func(*StorageClass) bool + +// predicateList holds the list of predicates +type predicateList []Predicate + +// ToAPIList converts StorageClassList to API StorageClassList +func (scl *StorageClassList) ToAPIList() *storagev1.StorageClassList { + sclist := &storagev1.StorageClassList{} + for _, sc := range scl.items { + sc := sc // Pin it + sclist.Items = append(sclist.Items, *sc.object) + } + return sclist +} + +// all returns true if all the predicateList +// succeed against the provided StorageClass +// instance +func (l predicateList) all(sc *StorageClass) bool { + for _, pred := range l { + if !pred(sc) { + return false + } + } + return true +} + +// Len returns the number of items present in the StorageClassList +func (scl *StorageClassList) Len() int { + return len(scl.items) +} + +// NewForAPIObject returns a new instance of StorageClass +func NewForAPIObject(obj *storagev1.StorageClass) *StorageClass { + sc := &StorageClass{object: obj} + return sc +} diff --git a/tests/stringer/stringer.go b/tests/stringer/stringer.go new file mode 100644 index 0000000..06cb1af --- /dev/null +++ b/tests/stringer/stringer.go @@ -0,0 +1,63 @@ +/* +Copyright 2019 The OpenEBS 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 stringer + +import ( + "encoding/json" + "fmt" + "github.com/ghodss/yaml" +) + +// Yaml returns the provided object +// as a yaml formatted string +func Yaml(ctx string, obj interface{}) string { + if obj == nil { + return fmt.Sprintf("\n%s {nil}", ctx) + } + + str, ok := obj.(string) + if ok { + return fmt.Sprintf("\n%s {%s}", ctx, str) + } + + b, err := yaml.Marshal(obj) + if err != nil { + return fmt.Sprintf("\n%s {nil}", ctx) + } + + return fmt.Sprintf("\n%s {%s}", ctx, string(b)) +} + +// JSONIndent returns the provided object +// as a json indent string +func JSONIndent(ctx string, obj interface{}) string { + if obj == nil { + return fmt.Sprintf("\n%s {nil}", ctx) + } + + str, ok := obj.(string) + if ok { + return fmt.Sprintf("\n%s {%s}", ctx, str) + } + + b, err := json.MarshalIndent(obj, "", ".") + if err != nil { + return fmt.Sprintf("\n%s {nil}", ctx) + } + + return fmt.Sprintf("\n%s %s", ctx, string(b)) +} diff --git a/tests/suite_test.go b/tests/suite_test.go new file mode 100644 index 0000000..25361cd --- /dev/null +++ b/tests/suite_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2019 The OpenEBS 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 tests + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openebs/zfs-localpv/pkg/builder" + "github.com/openebs/zfs-localpv/tests/deploy" + "github.com/openebs/zfs-localpv/tests/pod" + "github.com/openebs/zfs-localpv/tests/pvc" + "github.com/openebs/zfs-localpv/tests/sc" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "os" + "testing" + + // auth plugins + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" +) + +const ( + // zfs pool name where volume provisioning will happen + POOLNAME = "zfspv-pool" +) + +var ( + ZFSClient *builder.Kubeclient + SCClient *sc.Kubeclient + PVCClient *pvc.Kubeclient + DeployClient *deploy.Kubeclient + PodClient *pod.KubeClient + openebsNamespace = "openebs" + nsName = "zfspv-provision" + scName = "zfspv-sc" + ZFSProvisioner = "zfs.csi.openebs.io" + pvcName = "zfspv-pvc" + appName = "busybox-zfspv" + + nsObj *corev1.Namespace + scObj *storagev1.StorageClass + deployObj *appsv1.Deployment + pvcObj *corev1.PersistentVolumeClaim + appPod *corev1.PodList + accessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} + capacity = "5368709120" // 5Gi + KubeConfigPath string +) + +func init() { + KubeConfigPath = os.Getenv("KUBECONFIG") + + SCClient = sc.NewKubeClient(sc.WithKubeConfigPath(KubeConfigPath)) + PVCClient = pvc.NewKubeClient(pvc.WithKubeConfigPath(KubeConfigPath)) + DeployClient = deploy.NewKubeClient(deploy.WithKubeConfigPath(KubeConfigPath)) + PodClient = pod.NewKubeClient(pod.WithKubeConfigPath(KubeConfigPath)) + ZFSClient = builder.NewKubeclient(builder.WithKubeConfigPath(KubeConfigPath)) +} + +func TestSource(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Test ZFSPV volume provisioning") +} diff --git a/tests/utils.go b/tests/utils.go new file mode 100644 index 0000000..eff926e --- /dev/null +++ b/tests/utils.go @@ -0,0 +1,294 @@ +/* +Copyright 2019 The OpenEBS 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 tests + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openebs/zfs-localpv/pkg/zfs" + "github.com/openebs/zfs-localpv/tests/container" + "github.com/openebs/zfs-localpv/tests/deploy" + "github.com/openebs/zfs-localpv/tests/k8svolume" + "github.com/openebs/zfs-localpv/tests/pod" + "github.com/openebs/zfs-localpv/tests/pts" + "github.com/openebs/zfs-localpv/tests/pvc" + "github.com/openebs/zfs-localpv/tests/sc" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// IsPVCBoundEventually checks if the pvc is bound or not eventually +func IsPVCBoundEventually(pvcName string) bool { + return Eventually(func() bool { + volume, err := PVCClient. + Get(pvcName, metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + return pvc.NewForAPIObject(volume).IsBound() + }, + 120, 10). + Should(BeTrue()) +} + +// IsPodRunningEventually return true if the pod comes to running state +func IsPodRunningEventually(namespace, podName string) bool { + return Eventually(func() bool { + p, err := PodClient. + WithNamespace(namespace). + Get(podName, metav1.GetOptions{}) + Expect(err).ShouldNot(HaveOccurred()) + return pod.NewForAPIObject(p). + IsRunning() + }, + 150, 10). + Should(BeTrue()) +} + +// IsPVCDeletedEventually tries to get the deleted pvc +// and returns true if pvc is not found +// else returns false +func IsPVCDeletedEventually(pvcName string) bool { + return Eventually(func() bool { + _, err := PVCClient. + Get(pvcName, metav1.GetOptions{}) + return k8serrors.IsNotFound(err) + }, + 120, 10). + Should(BeTrue()) +} + +func createExt4StorageClass() { + var ( + err error + ) + + parameters := map[string]string{ + "poolname": POOLNAME, + "fstype": "ext4", + } + + By("building a ext4 storage class") + scObj, err = sc.NewBuilder(). + WithGenerateName(scName). + WithParametersNew(parameters). + WithProvisioner(ZFSProvisioner).Build() + Expect(err).ShouldNot(HaveOccurred(), + "while building ext4 storageclass obj with prefix {%s}", scName) + + scObj, err = SCClient.Create(scObj) + Expect(err).To(BeNil(), "while creating a ext4 storageclass {%s}", scName) +} + +func createZfsStorageClass() { + var ( + err error + ) + + parameters := map[string]string{ + "poolname": POOLNAME, + "fstype": "zfs", + } + + By("building a zfs storage class") + scObj, err = sc.NewBuilder(). + WithGenerateName(scName). + WithParametersNew(parameters). + WithProvisioner(ZFSProvisioner).Build() + Expect(err).ShouldNot(HaveOccurred(), + "while building zfs storageclass obj with prefix {%s}", scName) + + scObj, err = SCClient.Create(scObj) + Expect(err).To(BeNil(), "while creating a zfs storageclass {%s}", scName) +} + +func VerifyZFSVolume() { + By("fetching zfs volume") + vol, err := ZFSClient.WithNamespace(openebsNamespace). + Get(pvcObj.Spec.VolumeName, metav1.GetOptions{}) + Expect(err).To(BeNil(), "while fetching the zfs volume {%s}", pvcObj.Spec.VolumeName) + + volType := zfs.VOLTYPE_ZVOL + if scObj.Parameters["fstype"] == zfs.FSTYPE_ZFS { + volType = zfs.VOLTYPE_DATASET + } + + By("verifying zfs volume") + Expect(vol.Spec.PoolName).To(Equal(scObj.Parameters["poolname"]), + "while checking poolname of zfs volume", pvcObj.Spec.VolumeName) + Expect(vol.Spec.FsType).To(Equal(scObj.Parameters["fstype"]), + "while checking fstype of zfs volume", pvcObj.Spec.VolumeName) + Expect(vol.Spec.VolumeType).To(Equal(volType), + "while checking Volume type as dataset", pvcObj.Spec.VolumeName) + Expect(vol.Spec.Capacity).To(Equal(capacity), + "while checking capacity of zfs volume", pvcObj.Spec.VolumeName) + + // it might fail if we are checking finializer before event is processed by node agent + Expect(vol.Finalizers[0]).To(Equal(zfs.ZFSFinalizer), "while checking finializer to be set {%s}", pvcObj.Spec.VolumeName) +} + +func deleteStorageClass() { + err := SCClient.Delete(scObj.Name, &metav1.DeleteOptions{}) + Expect(err).To(BeNil(), + "while deleting zfs storageclass {%s}", scObj.Name) +} + +func createAndVerifyPVC() { + var ( + err error + pvcName = "zfspv-pvc" + ) + By("building a pvc") + pvcObj, err = pvc.NewBuilder(). + WithName(pvcName). + WithNamespace(openebsNamespace). + WithStorageClass(scObj.Name). + WithAccessModes(accessModes). + WithCapacity(capacity).Build() + Expect(err).ShouldNot( + HaveOccurred(), + "while building pvc {%s} in namespace {%s}", + pvcName, + openebsNamespace, + ) + + By("creating above pvc") + pvcObj, err = PVCClient.WithNamespace(openebsNamespace).Create(pvcObj) + Expect(err).To( + BeNil(), + "while creating pvc {%s} in namespace {%s}", + pvcName, + openebsNamespace, + ) + + By("verifying pvc status as bound") + + status := IsPVCBoundEventually(pvcName) + Expect(status).To(Equal(true), + "while checking status equal to bound") + + pvcObj, err = PVCClient.WithNamespace(openebsNamespace).Get(pvcObj.Name, metav1.GetOptions{}) + Expect(err).To( + BeNil(), + "while retrieving pvc {%s} in namespace {%s}", + pvcName, + openebsNamespace, + ) +} + +func createDeployVerifyApp() { + By("creating and deploying app pod", createAndDeployAppPod) + time.Sleep(30 * time.Second) + By("verifying app pod is running", verifyAppPodRunning) +} + +func createAndDeployAppPod() { + var err error + By("building a busybox app pod deployment using above zfs volume") + deployObj, err = deploy.NewBuilder(). + WithName(appName). + WithNamespace(openebsNamespace). + WithLabelsNew( + map[string]string{ + "app": "busybox", + }, + ). + WithSelectorMatchLabelsNew( + map[string]string{ + "app": "busybox", + }, + ). + WithPodTemplateSpecBuilder( + pts.NewBuilder(). + WithLabelsNew( + map[string]string{ + "app": "busybox", + }, + ). + WithContainerBuilders( + container.NewBuilder(). + WithImage("busybox"). + WithName("busybox"). + WithImagePullPolicy(corev1.PullIfNotPresent). + WithCommandNew( + []string{ + "sh", + "-c", + "date > /mnt/datadir/date.txt; sync; sleep 5; sync; tail -f /dev/null;", + }, + ). + WithVolumeMountsNew( + []corev1.VolumeMount{ + corev1.VolumeMount{ + Name: "datavol1", + MountPath: "/mnt/datadir", + }, + }, + ), + ). + WithVolumeBuilders( + k8svolume.NewBuilder(). + WithName("datavol1"). + WithPVCSource(pvcObj.Name), + ), + ). + Build() + + Expect(err).ShouldNot(HaveOccurred(), "while building app deployement {%s}", appName) + + deployObj, err = DeployClient.WithNamespace(openebsNamespace).Create(deployObj) + Expect(err).ShouldNot( + HaveOccurred(), + "while creating pod {%s} in namespace {%s}", + appName, + openebsNamespace, + ) +} + +func verifyAppPodRunning() { + var err error + appPod, err = PodClient.WithNamespace(openebsNamespace). + List(metav1.ListOptions{ + LabelSelector: "app=busybox", + }, + ) + Expect(err).ShouldNot(HaveOccurred(), "while verifying application pod") + + status := IsPodRunningEventually(openebsNamespace, appPod.Items[0].Name) + Expect(status).To(Equal(true), "while checking status of pod {%s}", appPod.Items[0].Name) +} + +func deleteAppDeployment() { + err := DeployClient.WithNamespace(openebsNamespace). + Delete(deployObj.Name, &metav1.DeleteOptions{}) + Expect(err).ShouldNot(HaveOccurred(), "while deleting application pod") +} + +func deletePVC() { + err := PVCClient.WithNamespace(openebsNamespace).Delete(pvcName, &metav1.DeleteOptions{}) + Expect(err).To( + BeNil(), + "while deleting pvc {%s} in namespace {%s}", + pvcName, + openebsNamespace, + ) + By("verifying deleted pvc") + status := IsPVCDeletedEventually(pvcName) + Expect(status).To(Equal(true), "while trying to get deleted pvc") + +}