673 lines
19 KiB
Go
673 lines
19 KiB
Go
/*
|
|
Copyright 2018 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 verify
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strings"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/openebs/litmus/pkg/kubectl"
|
|
)
|
|
|
|
// VerifyFile type defines a yaml file path that represents an installation
|
|
// and is used for various verification purposes
|
|
//
|
|
// A verify file is a yaml version of Installation struct
|
|
type VerifyFile string
|
|
|
|
// Condition type defines a condition that can be applied against a component
|
|
// or a set of components
|
|
type Condition string
|
|
|
|
const (
|
|
// UniqueNodeCond is a condition to check uniqueness of node
|
|
UniqueNodeCond Condition = "is-unique-node"
|
|
// ThreeReplicasCond is a condition to check if replica count is 3
|
|
ThreeReplicasCond Condition = "is-three-replicas"
|
|
// PVCBoundCond is a condition to check if PVC is bound
|
|
PVCBoundCond Condition = "is-pvc-bound"
|
|
// PVCUnBoundCond is a condition to check if PVC is unbound
|
|
PVCUnBoundCond Condition = "is-pvc-unbound"
|
|
)
|
|
|
|
// Action type defines a action that can be applied against a component
|
|
// or a set of components
|
|
type Action string
|
|
|
|
const (
|
|
// DeleteAnyPodAction is an action to delete any pod
|
|
DeleteAnyPodAction Action = "delete-any-pod"
|
|
// DeleteOldestPodAction is an action to delete the oldest pod
|
|
DeleteOldestPodAction Action = "delete-oldest-pod"
|
|
)
|
|
|
|
// DeleteVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if an installation was deleted successfully
|
|
type DeleteVerifier interface {
|
|
IsDeleted() (yes bool, err error)
|
|
}
|
|
|
|
// DeployVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if an installation was deployed successfully
|
|
type DeployVerifier interface {
|
|
IsDeployed() (yes bool, err error)
|
|
}
|
|
|
|
// ConnectVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if a connection is possible or not
|
|
type ConnectVerifier interface {
|
|
IsConnected() (yes bool, err error)
|
|
}
|
|
|
|
// RunVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if an entity is in a running state or not
|
|
type RunVerifier interface {
|
|
IsRunning() (yes bool, err error)
|
|
}
|
|
|
|
// ConditionVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if specific entities passes the condition
|
|
type ConditionVerifier interface {
|
|
IsCondition(alias string, condition Condition) (yes bool, err error)
|
|
}
|
|
|
|
// ActionVerifier provides contract(s) i.e. method signature(s) to evaluate
|
|
// if specific entities passes the action
|
|
type ActionVerifier interface {
|
|
IsAction(alias string, action Action) (yes bool, err error)
|
|
}
|
|
|
|
// DeployRunVerifier provides contract(s) i.e. method signature(s) to
|
|
// evaluate:
|
|
//
|
|
// 1/ if an entity is deployed &,
|
|
// 2/ if the entity is running
|
|
type DeployRunVerifier interface {
|
|
// DeployVerifier will check if the instance has been deployed or not
|
|
DeployVerifier
|
|
// RunVerifier will check if the instance is in a running state or not
|
|
RunVerifier
|
|
}
|
|
|
|
// AllVerifier provides contract(s) i.e. method signature(s) to
|
|
// evaluate:
|
|
//
|
|
// - if an entity is deleted,
|
|
// - if an entity is deployed,
|
|
// - if the entity is running,
|
|
// - if the entity satisfies the provided condition &
|
|
// - if the entity satisfies the provided action
|
|
type AllVerifier interface {
|
|
// DeleteVerifier will check if the instance has been deleted or not
|
|
DeleteVerifier
|
|
// DeployVerifier will check if the instance has been deployed or not
|
|
DeployVerifier
|
|
// RunVerifier will check if the instance is in a running state or not
|
|
RunVerifier
|
|
// ConditionVerifier will check if the instance satisfies the provided
|
|
// condition
|
|
ConditionVerifier
|
|
// ActionVerifier will check if the instance satisfies the provided action
|
|
ActionVerifier
|
|
}
|
|
|
|
// Installation represents a set of components that represent an installation
|
|
// e.g. an operator represented by its CRDs, RBACs and Deployments forms an
|
|
// installation
|
|
//
|
|
// NOTE:
|
|
// Installation struct is accepted as a yaml file that is meant to be verified.
|
|
// In addition this file allows the testing logic to take appropriate actions
|
|
// as directed in the .feature file.
|
|
type Installation struct {
|
|
// VerifyID is an identifier that is used to tie together related installations
|
|
// meant to be verified
|
|
VerifyID string `json:"verifyID"`
|
|
// Version of this installation, operator etc
|
|
Version string `json:"version"`
|
|
// Components of this installation
|
|
Components []Component `json:"components"`
|
|
}
|
|
|
|
// Component is the information about a particular component
|
|
// e.g. a Kubernetes Deployment, or a Kubernetes Pod, etc can be
|
|
// a component in the overall installation
|
|
type Component struct {
|
|
// Name of the component
|
|
Name string `json:"name"`
|
|
// Namespace of the component
|
|
Namespace string `json:"namespace"`
|
|
// Kind name of the component
|
|
// e.g. pods, deployments, services, etc
|
|
Kind string `json:"kind"`
|
|
// APIVersion of the component
|
|
APIVersion string `json:"apiVersion"`
|
|
// Labels of the component that is used for filtering the components
|
|
//
|
|
// Following are some valid sample values for labels:
|
|
//
|
|
// labels: name=app
|
|
// labels: name=app,env=prod
|
|
Labels string `json:"labels"`
|
|
// Alias provides a user understood description used for filtering the
|
|
// components. This is a single word setting.
|
|
//
|
|
// NOTE:
|
|
// Ensure unique alias values in an installation
|
|
//
|
|
// DETAILS:
|
|
// This is the text which is typically understood by the end user. This text
|
|
// which will be set in the installation file against a particular component.
|
|
// Verification logic will filter the component based on this alias & run
|
|
// various checks &/or actions
|
|
Alias string `json:"alias"`
|
|
}
|
|
|
|
// unmarshal takes the raw yaml data and unmarshals it into Installation
|
|
func unmarshal(data []byte) (installation *Installation, err error) {
|
|
installation = &Installation{}
|
|
|
|
err = yaml.Unmarshal(data, installation)
|
|
return
|
|
}
|
|
|
|
// load converts a verify file into an instance of *Installation
|
|
func load(file VerifyFile) (installation *Installation, err error) {
|
|
if len(file) == 0 {
|
|
err = fmt.Errorf("failed to load: verify file is not provided")
|
|
return
|
|
}
|
|
|
|
d, err := ioutil.ReadFile(string(file))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return unmarshal(d)
|
|
}
|
|
|
|
// KubeInstallVerify provides methods that handles verification related logic of
|
|
// an installation within kubernetes e.g. application, deployment, operator, etc
|
|
type KubeInstallVerify struct {
|
|
// installation is the set of components that determine the install
|
|
installation *Installation
|
|
}
|
|
|
|
// NewKubeInstallVerify provides a new instance of NewKubeInstallVerify based on
|
|
// the provided kubernetes runner & verify file
|
|
func NewKubeInstallVerify(file VerifyFile) (*KubeInstallVerify, error) {
|
|
i, err := load(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &KubeInstallVerify{
|
|
installation: i,
|
|
}, nil
|
|
}
|
|
|
|
// IsDeployed evaluates if all components of the installation are deployed
|
|
func (v *KubeInstallVerify) IsDeployed() (yes bool, err error) {
|
|
if v.installation == nil {
|
|
err = fmt.Errorf("failed to check IsDeployed: installation object is nil")
|
|
return
|
|
}
|
|
|
|
for _, component := range v.installation.Components {
|
|
yes, err = v.isComponentDeployed(component)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// IsDeleted evaluates if all components of the installation are deleted
|
|
func (v *KubeInstallVerify) IsDeleted() (yes bool, err error) {
|
|
if v.installation == nil {
|
|
err = fmt.Errorf("failed to check IsDeleted: installation object is nil")
|
|
return
|
|
}
|
|
|
|
for _, component := range v.installation.Components {
|
|
yes, err = v.isComponentDeleted(component)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// IsRunning evaluates if all components of the installation are running
|
|
func (v *KubeInstallVerify) IsRunning() (yes bool, err error) {
|
|
if v.installation == nil {
|
|
err = fmt.Errorf("failed to check IsRunning: installation object is nil")
|
|
return
|
|
}
|
|
|
|
for _, component := range v.installation.Components {
|
|
if component.Kind != "pod" {
|
|
continue
|
|
}
|
|
|
|
yes, err = v.isPodComponentRunning(component)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// IsCondition evaluates if specific components satisfies the condition
|
|
func (v *KubeInstallVerify) IsCondition(alias string, condition Condition) (yes bool, err error) {
|
|
switch condition {
|
|
case UniqueNodeCond:
|
|
return v.isEachComponentOnUniqueNode(alias)
|
|
case ThreeReplicasCond:
|
|
return v.hasComponentThreeReplicas(alias)
|
|
case PVCBoundCond:
|
|
return v.isPVCBound(alias)
|
|
case PVCUnBoundCond:
|
|
return v.isPVCUnBound(alias)
|
|
default:
|
|
err = fmt.Errorf("condition '%s' is not supported", condition)
|
|
}
|
|
return
|
|
}
|
|
|
|
// IsAction evaluates if specific components satisfies the action
|
|
func (v *KubeInstallVerify) IsAction(alias string, action Action) (yes bool, err error) {
|
|
switch action {
|
|
case DeleteAnyPodAction:
|
|
return v.isDeleteAnyRunningPod(alias)
|
|
case DeleteOldestPodAction:
|
|
return v.isDeleteOldestRunningPod(alias)
|
|
default:
|
|
err = fmt.Errorf("action '%s' is not supported", action)
|
|
}
|
|
return
|
|
}
|
|
|
|
// isDeleteAnyPod deletes a pod based on the alias
|
|
func (v *KubeInstallVerify) isDeleteAnyRunningPod(alias string) (yes bool, err error) {
|
|
var pods = []string{}
|
|
|
|
c, err := v.getMatchingPodComponent(alias)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(strings.TrimSpace(c.Labels)) == 0 {
|
|
err = fmt.Errorf("unable to fetch component '%s': component labels are missing '%s'", c.Kind, alias)
|
|
return
|
|
}
|
|
|
|
k := kubectl.New().Namespace(c.Namespace).Labels(c.Labels)
|
|
pods, err = kubectl.GetRunningPods(k)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(pods) == 0 {
|
|
err = fmt.Errorf("failed to delete any running pod: pods with alias '%s' and running state are not found", alias)
|
|
return
|
|
}
|
|
|
|
// delete any running pod
|
|
k = kubectl.New().Namespace(c.Namespace)
|
|
err = kubectl.DeletePod(k, pods[0])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
yes = true
|
|
return
|
|
}
|
|
|
|
// isDeleteOldestRunningPod deletes the oldset pod based on the alias
|
|
func (v *KubeInstallVerify) isDeleteOldestRunningPod(alias string) (yes bool, err error) {
|
|
var pod string
|
|
|
|
c, err := v.getMatchingPodComponent(alias)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// check for presence of labels
|
|
if len(strings.TrimSpace(c.Labels)) == 0 {
|
|
err = fmt.Errorf("unable to fetch component '%s': component labels are missing '%s'", c.Kind, alias)
|
|
return
|
|
}
|
|
|
|
// fetch oldest running pod
|
|
k := kubectl.New().Namespace(c.Namespace).Labels(c.Labels)
|
|
pod, err = kubectl.GetOldestRunningPod(k)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(pod) == 0 {
|
|
err = fmt.Errorf("failed to delete oldest running pod: pod with alias '%s' and running state is not found", alias)
|
|
return
|
|
}
|
|
|
|
// delete oldest running pod
|
|
k = kubectl.New().Namespace(c.Namespace)
|
|
err = kubectl.DeletePod(k, pod)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
yes = true
|
|
return
|
|
}
|
|
|
|
func (v *KubeInstallVerify) getMatchingPodComponent(alias string) (comp Component, err error) {
|
|
var filtered = []Component{}
|
|
|
|
// filter the components that are pods & match with the provided alias
|
|
for _, c := range v.installation.Components {
|
|
if c.Alias == alias && kubectl.IsPod(c.Kind) {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
|
|
if len(filtered) == 0 {
|
|
err = fmt.Errorf("component not found for alias '%s'", alias)
|
|
return
|
|
}
|
|
|
|
// there should be only one component that matches the alias
|
|
if len(filtered) > 1 {
|
|
err = fmt.Errorf("multiple components found for alias '%s': alias should be unique in an install", alias)
|
|
return
|
|
}
|
|
|
|
return filtered[0], nil
|
|
}
|
|
|
|
// isComponentDeleted flags if a particular component is deleted
|
|
func (v *KubeInstallVerify) isComponentDeleted(component Component) (yes bool, err error) {
|
|
var op string
|
|
|
|
if len(strings.TrimSpace(component.Kind)) == 0 {
|
|
err = fmt.Errorf("unable to verify component delete status: component kind is missing")
|
|
return
|
|
}
|
|
|
|
// either name or labels is required
|
|
if len(strings.TrimSpace(component.Name)) == 0 && len(strings.TrimSpace(component.Labels)) == 0 {
|
|
err = fmt.Errorf("unable to verify component delete status: either component name or its labels is required")
|
|
return
|
|
}
|
|
|
|
// check via name
|
|
if len(strings.TrimSpace(component.Name)) != 0 {
|
|
op, err = kubectl.New().
|
|
Namespace(component.Namespace).
|
|
Run([]string{"get", component.Kind, component.Name})
|
|
|
|
if err == nil {
|
|
err = fmt.Errorf("component '%#v' is not deleted: output '%s'", component, op)
|
|
return
|
|
}
|
|
|
|
if strings.Contains(err.Error(), "(NotFound)") {
|
|
// yes, it is deleted
|
|
yes = true
|
|
// We wanted to make sure that this component was deleted.
|
|
// Hence the get operation is expected to result in NotFound error
|
|
// from server. Now we can reset the err to nil to let the flow
|
|
// continue
|
|
err = nil
|
|
return
|
|
}
|
|
|
|
err = fmt.Errorf("unable to verify delete status of component '%#v': output '%s'", component, op)
|
|
return
|
|
}
|
|
|
|
// or check via labels
|
|
op, err = kubectl.New().
|
|
Namespace(component.Namespace).
|
|
Labels(component.Labels).
|
|
Run([]string{"get", component.Kind})
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(strings.TrimSpace(op)) == 0 || strings.Contains(op, "No resources found") {
|
|
// yes, it is deleted
|
|
yes = true
|
|
return
|
|
}
|
|
|
|
err = fmt.Errorf("unable to verify delete status of component '%#v': output '%s'", component, op)
|
|
return
|
|
}
|
|
|
|
// isComponentDeployed flags if a particular component is deployed
|
|
func (v *KubeInstallVerify) isComponentDeployed(component Component) (yes bool, err error) {
|
|
var op string
|
|
|
|
if len(strings.TrimSpace(component.Kind)) == 0 {
|
|
err = fmt.Errorf("unable to verify component deploy status: component kind is missing")
|
|
return
|
|
}
|
|
|
|
// either name or labels is required
|
|
if len(strings.TrimSpace(component.Name)) == 0 && len(strings.TrimSpace(component.Labels)) == 0 {
|
|
err = fmt.Errorf("unable to verify component deploy status: either component name or its labels is required")
|
|
return
|
|
}
|
|
|
|
// check via name
|
|
if len(strings.TrimSpace(component.Name)) != 0 {
|
|
op, err = kubectl.New().
|
|
Namespace(component.Namespace).
|
|
Run([]string{"get", component.Kind, component.Name, "-o", "jsonpath='{.metadata.name}'"})
|
|
|
|
if err == nil && len(strings.TrimSpace(op)) != 0 {
|
|
// yes, it is deployed
|
|
yes = true
|
|
}
|
|
return
|
|
}
|
|
|
|
// or check via labels
|
|
op, err = kubectl.New().
|
|
Namespace(component.Namespace).
|
|
Labels(component.Labels).
|
|
Run([]string{"get", component.Kind, "-o", "jsonpath='{.items[*].metadata.name}'"})
|
|
|
|
if err == nil && len(strings.TrimSpace(op)) != 0 {
|
|
// yes, it is deployed
|
|
yes = true
|
|
}
|
|
return
|
|
}
|
|
|
|
// isPodComponentRunning flags if a particular component is running
|
|
func (v *KubeInstallVerify) isPodComponentRunning(component Component) (yes bool, err error) {
|
|
// either name or labels is required
|
|
if len(strings.TrimSpace(component.Name)) == 0 && len(strings.TrimSpace(component.Labels)) == 0 {
|
|
err = fmt.Errorf("unable to verify pod component running status: either name or its labels is required")
|
|
return
|
|
}
|
|
|
|
// check via name
|
|
if len(strings.TrimSpace(component.Name)) != 0 {
|
|
k := kubectl.New().Namespace(component.Namespace)
|
|
return kubectl.IsPodRunning(k, component.Name)
|
|
}
|
|
|
|
// or check via labels
|
|
k := kubectl.New().
|
|
Namespace(component.Namespace).
|
|
Labels(component.Labels)
|
|
return kubectl.ArePodsRunning(k)
|
|
}
|
|
|
|
// hasComponentThreeReplicas flags if a component has three replicas
|
|
func (v *KubeInstallVerify) hasComponentThreeReplicas(alias string) (yes bool, err error) {
|
|
err = fmt.Errorf("hasComponentThreeReplicas is not implemented")
|
|
return
|
|
}
|
|
|
|
// getPVCVolume fetches the PVC's volume
|
|
func (v *KubeInstallVerify) getPVCVolume(alias string) (op string, err error) {
|
|
var filtered = []Component{}
|
|
|
|
// filter the components based on the provided alias
|
|
for _, c := range v.installation.Components {
|
|
if c.Alias == alias {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
|
|
if len(filtered) == 0 {
|
|
err = fmt.Errorf("unable to check pvc bound status: no component with alias '%s'", alias)
|
|
return
|
|
}
|
|
|
|
if len(filtered) > 1 {
|
|
err = fmt.Errorf("unable to check pvc bound status: more than one components found with alias '%s'", alias)
|
|
return
|
|
}
|
|
|
|
if len(filtered[0].Name) == 0 {
|
|
err = fmt.Errorf("unable to check pvc bound status: component name is required: '%#v'", filtered[0])
|
|
return
|
|
}
|
|
|
|
if filtered[0].Kind != "pvc" {
|
|
err = fmt.Errorf("unable to check pvc bound status: component is not a pvc resource: '%#v'", filtered[0])
|
|
return
|
|
}
|
|
|
|
op, err = kubectl.New().
|
|
Namespace(filtered[0].Namespace).
|
|
Run([]string{"get", "pvc", filtered[0].Name, "-o", "jsonpath='{.spec.volumeName}'"})
|
|
|
|
return
|
|
}
|
|
|
|
// isPVCBound flags if a PVC component is bound
|
|
func (v *KubeInstallVerify) isPVCBound(alias string) (yes bool, err error) {
|
|
var vol string
|
|
vol, err = v.getPVCVolume(alias)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// if no vol then pvc is not bound
|
|
if len(strings.TrimSpace(vol)) == 0 {
|
|
err = fmt.Errorf("pvc component is not bound")
|
|
return
|
|
}
|
|
|
|
yes = true
|
|
return
|
|
}
|
|
|
|
// isPVCUnBound flags if a PVC component is unbound
|
|
func (v *KubeInstallVerify) isPVCUnBound(alias string) (yes bool, err error) {
|
|
var vol string
|
|
vol, err = v.getPVCVolume(alias)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// if no vol then pvc is not bound
|
|
if len(strings.TrimSpace(vol)) != 0 {
|
|
err = fmt.Errorf("pvc component is bound")
|
|
return
|
|
}
|
|
|
|
yes = true
|
|
return
|
|
}
|
|
|
|
// isEachComponentOnUniqueNode flags if each component is placed on unique node
|
|
func (v *KubeInstallVerify) isEachComponentOnUniqueNode(alias string) (bool, error) {
|
|
var filtered = []Component{}
|
|
var nodes = []string{}
|
|
|
|
// filter the components based on the provided alias
|
|
for _, c := range v.installation.Components {
|
|
if c.Alias == alias {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
|
|
// get the node of each filtered component
|
|
for _, f := range filtered {
|
|
// skip for non pod components
|
|
if !kubectl.IsPod(f.Kind) {
|
|
continue
|
|
}
|
|
|
|
// if pod then get the node on which it is running
|
|
if len(strings.TrimSpace(f.Labels)) == 0 {
|
|
return false, fmt.Errorf("unable to fetch component '%s' node: component labels are required", f.Kind)
|
|
}
|
|
|
|
k := kubectl.New().Namespace(f.Namespace).Labels(f.Labels)
|
|
n, err := kubectl.GetPodNodes(k)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
nodes = append(nodes, n...)
|
|
}
|
|
|
|
// check if condition is satisfied i.e. no duplicate nodes
|
|
exists := map[string]string{}
|
|
for _, n := range nodes {
|
|
if _, ok := exists[n]; ok {
|
|
return false, nil
|
|
}
|
|
exists[n] = "tracked"
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// KubeConnectionVerify provides methods that verifies connection to a kubernetes
|
|
// environment
|
|
type KubeConnectionVerify struct{}
|
|
|
|
// NewKubeConnectionVerify provides a new instance of KubeConnectionVerify
|
|
func NewKubeConnectionVerify() *KubeConnectionVerify {
|
|
return &KubeConnectionVerify{}
|
|
}
|
|
|
|
// IsConnected verifies if kubectl can connect to the target Kubernetes cluster
|
|
func (k *KubeConnectionVerify) IsConnected() (yes bool, err error) {
|
|
_, err = kubectl.New().Run([]string{"get", "pods"})
|
|
if err == nil {
|
|
yes = true
|
|
}
|
|
return
|
|
}
|