From 0e57532b49c88548515e42e72ae6431c24d0c789 Mon Sep 17 00:00:00 2001 From: Kiran Mova Date: Thu, 10 Dec 2020 23:47:27 +0530 Subject: [PATCH] feat(hostpath): allow custom node affinity label (#15) Ref: https://github.com/openebs/openebs/issues/2875 provide a feature for administrators to configure a custom node affinity label in place of hostnames. This will help in scenarios, where hostnames can change when node are removed and added back to the cluster with the underlying disks intact. cluster admin can setup custom labels to the nodes and provide this information to Local PV hostpath provisioner to use via StorageClass config key called `NodeAffinityLabel` ``` + //Example: Local PV device StorageClass for using a custom + //node label as: openebs.io/node-affinity-value + //will be as follows + // + // kind: StorageClass + // metadata: + // name: openebs-hostpath + // annotations: + // openebs.io/cas-type: local + // cas.openebs.io/config: | + // - name: StorageType + // value: "device" + // - name: NodeAffinityLabel + // value: "openebs.io/node-affinity-value" + // provisioner: openebs.io/local + // volumeBindingMode: WaitForFirstConsumer + // reclaimPolicy: Delete + // ``` Signed-off-by: kmova --- cmd/provisioner-localpv/app/config.go | 45 ++ .../app/helper_hostpath.go | 21 +- .../app/provisioner_hostpath.go | 66 +- deploy/kubectl/busybox-localpv-path.yaml | 33 + deploy/kubectl/fillup-localpv-hostpath.yaml | 33 + deploy/kubectl/openebs-lite-sc.yaml | 29 + deploy/kubectl/provisioner-hostpath.yaml | 143 +++++ go.mod | 2 +- go.sum | 116 ++++ .../api/core/v1/container/container.go | 442 ++++++++++++++ .../api/core/v1/container/container_test.go | 426 +++++++++++++ .../api/core/v1/persistentvolume/build.go | 224 +++++++ .../core/v1/persistentvolume/build_test.go | 357 +++++++++++ .../api/core/v1/persistentvolume/buildlist.go | 98 +++ .../v1/persistentvolume/buildlist_test.go | 106 ++++ .../core/v1/persistentvolume/kubernetes.go | 229 +++++++ .../v1/persistentvolume/kubernetes_test.go | 491 +++++++++++++++ .../v1/persistentvolume/persistentvolume.go | 210 +++++++ .../persistentvolume/persistentvolume_test.go | 32 + .../core/v1/persistentvolumeclaim/build.go | 193 ++++++ .../v1/persistentvolumeclaim/build_test.go | 371 +++++++++++ .../v1/persistentvolumeclaim/buildlist.go | 165 +++++ .../persistentvolumeclaim/buildlist_test.go | 159 +++++ .../v1/persistentvolumeclaim/kubernetes.go | 288 +++++++++ .../persistentvolumeclaim/kubernetes_test.go | 574 ++++++++++++++++++ .../persistentvolumeclaim.go | 116 ++++ .../persistentvolumeclaim_test.go | 46 ++ pkg/kubernetes/api/core/v1/pod/build.go | 251 ++++++++ pkg/kubernetes/api/core/v1/pod/buildlist.go | 82 +++ .../api/core/v1/pod/buildlist_test.go | 172 ++++++ pkg/kubernetes/api/core/v1/pod/kubernetes.go | 406 +++++++++++++ .../api/core/v1/pod/kubernetes_test.go | 470 ++++++++++++++ pkg/kubernetes/api/core/v1/pod/pod.go | 183 ++++++ pkg/kubernetes/api/core/v1/pod/pod_test.go | 105 ++++ .../v1/podtemplatespec/podtemplatespec.go | 507 ++++++++++++++++ .../podtemplatespec/podtemplatespec_test.go | 421 +++++++++++++ pkg/kubernetes/api/core/v1/volume/build.go | 195 ++++++ .../api/core/v1/volume/build_test.go | 207 +++++++ pkg/kubernetes/api/core/v1/volume/volume.go | 71 +++ pkg/kubernetes/client/client.go | 329 ++++++++++ pkg/kubernetes/client/client_test.go | 318 ++++++++++ 41 files changed, 8696 insertions(+), 36 deletions(-) create mode 100644 deploy/kubectl/busybox-localpv-path.yaml create mode 100644 deploy/kubectl/fillup-localpv-hostpath.yaml create mode 100644 deploy/kubectl/openebs-lite-sc.yaml create mode 100644 deploy/kubectl/provisioner-hostpath.yaml create mode 100644 pkg/kubernetes/api/core/v1/container/container.go create mode 100644 pkg/kubernetes/api/core/v1/container/container_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/build.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/build_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/buildlist.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/buildlist_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/kubernetes.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/kubernetes_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/build.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/build_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes_test.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim.go create mode 100644 pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim_test.go create mode 100644 pkg/kubernetes/api/core/v1/pod/build.go create mode 100644 pkg/kubernetes/api/core/v1/pod/buildlist.go create mode 100644 pkg/kubernetes/api/core/v1/pod/buildlist_test.go create mode 100644 pkg/kubernetes/api/core/v1/pod/kubernetes.go create mode 100644 pkg/kubernetes/api/core/v1/pod/kubernetes_test.go create mode 100644 pkg/kubernetes/api/core/v1/pod/pod.go create mode 100644 pkg/kubernetes/api/core/v1/pod/pod_test.go create mode 100644 pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec.go create mode 100644 pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec_test.go create mode 100644 pkg/kubernetes/api/core/v1/volume/build.go create mode 100644 pkg/kubernetes/api/core/v1/volume/build_test.go create mode 100644 pkg/kubernetes/api/core/v1/volume/volume.go create mode 100644 pkg/kubernetes/client/client.go create mode 100644 pkg/kubernetes/client/client_test.go diff --git a/cmd/provisioner-localpv/app/config.go b/cmd/provisioner-localpv/app/config.go index eac37cb5..e7346161 100644 --- a/cmd/provisioner-localpv/app/config.go +++ b/cmd/provisioner-localpv/app/config.go @@ -78,6 +78,29 @@ const ( // KeyBDTag = "BlockDeviceTag" + //KeyNodeAffinityLabel defines the label key that should be + //used in the nodeAffinitySpec. Default is to use "kubernetes.io/hostname" + // + //Example: Local PV device StorageClass for using a custom + //node label as: openebs.io/node-affinity-value + //will be as follows + // + // kind: StorageClass + // metadata: + // name: openebs-device-tag-x + // annotations: + // openebs.io/cas-type: local + // cas.openebs.io/config: | + // - name: StorageType + // value: "device" + // - name: NodeAffinityLabel + // value: "openebs.io/node-affinity-value" + // provisioner: openebs.io/local + // volumeBindingMode: WaitForFirstConsumer + // reclaimPolicy: Delete + // + KeyNodeAffinityLabel = "NodeAffinityLabel" + //KeyPVRelativePath defines the alternate folder name under the BasePath // By default, the pv name will be used as the folder name. // KeyPVBasePath can be useful for providing the same underlying folder @@ -188,6 +211,18 @@ func (c *VolumeConfig) GetBDTagValue() string { return bdTagValue } +//GetNodeAffinityLabelKey returns the custom node affinity +//label key as configured in StorageClass. +// +//Default is "", use the standard kubernetes.io/hostname label. +func (c *VolumeConfig) GetNodeAffinityLabelKey() string { + nodeAffinityLabelKey := c.getValue(KeyNodeAffinityLabel) + if len(strings.TrimSpace(nodeAffinityLabelKey)) == 0 { + return "" + } + return nodeAffinityLabelKey +} + //GetPath returns a valid PV path based on the configuration // or an error. The Path is constructed using the following rules: // If AbsolutePath is specified return it. (Future) @@ -277,6 +312,16 @@ func GetNodeHostname(n *v1.Node) string { return hostname } +// GetNodeLabelValue extracts the value from the given label on the Node +// If specificed label is not present an empty string is returned. +func GetNodeLabelValue(n *v1.Node, labelKey string) string { + labelValue, found := n.Labels[labelKey] + if !found { + return "" + } + return labelValue +} + // GetTaints extracts the Taints from the Spec on the node // If Taints are empty, it just returns empty structure of corev1.Taints func GetTaints(n *v1.Node) []v1.Taint { diff --git a/cmd/provisioner-localpv/app/helper_hostpath.go b/cmd/provisioner-localpv/app/helper_hostpath.go index 5737e675..c9ce9abb 100644 --- a/cmd/provisioner-localpv/app/helper_hostpath.go +++ b/cmd/provisioner-localpv/app/helper_hostpath.go @@ -28,9 +28,9 @@ import ( hostpath "github.com/openebs/maya/pkg/hostpath/v1alpha1" - container "github.com/openebs/maya/pkg/kubernetes/container/v1alpha1" - pod "github.com/openebs/maya/pkg/kubernetes/pod/v1alpha1" - volume "github.com/openebs/maya/pkg/kubernetes/volume/v1alpha1" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/container" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/pod" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/volume" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -52,8 +52,11 @@ var ( // to execute a command (cmdsForPath) on a given // volume path (path) type HelperPodOptions struct { - //nodeHostname represents the hostname of the node where pod should be launched. - nodeHostname string + //nodeAffinityLabelKey represents the label key of the node where pod should be launched. + nodeAffinityLabelKey string + + //nodeAffinityLabelValue represents the label value of the node where pod should be launched. + nodeAffinityLabelValue string //name is the name of the PV for which the pod is being launched name string @@ -78,7 +81,8 @@ type HelperPodOptions struct { func (pOpts *HelperPodOptions) validate() error { if pOpts.name == "" || pOpts.path == "" || - pOpts.nodeHostname == "" || + pOpts.nodeAffinityLabelKey == "" || + pOpts.nodeAffinityLabelValue == "" || pOpts.serviceAccountName == "" { return errors.Errorf("invalid empty name or hostpath or hostname or service account name") } @@ -165,9 +169,10 @@ func (p *Provisioner) launchPod(config podConfig) (*corev1.Pod, error) { privileged := true helperPod, err := pod.NewBuilder(). - WithName(config.podName + "-" + config.pOpts.name). + WithName(config.podName+"-"+config.pOpts.name). WithRestartPolicy(corev1.RestartPolicyNever). - WithNodeSelectorHostnameNew(config.pOpts.nodeHostname). + //WithNodeSelectorHostnameNew(config.pOpts.nodeHostname). + WithNodeAffinityNew(config.pOpts.nodeAffinityLabelKey, config.pOpts.nodeAffinityLabelValue). WithServiceAccountName(config.pOpts.serviceAccountName). WithTolerationsForTaints(config.taints...). WithContainerBuilder( diff --git a/cmd/provisioner-localpv/app/provisioner_hostpath.go b/cmd/provisioner-localpv/app/provisioner_hostpath.go index dbe053fe..7d76c3d7 100644 --- a/cmd/provisioner-localpv/app/provisioner_hostpath.go +++ b/cmd/provisioner-localpv/app/provisioner_hostpath.go @@ -26,20 +26,25 @@ import ( pvController "sigs.k8s.io/sig-storage-lib-external-provisioner/controller" //pvController "github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/controller" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/persistentvolume" mconfig "github.com/openebs/maya/pkg/apis/openebs.io/v1alpha1" - persistentvolume "github.com/openebs/maya/pkg/kubernetes/persistentvolume/v1alpha1" ) // ProvisionHostPath is invoked by the Provisioner which expect HostPath PV // to be provisioned and a valid PV spec returned. func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volumeConfig *VolumeConfig) (*v1.PersistentVolume, error) { pvc := opts.PVC - nodeHostname := GetNodeHostname(opts.SelectedNode) taints := GetTaints(opts.SelectedNode) name := opts.PVName stgType := volumeConfig.GetStorageType() saName := getOpenEBSServiceAccountName() + nodeAffinityKey := volumeConfig.GetNodeAffinityLabelKey() + if len(nodeAffinityKey) == 0 { + nodeAffinityKey = k8sNodeLabelKeyHostname + } + nodeAffinityValue := GetNodeLabelValue(opts.SelectedNode, nodeAffinityKey) + path, err := volumeConfig.GetPath() if err != nil { alertlog.Logger.Errorw("", @@ -52,17 +57,18 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu return nil, err } - klog.Infof("Creating volume %v at %v:%v", name, nodeHostname, path) + klog.Infof("Creating volume %v at node with label %v=%v, path:%v", name, nodeAffinityKey, nodeAffinityValue, path) //Before using the path for local PV, make sure it is created. initCmdsForPath := []string{"mkdir", "-m", "0777", "-p"} podOpts := &HelperPodOptions{ - cmdsForPath: initCmdsForPath, - name: name, - path: path, - nodeHostname: nodeHostname, - serviceAccountName: saName, - selectedNodeTaints: taints, + cmdsForPath: initCmdsForPath, + name: name, + path: path, + nodeAffinityLabelKey: nodeAffinityKey, + nodeAffinityLabelValue: nodeAffinityValue, + serviceAccountName: saName, + selectedNodeTaints: taints, } iErr := p.createInitPod(podOpts) if iErr != nil { @@ -104,7 +110,7 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu WithVolumeMode(fs). WithCapacityQty(pvc.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]). WithLocalHostDirectory(path). - WithNodeAffinity(nodeHostname). + WithNodeAffinity(nodeAffinityKey, nodeAffinityValue). Build() if err != nil { @@ -126,12 +132,11 @@ func (p *Provisioner) ProvisionHostPath(opts pvController.ProvisionOptions, volu return pvObj, nil } -// GetNodeObjectFromHostName returns the Node Object with matching NodeHostName. -func (p *Provisioner) GetNodeObjectFromHostName(hostName string) (*v1.Node, error) { - labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{persistentvolume.KeyNode: hostName}} +// GetNodeObjectFromLabels returns the Node Object with matching label key and value +func (p *Provisioner) GetNodeObjectFromLabels(key, value string) (*v1.Node, error) { + labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{key: value}} listOptions := metav1.ListOptions{ LabelSelector: labels.Set(labelSelector.MatchLabels).String(), - Limit: 1, } nodeList, err := p.kubeClient.CoreV1().Nodes().List(listOptions) if err != nil || len(nodeList.Items) == 0 { @@ -139,7 +144,13 @@ func (p *Provisioner) GetNodeObjectFromHostName(hostName string) (*v1.Node, erro // based on kubernetes.io/hostname label, either: // - hostname label changed on the node or // - the node is deleted from the cluster. - return nil, errors.Errorf("Unable to get the Node with the NodeHostName [%s]", hostName) + return nil, errors.Errorf("Unable to get the Node with the Node Label %s [%s]", key, value) + } + if len(nodeList.Items) != 1 { + // After the PV is created and node affinity is set + // on a custom affinity label, there may be a transitory state + // with two nodes matching (old and new) label. + return nil, errors.Errorf("Unable to determine the Node. Found multiple nodes matching the labels %s [%s].", key, value) } return &nodeList.Items[0], nil @@ -162,28 +173,29 @@ func (p *Provisioner) DeleteHostPath(pv *v1.PersistentVolume) (err error) { return errors.Errorf("no HostPath set") } - hostname := pvObj.GetAffinitedNodeHostname() - if hostname == "" { - return errors.Errorf("cannot find affinited node hostname") + nodeAffinityKey, nodeAffinityValue := pvObj.GetAffinitedNodeLabelKeyAndValue() + if nodeAffinityValue == "" { + return errors.Errorf("cannot find affinited node details") } - alertlog.Logger.Infof("Get the Node Object from hostName: %v", hostname) + alertlog.Logger.Infof("Get the Node Object with label %v : %v", nodeAffinityKey, nodeAffinityValue) //Get the node Object once again to get updated Taints. - nodeObject, err := p.GetNodeObjectFromHostName(hostname) + nodeObject, err := p.GetNodeObjectFromLabels(nodeAffinityKey, nodeAffinityValue) if err != nil { return err } taints := GetTaints(nodeObject) //Initiate clean up only when reclaim policy is not retain. - klog.Infof("Deleting volume %v at %v:%v", pv.Name, hostname, path) + klog.Infof("Deleting volume %v at %v:%v", pv.Name, GetNodeHostname(nodeObject), path) cleanupCmdsForPath := []string{"rm", "-rf"} podOpts := &HelperPodOptions{ - cmdsForPath: cleanupCmdsForPath, - name: pv.Name, - path: path, - nodeHostname: hostname, - serviceAccountName: saName, - selectedNodeTaints: taints, + cmdsForPath: cleanupCmdsForPath, + name: pv.Name, + path: path, + nodeAffinityLabelKey: nodeAffinityKey, + nodeAffinityLabelValue: nodeAffinityValue, + serviceAccountName: saName, + selectedNodeTaints: taints, } if err := p.createCleanupPod(podOpts); err != nil { diff --git a/deploy/kubectl/busybox-localpv-path.yaml b/deploy/kubectl/busybox-localpv-path.yaml new file mode 100644 index 00000000..86d6f62e --- /dev/null +++ b/deploy/kubectl/busybox-localpv-path.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Pod +metadata: + name: busybox + namespace: default +spec: + containers: + - command: + - sh + - -c + - 'date >> /mnt/store1/date.txt; hostname >> /mnt/store1/hostname.txt; sync; sleep 5; sync; tail -f /dev/null;' + image: busybox + imagePullPolicy: Always + name: busybox + volumeMounts: + - mountPath: /mnt/store1 + name: demo-vol1 + volumes: + - name: demo-vol1 + persistentVolumeClaim: + claimName: demo-vol1-claim +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: demo-vol1-claim +spec: + storageClassName: openebs-hostpath + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5G diff --git a/deploy/kubectl/fillup-localpv-hostpath.yaml b/deploy/kubectl/fillup-localpv-hostpath.yaml new file mode 100644 index 00000000..bcb282b4 --- /dev/null +++ b/deploy/kubectl/fillup-localpv-hostpath.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Pod +metadata: + name: fillup + namespace: default +spec: + containers: + - command: + - sh + - -c + - 'dd if=/dev/zero of=/mnt/store1/dump.dd bs=1M; sync; sleep 5; sync; tail -f /dev/null;' + image: busybox + imagePullPolicy: Always + name: fillup-bb + volumeMounts: + - mountPath: /mnt/store1 + name: fillup + volumes: + - name: fillup + persistentVolumeClaim: + claimName: fillup-claim +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: fillup-claim +spec: + storageClassName: openebs-hostpath + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1G diff --git a/deploy/kubectl/openebs-lite-sc.yaml b/deploy/kubectl/openebs-lite-sc.yaml new file mode 100644 index 00000000..f7ed17ec --- /dev/null +++ b/deploy/kubectl/openebs-lite-sc.yaml @@ -0,0 +1,29 @@ +#Sample storage classes for OpenEBS Local PV +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: openebs-hostpath + annotations: + openebs.io/cas-type: local + cas.openebs.io/config: | + #hostpath type will create a PV by + # creating a sub-directory under the + # BASEPATH provided below. + - name: StorageType + value: "hostpath" + #Specify the location (directory) where + # where PV(volume) data will be saved. + # A sub-directory with pv-name will be + # created. When the volume is deleted, + # the PV sub-directory will be deleted. + #Default value is /var/openebs/local + - name: BasePath + value: "/var/openebs/local/" + #Specify the node affinity label + # to be added to the PV + #Default: kubernetes.io/hostname + #- name: NodeAffinityLabel + # value: "openebs.io/stg-node-name" +provisioner: openebs.io/local +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Delete diff --git a/deploy/kubectl/provisioner-hostpath.yaml b/deploy/kubectl/provisioner-hostpath.yaml new file mode 100644 index 00000000..9e448e6f --- /dev/null +++ b/deploy/kubectl/provisioner-hostpath.yaml @@ -0,0 +1,143 @@ +# This manifest deploys the OpenEBS control plane components, with associated CRs & RBAC rules +# NOTE: On GKE, deploy the openebs-operator.yaml in admin context + +# Create the OpenEBS namespace +apiVersion: v1 +kind: Namespace +metadata: + name: openebs +--- +# Create Maya Service Account +apiVersion: v1 +kind: ServiceAccount +metadata: + name: openebs-maya-operator + namespace: openebs +--- +# Define Role that allows operations on K8s pods/deployments +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: openebs-maya-operator +rules: +- apiGroups: ["*"] + resources: ["nodes", "nodes/proxy"] + verbs: ["*"] +- apiGroups: ["*"] + resources: ["namespaces", "services", "pods", "pods/exec", "deployments", "deployments/finalizers", "replicationcontrollers", "replicasets", "events", "endpoints", "configmaps", "secrets", "jobs", "cronjobs"] + verbs: ["*"] +- apiGroups: ["*"] + resources: ["statefulsets", "daemonsets"] + verbs: ["*"] +- apiGroups: ["*"] + resources: ["resourcequotas", "limitranges"] + verbs: ["list", "watch"] +- apiGroups: ["*"] + resources: ["ingresses", "horizontalpodautoscalers", "verticalpodautoscalers", "poddisruptionbudgets", "certificatesigningrequests"] + verbs: ["list", "watch"] +- apiGroups: ["*"] + resources: ["storageclasses", "persistentvolumeclaims", "persistentvolumes"] + verbs: ["*"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: [ "get", "list", "create", "update", "delete", "patch"] +- apiGroups: ["openebs.io"] + resources: [ "*"] + verbs: ["*"] +- nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +# Bind the Service Account with the Role Privileges. +# TODO: Check if default account also needs to be there +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: openebs-maya-operator +subjects: +- kind: ServiceAccount + name: openebs-maya-operator + namespace: openebs +roleRef: + kind: ClusterRole + name: openebs-maya-operator + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openebs-localpv-provisioner + namespace: openebs + labels: + name: openebs-localpv-provisioner + openebs.io/component-name: openebs-localpv-provisioner + openebs.io/version: dev +spec: + selector: + matchLabels: + name: openebs-localpv-provisioner + openebs.io/component-name: openebs-localpv-provisioner + replicas: 1 + strategy: + type: Recreate + template: + metadata: + labels: + name: openebs-localpv-provisioner + openebs.io/component-name: openebs-localpv-provisioner + openebs.io/version: dev + spec: + serviceAccountName: openebs-maya-operator + containers: + - name: openebs-provisioner-hostpath + imagePullPolicy: IfNotPresent + image: openebs/provisioner-localpv-ci:dev-120403 + env: + # OPENEBS_IO_K8S_MASTER enables openebs provisioner to connect to K8s + # based on this address. This is ignored if empty. + # This is supported for openebs provisioner version 0.5.2 onwards + #- name: OPENEBS_IO_K8S_MASTER + # value: "http://10.128.0.12:8080" + # OPENEBS_IO_KUBE_CONFIG enables openebs provisioner to connect to K8s + # based on this config. This is ignored if empty. + # This is supported for openebs provisioner version 0.5.2 onwards + #- name: OPENEBS_IO_KUBE_CONFIG + # value: "/home/ubuntu/.kube/config" + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: OPENEBS_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # OPENEBS_SERVICE_ACCOUNT provides the service account of this pod as + # environment variable + - name: OPENEBS_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: OPENEBS_IO_ENABLE_ANALYTICS + value: "true" + - name: OPENEBS_IO_INSTALLER_TYPE + value: "openebs-operator-lite" + - name: OPENEBS_IO_HELPER_IMAGE + value: "openebs/linux-utils:2.3.0" + # LEADER_ELECTION_ENABLED is used to enable/disable leader election. By default + # leader election is enabled. + #- name: LEADER_ELECTION_ENABLED + # value: "true" + # Process name used for matching is limited to the 15 characters + # present in the pgrep output. + # So fullname can't be used here with pgrep (>15 chars).A regular expression + # that matches the entire command name has to specified. + # Anchor `^` : matches any string that starts with `provisioner-loc` + # `.*`: matches any string that has `provisioner-loc` followed by zero or more char + livenessProbe: + exec: + command: + - sh + - -c + - test `pgrep -c "^provisioner-loc.*"` = 1 + initialDelaySeconds: 30 + periodSeconds: 60 +--- diff --git a/go.mod b/go.mod index ce850c41..b2758148 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.13 require ( github.com/openebs/maya v1.12.1 github.com/pkg/errors v0.9.1 - github.com/spf13/cobra v0.0.5 + github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 k8s.io/api v0.17.3 k8s.io/apimachinery v0.17.3 diff --git a/go.sum b/go.sum index cd0a3b25..38a6415d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,16 @@ bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1: cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= @@ -28,6 +38,7 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -44,6 +55,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= @@ -58,12 +71,14 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= @@ -77,7 +92,9 @@ github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/coredns/corefile-migration v1.0.4/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -87,7 +104,9 @@ github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -96,6 +115,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -132,9 +152,11 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0 github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= @@ -205,9 +227,12 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -247,11 +272,13 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= @@ -260,19 +287,38 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/heketi/heketi v9.0.1-0.20190917153846-c2e2a4ab7ab9+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o= github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -349,17 +395,23 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4= github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -384,6 +436,7 @@ github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -406,6 +459,7 @@ github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/ github.com/opencontainers/selinux v1.3.1-0.20190929122143-5215b1806f52/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= github.com/openebs/maya v1.12.1 h1:LeuCkpC8mf3ntWetZ/K89gXCdNKmgFJYLMs3HBOXxFY= github.com/openebs/maya v1.12.1/go.mod h1:E9CmKbURtsthTyASz0piTxljLmGxjbaJ3aFhtWEko2Y= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -418,19 +472,25 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= @@ -441,24 +501,30 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -468,6 +534,8 @@ github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -478,6 +546,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -487,12 +556,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= @@ -509,13 +580,16 @@ github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59b github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -530,6 +604,7 @@ go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1 golang.org/x/build v0.0.0-20190927031335-2835ba2e683f/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -539,23 +614,32 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -563,8 +647,10 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -573,8 +659,10 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -598,9 +686,11 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -613,9 +703,12 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -653,17 +746,25 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -672,17 +773,30 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6d gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -699,6 +813,7 @@ gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -770,6 +885,7 @@ modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/sig-storage-lib-external-provisioner v4.1.0+incompatible h1:pp7GUmQZKI57EjGnjkY88V4QbVuMpkw/ijKXqL67EsI= sigs.k8s.io/sig-storage-lib-external-provisioner v4.1.0+incompatible/go.mod h1:qhqLyNwJC49PoUalmtzYb4s9fT8HOMBTLbTY1QoVOqI= diff --git a/pkg/kubernetes/api/core/v1/container/container.go b/pkg/kubernetes/api/core/v1/container/container.go new file mode 100644 index 00000000..841f26c0 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/container/container.go @@ -0,0 +1,442 @@ +/* +Copyright 2020 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 ( + errors "github.com/pkg/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 +} + +// WithVolumeDevices builds the containers with the appropriate volumeDevices +func (b *Builder) WithVolumeDevices(volumeDevices []corev1.VolumeDevice) *Builder { + if volumeDevices == nil { + b.errors = append( + b.errors, + errors.New("failed to build container object: nil volumedevices"), + ) + return b + } + if len(volumeDevices) == 0 { + b.errors = append( + b.errors, + errors.New("failed to build container object: missing volumedevices"), + ) + return b + } + newVolumeDevices := []corev1.VolumeDevice{} + newVolumeDevices = append(newVolumeDevices, volumeDevices...) + b.con.VolumeDevices = newVolumeDevices + 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/pkg/kubernetes/api/core/v1/container/container_test.go b/pkg/kubernetes/api/core/v1/container/container_test.go new file mode 100644 index 00000000..9da78b9f --- /dev/null +++ b/pkg/kubernetes/api/core/v1/container/container_test.go @@ -0,0 +1,426 @@ +/* +Copyright 2020 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 ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" +) + +// fakeAlwaysTrue is a concrete implementation of container Predicate +func fakeAlwaysTrue(d *container) (string, bool) { + return "fakeAlwaysTrue", true +} + +// fakeAlwaysFalse is a concrete implementation of container Predicate +func fakeAlwaysFalse(d *container) (string, bool) { + return "fakeAlwaysFalse", false +} + +func TestPredicateFailedError(t *testing.T) { + tests := map[string]struct { + predicateMessage string + expectedErr string + }{ + "always true": {"fakeAlwaysTrue", "predicatefailed: fakeAlwaysTrue"}, + "always false": {"fakeAlwaysFalse", "predicatefailed: fakeAlwaysFalse"}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + e := predicateFailedError(mock.predicateMessage) + if e.Error() != mock.expectedErr { + t.Fatalf( + "test '%s' failed: expected '%s': actual '%s'", + name, + mock.expectedErr, + e.Error(), + ) + } + }) + } +} + +func TestNewWithName(t *testing.T) { + n := "con1" + c := New(WithName(n)) + if c.Name != n { + t.Fatalf("test failed: expected name '%s': actual '%s'", n, c.Name) + } +} + +func TestNewWithImage(t *testing.T) { + i := "openebs.io/m-apiserver:1.0.0" + c := New(WithImage(i)) + if c.Image != i { + t.Fatalf("test failed: expected image '%s': actual '%s'", i, c.Image) + } +} + +func TestBuilderBuild(t *testing.T) { + _, err := NewBuilder().Build() + if err != nil { + t.Fatalf("test failed: expected no err: actual '%+v'", err) + } +} + +func TestBuilderValidation(t *testing.T) { + tests := map[string]struct { + checks []Predicate + isError bool + }{ + "always true": {[]Predicate{fakeAlwaysTrue}, false}, + "always false": {[]Predicate{fakeAlwaysFalse}, true}, + "true & false": {[]Predicate{fakeAlwaysTrue, fakeAlwaysFalse}, true}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + _, err := NewBuilder().AddChecks(mock.checks).Build() + if mock.isError && err == nil { + t.Fatalf( + "test '%s' failed: expected error: actual no error", + name, + ) + } + if !mock.isError && err != nil { + t.Fatalf( + "test '%s' failed: expected no error: actual error '%+v'", + name, + err, + ) + } + }) + } +} + +func TestBuilderAddChecks(t *testing.T) { + tests := map[string]struct { + checks []Predicate + expectedCount int + }{ + "zero": {[]Predicate{}, 0}, + "one": {[]Predicate{fakeAlwaysTrue}, 1}, + "two": {[]Predicate{fakeAlwaysTrue, fakeAlwaysFalse}, 2}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + b := NewBuilder().AddChecks(mock.checks) + if len(b.checks) != mock.expectedCount { + t.Fatalf( + "test '%s' failed: expected no of checks '%d': actual '%d'", + name, + mock.expectedCount, + len(b.checks), + ) + } + }) + } +} + +func TestBuilderWithName(t *testing.T) { + tests := map[string]struct { + name string + expectedName string + }{ + "t1": {"nginx", "nginx"}, + "t2": {"maya", "maya"}, + "t3": {"ndm", "ndm"}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + c, _ := NewBuilder().WithName(mock.name).Build() + if c.Name != mock.expectedName { + t.Fatalf( + "test '%s' failed: expected name '%s': actual '%s'", + name, + mock.expectedName, + c.Name, + ) + } + }) + } +} + +func TestBuilderWithImage(t *testing.T) { + tests := map[string]struct { + image string + expectedImage string + }{ + "t1": {"nginx:1.0.0", "nginx:1.0.0"}, + "t2": {"openebs.io/maya:1.0", "openebs.io/maya:1.0"}, + "t3": {"openebs.io/ndm:1.0", "openebs.io/ndm:1.0"}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + c, _ := NewBuilder().WithImage(mock.image).Build() + if c.Image != mock.expectedImage { + t.Fatalf( + "test '%s' failed: expected image '%s': actual '%s'", + name, + mock.expectedImage, + c.Image, + ) + } + }) + } +} + +func TestBuilderWithCommandNew(t *testing.T) { + tests := map[string]struct { + cmd []string + expectedCmd []string + }{ + "t1": { + []string{"kubectl", "get", "po"}, + []string{"kubectl", "get", "po"}, + }, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + c, _ := NewBuilder().WithCommandNew(mock.cmd).Build() + if !reflect.DeepEqual(c.Command, mock.expectedCmd) { + t.Fatalf( + "test '%s' failed: expected command '%q': actual '%q'", + name, + mock.expectedCmd, + c.Command, + ) + } + }) + } +} + +func TestBuilderWithArgumentsNew(t *testing.T) { + tests := map[string]struct { + args []string + expectedArgs []string + }{ + "t1": { + []string{"-jsonpath", "metadata.name"}, + []string{"-jsonpath", "metadata.name"}, + }, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + c, _ := NewBuilder().WithArgumentsNew(mock.args).Build() + if !reflect.DeepEqual(c.Args, mock.expectedArgs) { + t.Fatalf( + "test '%s' failed: expected arguments '%q': actual '%q'", + name, + mock.expectedArgs, + c.Args, + ) + } + }) + } +} + +func TestBuilderWithPrivilegedSecurityContext(t *testing.T) { + tests := map[string]struct { + secCont bool + builder *Builder + expectErr bool + }{ + "Test Builder with templateSpec": { + secCont: true, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + "Test Builder without templateSpec": { + secCont: false, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithPrivilegedSecurityContext(&mock.secCont) + if mock.expectErr && len(b.errors) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errors) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithEnvsNew(t *testing.T) { + tests := map[string]struct { + envList []corev1.EnvVar + builder *Builder + expectErr bool + }{ + "Test Builder with envList": { + envList: []corev1.EnvVar{ + corev1.EnvVar{}, + }, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + "Test Builder without envList": { + envList: nil, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithEnvsNew(mock.envList) + if mock.expectErr && len(b.errors) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errors) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithPortsNew(t *testing.T) { + tests := map[string]struct { + portList []corev1.ContainerPort + builder *Builder + expectErr bool + }{ + "Test Builder with portList": { + portList: []corev1.ContainerPort{ + corev1.ContainerPort{}, + }, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + "Test Builder without portList": { + portList: nil, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithPortsNew(mock.portList) + if mock.expectErr && len(b.errors) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errors) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithResources(t *testing.T) { + tests := map[string]struct { + requirements *corev1.ResourceRequirements + builder *Builder + expectErr bool + }{ + "Test Builder with resource requirements": { + requirements: &corev1.ResourceRequirements{}, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + "Test Builder without resource requirements": { + requirements: nil, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithResources(mock.requirements) + if mock.expectErr && len(b.errors) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errors) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithVolumeMountsNew(t *testing.T) { + tests := map[string]struct { + mounts []corev1.VolumeMount + builder *Builder + expectErr bool + }{ + "Test Builder with volume mounts": { + mounts: []corev1.VolumeMount{ + corev1.VolumeMount{}, + }, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: false, + }, + "Test Builder without volume mounts": { + mounts: nil, + builder: &Builder{con: &container{ + corev1.Container{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithVolumeMountsNew(mock.mounts) + if mock.expectErr && len(b.errors) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errors) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/build.go b/pkg/kubernetes/api/core/v1/persistentvolume/build.go new file mode 100644 index 00000000..3ce863d8 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/build.go @@ -0,0 +1,224 @@ +/* +Copyright 2020 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 persistentvolume + +import ( + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// Builder is the builder object for PV +type Builder struct { + pv *PV + errs []error +} + +// NewBuilder returns new instance of Builder +func NewBuilder() *Builder { + return &Builder{pv: &PV{object: &corev1.PersistentVolume{}}} +} + +// WithName sets the Name field of PV with provided value. +func (b *Builder) WithName(name string) *Builder { + if len(name) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing PV name")) + return b + } + b.pv.object.Name = name + return b +} + +// WithAnnotations sets the Annotations field of PV 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 PV object: missing annotations")) + return b + } + b.pv.object.Annotations = annotations + return b +} + +// WithLabels sets the Labels field of PV with provided arguments +func (b *Builder) WithLabels(labels map[string]string) *Builder { + if len(labels) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing labels")) + return b + } + b.pv.object.Labels = labels + return b +} + +// WithReclaimPolicy sets the PV ReclaimPolicy field with provided argument +func (b *Builder) WithReclaimPolicy(reclaimPolicy corev1.PersistentVolumeReclaimPolicy) *Builder { + b.pv.object.Spec.PersistentVolumeReclaimPolicy = reclaimPolicy + return b +} + +// WithVolumeMode sets the VolumeMode field in PV with provided arguments +func (b *Builder) WithVolumeMode(volumeMode corev1.PersistentVolumeMode) *Builder { + b.pv.object.Spec.VolumeMode = &volumeMode + return b +} + +// WithAccessModes sets the AccessMode field in PV 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 PV object: missing accessmodes")) + return b + } + b.pv.object.Spec.AccessModes = accessMode + return b +} + +// WithCapacity sets the Capacity field in PV by converting string +// capacity into Quantity +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 PV object: failed to parse capacity {%s}", capacity)) + return b + } + return b.WithCapacityQty(resCapacity) +} + +// WithCapacityQty sets the Capacity field in PV with provided arguments +func (b *Builder) WithCapacityQty(resCapacity resource.Quantity) *Builder { + resourceList := corev1.ResourceList{ + corev1.ResourceName(corev1.ResourceStorage): resCapacity, + } + b.pv.object.Spec.Capacity = resourceList + return b +} + +// WithLocalHostDirectory sets the LocalVolumeSource field of PV with provided hostpath +func (b *Builder) WithLocalHostDirectory(path string) *Builder { + return b.WithLocalHostPathFormat(path, "") +} + +// WithLocalHostPathFormat sets the LocalVolumeSource field of PV with provided hostpath +// and request to format it with fstype - if not already formatted. A "" value for fstype +// indicates that the Local PV can determine the type of FS. +func (b *Builder) WithLocalHostPathFormat(path, fstype string) *Builder { + if len(path) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing PV path")) + return b + } + volumeSource := corev1.PersistentVolumeSource{ + Local: &corev1.LocalVolumeSource{ + Path: path, + FSType: &fstype, + }, + } + + b.pv.object.Spec.PersistentVolumeSource = volumeSource + return b +} + +// WithPersistentVolumeSource sets the volume source field of PV with provided source +func (b *Builder) WithPersistentVolumeSource(source *corev1.PersistentVolumeSource) *Builder { + if source == nil { + b.errs = append(b.errs, errors.New("failed to build PV object: missing PV source")) + return b + } + b.pv.object.Spec.PersistentVolumeSource = *source + return b +} + +// WithNodeAffinityHostname sets the NodeAffinity field of PV with provided node name +func (b *Builder) WithNodeAffinityHostname(nodeName string) *Builder { + if len(nodeName) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing PV node name")) + return b + } + nodeAffinity := &corev1.VolumeNodeAffinity{ + Required: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: KeyNode, + Operator: corev1.NodeSelectorOpIn, + Values: []string{ + nodeName, + }, + }, + }, + }, + }, + }, + } + b.pv.object.Spec.NodeAffinity = nodeAffinity + return b +} + +// WithNodeAffinity sets the NodeAffinity field of PV with provided node label and key +func (b *Builder) WithNodeAffinity(key, value string) *Builder { + if len(key) == 0 || len(value) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing PV node label key or value")) + return b + } + nodeAffinity := &corev1.VolumeNodeAffinity{ + Required: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: key, + Operator: corev1.NodeSelectorOpIn, + Values: []string{ + value, + }, + }, + }, + }, + }, + }, + } + b.pv.object.Spec.NodeAffinity = nodeAffinity + return b +} + +// WithNFS sets the NFS volume source settings +func (b *Builder) WithNFS(server, path string, readOnly bool) *Builder { + if len(server) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing NFS Server address")) + return b + } + if len(path) == 0 { + b.errs = append(b.errs, errors.New("failed to build PV object: missing NFS Path")) + return b + } + volumeSource := corev1.PersistentVolumeSource{ + NFS: &corev1.NFSVolumeSource{ + Server: server, + Path: path, + ReadOnly: readOnly, + }, + } + + b.pv.object.Spec.PersistentVolumeSource = volumeSource + return b +} + +// Build returns the PV API instance +func (b *Builder) Build() (*corev1.PersistentVolume, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("%+v", b.errs) + } + return b.pv.object, nil +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/build_test.go b/pkg/kubernetes/api/core/v1/persistentvolume/build_test.go new file mode 100644 index 00000000..bfda4cb2 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/build_test.go @@ -0,0 +1,357 @@ +/* +Copyright 2020 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 persistentvolume + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBuilderWithName(t *testing.T) { + tests := map[string]struct { + name string + builder *Builder + expectErr bool + }{ + "Test Builder with name": { + name: "PV1", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builder without name": { + name: "", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithName(mock.name) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAnnotations(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith annotations": { + annotations: map[string]string{"persistent-volume": "PV", "application": "percona"}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout annotations": { + annotations: map[string]string{}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAnnotations(mock.annotations) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLabels(t *testing.T) { + tests := map[string]struct { + labels map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith labels": { + labels: map[string]string{"persistent-volume": "PV", "application": "percona"}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout labels": { + labels: map[string]string{}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLabels(mock.labels) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAccessModes(t *testing.T) { + tests := map[string]struct { + accessModes []corev1.PersistentVolumeAccessMode + builder *Builder + expectErr bool + }{ + "Test Builderwith accessModes": { + accessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce, corev1.ReadOnlyMany}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout accessModes": { + accessModes: []corev1.PersistentVolumeAccessMode{}, + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAccessModes(mock.accessModes) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithCapacity(t *testing.T) { + tests := map[string]struct { + capacity string + builder *Builder + expectErr bool + }{ + "Test Builderwith capacity": { + capacity: "5G", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout capacity": { + capacity: "", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithCapacity(mock.capacity) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLocalHostDirectory(t *testing.T) { + tests := map[string]struct { + path string + builder *Builder + expectErr bool + }{ + "Test Builderwith hostpath": { + path: "/var/openebs/local", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout hostpath": { + path: "", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLocalHostDirectory(mock.path) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithNodeAffinityHostname(t *testing.T) { + tests := map[string]struct { + nodeName string + builder *Builder + expectErr bool + }{ + "Test Builderwith node name": { + nodeName: "node1", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: false, + }, + "Test Builderwithout node name": { + nodeName: "", + builder: &Builder{pv: &PV{ + object: &corev1.PersistentVolume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithNodeAffinityHostname(mock.nodeName) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildHostPath(t *testing.T) { + fsType := "" + + tests := map[string]struct { + name string + capacity string + path string + nodeName string + expectedPV *corev1.PersistentVolume + expectedErr bool + }{ + "Hostpath PV with correct details": { + name: "PV1", + capacity: "10Ti", + path: "/var/openebs/local/PV1", + nodeName: "node1", + expectedPV: &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "PV1"}, + Spec: corev1.PersistentVolumeSpec{ + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: fakeCapacity("10Ti"), + }, + PersistentVolumeSource: corev1.PersistentVolumeSource{ + Local: &corev1.LocalVolumeSource{ + Path: "/var/openebs/local/PV1", + FSType: &fsType, + }, + }, + NodeAffinity: &corev1.VolumeNodeAffinity{ + Required: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: KeyNode, + Operator: corev1.NodeSelectorOpIn, + Values: []string{ + "node1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErr: false, + }, + "Hostpath PV with error": { + name: "", + capacity: "500Gi", + path: "", + nodeName: "500Gi", + expectedPV: nil, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + pvObj, err := NewBuilder(). + WithName(mock.name). + WithCapacity(mock.capacity). + WithLocalHostDirectory(mock.path). + WithNodeAffinityHostname(mock.nodeName). + Build() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !reflect.DeepEqual(pvObj, mock.expectedPV) { + t.Fatalf("Test %q failed: pv mismatch", name) + } + }) + } +} + +func fakeCapacity(capacity string) resource.Quantity { + q, _ := resource.ParseQuantity(capacity) + return q +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/buildlist.go b/pkg/kubernetes/api/core/v1/persistentvolume/buildlist.go new file mode 100644 index 00000000..70e54ace --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/buildlist.go @@ -0,0 +1,98 @@ +/* +Copyright 2020 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 persistentvolume + +import ( + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" +) + +// ListBuilder enables building an instance of +// PVlist +type ListBuilder struct { + list *PVList + filters PredicateList + errs []error +} + +// NewListBuilder returns an instance of ListBuilder +func NewListBuilder() *ListBuilder { + return &ListBuilder{list: &PVList{}} +} + +// ListBuilderForAPIObjects builds the ListBuilder object based on PV api list +func ListBuilderForAPIObjects(pvs *corev1.PersistentVolumeList) *ListBuilder { + b := &ListBuilder{list: &PVList{}} + if pvs == nil { + b.errs = append(b.errs, errors.New("failed to build pv list: missing api list")) + return b + } + for _, pv := range pvs.Items { + pv := pv + b.list.items = append(b.list.items, &PV{object: &pv}) + } + return b +} + +// ListBuilderForObjects builds the ListBuilder object based on PVList +func ListBuilderForObjects(pvs *PVList) *ListBuilder { + b := &ListBuilder{} + if pvs == nil { + b.errs = append(b.errs, errors.New("failed to build pv list: missing object list")) + return b + } + b.list = pvs + return b +} + +// List returns the list of pv +// instances that was built by this +// builder +func (b *ListBuilder) List() (*PVList, error) { + if len(b.errs) > 0 { + return nil, errors.Errorf("failed to list pv: %+v", b.errs) + } + if b.filters == nil || len(b.filters) == 0 { + return b.list, nil + } + filteredList := &PVList{} + for _, pv := range b.list.items { + if b.filters.all(pv) { + filteredList.items = append(filteredList.items, pv) + } + } + 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 PV list using listbuilder +func (b *ListBuilder) APIList() (*corev1.PersistentVolumeList, error) { + l, err := b.List() + if err != nil { + return nil, err + } + return l.ToAPIList(), nil +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/buildlist_test.go b/pkg/kubernetes/api/core/v1/persistentvolume/buildlist_test.go new file mode 100644 index 00000000..75470b73 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/buildlist_test.go @@ -0,0 +1,106 @@ +/* +Copyright 2020 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 persistentvolume + +import ( + "testing" +) + +func TestListBuilderWithAPIList(t *testing.T) { + tests := map[string]struct { + availablePVs []string + expectedPVLen int + }{ + "PV set 1": {[]string{}, 0}, + "PV set 2": {[]string{"pv1"}, 1}, + "PV set 3": {[]string{"pv1", "pv2"}, 2}, + "PV set 4": {[]string{"pv1", "pv2", "pv3"}, 3}, + "PV set 5": {[]string{"pv1", "pv2", "pv3", "pv4"}, 4}, + "PV set 6": {[]string{"pv1", "pv2", "pv3", "pv4", "pv5"}, 5}, + "PV set 7": {[]string{"pv1", "pv2", "pv3", "pv4", "pv5", "pv6"}, 6}, + "PV set 8": {[]string{"pv1", "pv2", "pv3", "pv4", "pv5", "pv6", "pv7"}, 7}, + "PV set 9": {[]string{"pv1", "pv2", "pv3", "pv4", "pv5", "pv6", "pv7", "pv8"}, 8}, + "PV set 10": {[]string{"pv1", "pv2", "pv3", "pv4", "pv5", "pv6", "pv7", "pv8", "pv9"}, 9}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := ListBuilderForAPIObjects(fakeAPIPVList(mock.availablePVs)) + if mock.expectedPVLen != len(b.list.items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPVLen, len(b.list.items)) + } + }) + } +} + +func TestListBuilderWithAPIObjects(t *testing.T) { + tests := map[string]struct { + availablePVs []string + expectedPVLen int + expectedErr bool + }{ + "PV set 1": {[]string{}, 0, true}, + "PV set 2": {[]string{"pv1"}, 1, false}, + "PV set 3": {[]string{"pv1", "pv2"}, 2, false}, + "PV set 4": {[]string{"pv1", "pv2", "pv3"}, 3, false}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b, err := ListBuilderForAPIObjects(fakeAPIPVList(mock.availablePVs)).APIList() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !mock.expectedErr && mock.expectedPVLen != len(b.Items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.availablePVs, len(b.Items)) + } + }) + } +} + +func TestListBuilderAPIList(t *testing.T) { + tests := map[string]struct { + availablePVs []string + expectedPVLen int + expectedErr bool + }{ + "PV set 1": {[]string{}, 0, true}, + "PV set 2": {[]string{"pv1"}, 1, false}, + "PV set 3": {[]string{"pv1", "pv2"}, 2, false}, + "PV set 4": {[]string{"pv1", "pv2", "pv3"}, 3, false}, + "PV set 5": {[]string{"pv1", "pv2", "pv3", "pv4"}, 4, false}, + } + for name, mock := range tests { + + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b, err := ListBuilderForAPIObjects(fakeAPIPVList(mock.availablePVs)).APIList() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if err == nil && mock.expectedPVLen != len(b.Items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPVLen, len(b.Items)) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes.go b/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes.go new file mode 100644 index 00000000..9197c662 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes.go @@ -0,0 +1,229 @@ +// Copyright © 2018-2020 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 persistentvolume + +import ( + "strings" + + stringer "github.com/openebs/maya/pkg/apis/stringer/v1alpha1" + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/client" + "k8s.io/client-go/kubernetes" +) + +const ( + //KeyNode represents the key values used for specifying the Node Affinity + // based on the hostname + KeyNode = "kubernetes.io/hostname" +) + +// 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 pv +type getFn func(cli *kubernetes.Clientset, name string, opts metav1.GetOptions) (*corev1.PersistentVolume, error) + +// listFn is a typed function that abstracts +// listing of pvs +type listFn func(cli *kubernetes.Clientset, opts metav1.ListOptions) (*corev1.PersistentVolumeList, error) + +// deleteFn is a typed function that abstracts +// deletion of pvs +type deleteFn func(cli *kubernetes.Clientset, name string, deleteOpts *metav1.DeleteOptions) error + +// deleteFn is a typed function that abstracts +// deletion of pv's collection +type deleteCollectionFn func(cli *kubernetes.Clientset, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error + +// createFn is a typed function that abstracts +// creation of pv +type createFn func(cli *kubernetes.Clientset, pv *corev1.PersistentVolume) (*corev1.PersistentVolume, error) + +// Kubeclient enables kubernetes API operations +// on pv instance +type Kubeclient struct { + // clientset refers to pvc 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 + 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, opts metav1.GetOptions) (*corev1.PersistentVolume, error) { + return cli.CoreV1().PersistentVolumes().Get(name, opts) + } + } + if k.list == nil { + k.list = func(cli *kubernetes.Clientset, opts metav1.ListOptions) (*corev1.PersistentVolumeList, error) { + return cli.CoreV1().PersistentVolumes().List(opts) + } + } + if k.del == nil { + k.del = func(cli *kubernetes.Clientset, name string, deleteOpts *metav1.DeleteOptions) error { + return cli.CoreV1().PersistentVolumes().Delete(name, deleteOpts) + } + } + if k.delCollection == nil { + k.delCollection = func(cli *kubernetes.Clientset, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + return cli.CoreV1().PersistentVolumes().DeleteCollection(deleteOpts, listOpts) + } + } + if k.create == nil { + k.create = func(cli *kubernetes.Clientset, pv *corev1.PersistentVolume) (*corev1.PersistentVolume, error) { + return cli.CoreV1().PersistentVolumes().Create(pv) + } + } +} + +// 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 +// cstor volume replica operations +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() +} + +// 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 pv resource +// instances present in kubernetes cluster +func (k *Kubeclient) Get(name string, opts metav1.GetOptions) (*corev1.PersistentVolume, error) { + if strings.TrimSpace(name) == "" { + return nil, errors.New("failed to get pv: missing pv name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to get pv {%s}", name) + } + return k.get(cli, name, opts) +} + +// List returns a list of pv +// instances present in kubernetes cluster +func (k *Kubeclient) List(opts metav1.ListOptions) (*corev1.PersistentVolumeList, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to list pv listoptions: '%v'", opts) + } + return k.list(cli, opts) +} + +// Delete deletes a pv 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 pv name") + } + cli, err := k.getClientsetOrCached() + if err != nil { + return errors.Wrapf(err, "failed to delete pv {%s}", name) + } + return k.del(cli, name, deleteOpts) +} + +// Create creates a pv in kubernetes cluster +func (k *Kubeclient) Create(pv *corev1.PersistentVolume) (*corev1.PersistentVolume, error) { + cli, err := k.getClientsetOrCached() + if err != nil { + return nil, errors.Wrapf(err, "failed to create pv: %s", stringer.Yaml("persistent volume", pv)) + } + return k.create(cli, pv) +} + +// DeleteCollection deletes a collection of pv 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 pvs") + } + return k.delCollection(cli, listOpts, deleteOpts) +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes_test.go b/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes_test.go new file mode 100644 index 00000000..f919f536 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/kubernetes_test.go @@ -0,0 +1,491 @@ +// Copyright © 2018-2020 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 persistentvolume + +import ( + "testing" + + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func fakeGetClientSetOk() (cli *kubernetes.Clientset, err error) { + return &kubernetes.Clientset{}, nil +} + +func fakeGetClientSetForPathOk(fakeConfigPath string) (cli *kubernetes.Clientset, err error) { + return &kubernetes.Clientset{}, nil +} + +func fakeGetClientSetForPathErr(fakeConfigPath string) (cli *kubernetes.Clientset, err error) { + return nil, errors.New("fake error") +} + +func fakeGetOk(cli *kubernetes.Clientset, name string, opts metav1.GetOptions) (*corev1.PersistentVolume, error) { + return &corev1.PersistentVolume{}, nil +} + +func fakeListOk(cli *kubernetes.Clientset, opts metav1.ListOptions) (*corev1.PersistentVolumeList, error) { + return &corev1.PersistentVolumeList{}, nil +} + +func fakeDeleteOk(cli *kubernetes.Clientset, name string, opts *metav1.DeleteOptions) error { + return nil +} + +func fakeListErr(cli *kubernetes.Clientset, opts metav1.ListOptions) (*corev1.PersistentVolumeList, error) { + return &corev1.PersistentVolumeList{}, errors.New("some error") +} + +func fakeGetErr(cli *kubernetes.Clientset, name string, opts metav1.GetOptions) (*corev1.PersistentVolume, error) { + return &corev1.PersistentVolume{}, errors.New("some error") +} + +func fakeDeleteErr(cli *kubernetes.Clientset, name string, opts *metav1.DeleteOptions) error { + return errors.New("some error") +} + +func fakeSetClientset(k *Kubeclient) { + k.clientset = &kubernetes.Clientset{} +} + +func fakeSetKubeConfigPath(k *Kubeclient) { + k.kubeConfigPath = "fake-path" +} + +func fakeSetNilClientset(k *Kubeclient) { + k.clientset = nil +} + +func fakeGetClientSetNil() (clientset *kubernetes.Clientset, err error) { + return nil, nil +} + +func fakeGetClientSetErr() (clientset *kubernetes.Clientset, err error) { + return nil, errors.New("Some error") +} + +func fakeClientSet(k *Kubeclient) {} + +func fakeCreateFnOk(cli *kubernetes.Clientset, pv *corev1.PersistentVolume) (*corev1.PersistentVolume, error) { + return &corev1.PersistentVolume{}, nil +} + +func fakeCreateFnErr(cli *kubernetes.Clientset, pv *corev1.PersistentVolume) (*corev1.PersistentVolume, error) { + return nil, errors.New("failed to create PV") +} + +func TestWithDefaultOptions(t *testing.T) { + tests := map[string]struct { + KubeClient *Kubeclient + }{ + "When all are nil": {&Kubeclient{}}, + "When clientset is nil": {&Kubeclient{ + clientset: nil, + getClientset: fakeGetClientSetOk, + getClientsetForPath: fakeGetClientSetForPathOk, + list: fakeListOk, + get: fakeGetOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When listFn nil": {&Kubeclient{ + getClientset: fakeGetClientSetOk, + list: nil, + getClientsetForPath: fakeGetClientSetForPathErr, + get: fakeGetOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When getClientsetFn nil": {&Kubeclient{ + getClientset: nil, + list: fakeListOk, + get: fakeGetOk, + getClientsetForPath: fakeGetClientSetForPathOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When getFn and CreateFn are nil": {&Kubeclient{ + getClientset: fakeGetClientSetOk, + list: fakeListOk, + getClientsetForPath: fakeGetClientSetForPathErr, + get: nil, + create: nil, + del: fakeDeleteOk, + delCollection: nil, + }}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + mock.KubeClient.withDefaults() + if mock.KubeClient.getClientset == nil { + t.Fatalf("test %q failed: expected getClientset not to be empty", name) + } + if mock.KubeClient.getClientsetForPath == nil { + t.Fatalf("test %q failed: expected getClientset not to be nil", name) + } + if mock.KubeClient.list == nil { + t.Fatalf("test %q failed: expected list not to be empty", name) + } + if mock.KubeClient.get == nil { + t.Fatalf("test %q failed: expected get not to be empty", name) + } + if mock.KubeClient.create == nil { + t.Fatalf("test %q failed: expected create not to be empty", name) + } + if mock.KubeClient.del == nil { + t.Fatalf("test %q failed: expected del not to be empty", name) + } + if mock.KubeClient.delCollection == nil { + t.Fatalf("test %q failed: expected delCollection not to be empty", name) + } + }) + } +} + +func TestGetClientSetForPathOrDirect(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + isErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetForPathOrDirect() + if mock.isErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.isErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestWithClientsetBuildOption(t *testing.T) { + tests := map[string]struct { + Clientset *kubernetes.Clientset + expectKubeClientEmpty bool + }{ + "Clientset is empty": {nil, true}, + "Clientset is not empty": {&kubernetes.Clientset{}, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + h := WithClientSet(mock.Clientset) + fake := &Kubeclient{} + h(fake) + if mock.expectKubeClientEmpty && fake.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if !mock.expectKubeClientEmpty && fake.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + }) + } +} + +func TestKubeClientBuildOption(t *testing.T) { + tests := map[string]struct { + opts []KubeclientBuildOption + expectClientSet bool + expectedKubeConfigPath bool + }{ + "Positive 1": {[]KubeclientBuildOption{fakeSetClientset, fakeSetKubeConfigPath}, true, true}, + "Positive 2": {[]KubeclientBuildOption{fakeSetClientset, fakeClientSet}, true, false}, + "Positive 3": {[]KubeclientBuildOption{fakeSetClientset, fakeClientSet, fakeClientSet}, true, false}, + + "Negative 1": {[]KubeclientBuildOption{fakeSetNilClientset, fakeSetKubeConfigPath}, false, true}, + "Negative 2": {[]KubeclientBuildOption{fakeSetNilClientset, fakeClientSet, fakeSetKubeConfigPath}, false, true}, + "Negative 3": {[]KubeclientBuildOption{fakeSetNilClientset, fakeClientSet, fakeClientSet}, false, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + c := NewKubeClient(mock.opts...) + if !mock.expectClientSet && c.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if mock.expectClientSet && c.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + if mock.expectedKubeConfigPath && c.kubeConfigPath == "" { + t.Fatalf("test %q failed expected kubeConfigPath not to be empty", name) + } + if !mock.expectedKubeConfigPath && c.kubeConfigPath != "" { + t.Fatalf("test %q failed expected kubeConfigPath to be empty", name) + } + }) + } +} + +func TestGetClientOrCached(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + expectErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetOrCached() + if mock.expectErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.expectErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestKubernetesPVList(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + list listFn + expectedErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", fakeListOk, false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListOk, false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", fakeListOk, false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", fakeListOk, false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeListOk, true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeListOk, true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", fakeListOk, true}, + "Negative 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListErr, true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + list: mock.list, + } + _, err := fc.List(metav1.ListOptions{}) + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubenetesGetPV(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + get getFn + podName string + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeGetOk, "pod-1", true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeGetOk, "pod-1", true}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeGetOk, "pod-2", false}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fp", fakeGetErr, "pod-3", true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", fakeGetOk, "", true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + get: mock.get, + } + _, err := k.Get(mock.podName, metav1.GetOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesDeletePV(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + podName string + delete deleteFn + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", "pod-1", fakeDeleteOk, true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fake-path2", "pod-2", fakeDeleteOk, false}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", "pod-3", fakeDeleteErr, true}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", "", fakeDeleteOk, true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteOk, true}, + "Test 6": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteErr, true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + del: mock.delete, + } + err := k.Delete(mock.podName, &metav1.DeleteOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesPVCreate(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + create createFn + pv *v1.PersistentVolume + expectErr bool + }{ + "Test 1": { + getClientSet: fakeGetClientSetErr, + getClientSetForPath: fakeGetClientSetForPathErr, + kubeConfigPath: "", + create: fakeCreateFnOk, + pv: &v1.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: "PV-1"}}, + expectErr: true, + }, + "Test 2": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "", + create: fakeCreateFnErr, + pv: &v1.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: "PV-2"}}, + expectErr: true, + }, + "Test 3": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "fake-path", + create: fakeCreateFnErr, + pv: nil, + expectErr: true, + }, + "Test 4": { + getClientSet: fakeGetClientSetErr, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "fake-path", + create: fakeCreateFnOk, + pv: nil, + expectErr: false, + }, + "Test 5": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathErr, + kubeConfigPath: "fake-path", + create: fakeCreateFnOk, + pv: nil, + expectErr: true, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + create: mock.create, + } + _, err := fc.Create(mock.pv) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume.go b/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume.go new file mode 100644 index 00000000..47690188 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume.go @@ -0,0 +1,210 @@ +// Copyright © 2018-2020 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 persistentvolume + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" +) + +// PV is a wrapper over persistentvolume api +// object. It provides build, validations and other common +// logic to be used by various feature specific callers. +type PV struct { + object *corev1.PersistentVolume +} + +// PVList is a wrapper over persistentvolume api +// object. It provides build, validations and other common +// logic to be used by various feature specific callers. +type PVList struct { + items []*PV +} + +// Len returns the number of items present +// in the PVList +func (p *PVList) Len() int { + return len(p.items) +} + +// ToAPIList converts PVList to API PVList +func (p *PVList) ToAPIList() *corev1.PersistentVolumeList { + plist := &corev1.PersistentVolumeList{} + for _, pvc := range p.items { + plist.Items = append(plist.Items, *pvc.object) + } + return plist +} + +type pvBuildOption func(*PV) + +// NewForAPIObject returns a new instance of PV +func NewForAPIObject(obj *corev1.PersistentVolume, opts ...pvBuildOption) *PV { + p := &PV{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(*PV) bool + +// IsNil returns true if the PV instance +// is nil +func (p *PV) IsNil() bool { + return p.object == nil +} + +// GetPath returns path configured on VolumeSource +// The VolumeSource can be either Local or HostPath +func (p *PV) GetPath() string { + local := p.object.Spec.PersistentVolumeSource.Local + if local != nil { + return local.Path + } + //Handle the case of Local PV created in 0.9 using + //HostPath VolumeSource + hostPath := p.object.Spec.PersistentVolumeSource.HostPath + if hostPath != nil { + return hostPath.Path + } + return "" +} + +// GetAffinitedNodeHostname returns hostname configured using the NodeAffinity +// This method expects only a single hostname to be set. +// +// The PV object will have the node's hostname specified as follows: +// nodeAffinity: +// required: +// nodeSelectorTerms: +// - matchExpressions: +// - key: kubernetes.io/hostname +// operator: In +// values: +// - hostname +// +func (p *PV) GetAffinitedNodeHostname() string { + nodeAffinity := p.object.Spec.NodeAffinity + if nodeAffinity == nil { + return "" + } + required := nodeAffinity.Required + if required == nil { + return "" + } + + hostname := "" + for _, selectorTerm := range required.NodeSelectorTerms { + for _, expression := range selectorTerm.MatchExpressions { + if expression.Key == KeyNode && + expression.Operator == corev1.NodeSelectorOpIn { + if len(expression.Values) != 1 { + return "" + } + hostname = expression.Values[0] + break + } + } + if hostname != "" { + break + } + } + return hostname +} + +// GetAffinitedNodeLabelKeyAndValue returns label key and value configured using +// the NodeAffinity. This method expects only a single value to be set. +// +// The PV object will have the node's hostname specified as follows: +// nodeAffinity: +// required: +// nodeSelectorTerms: +// - matchExpressions: +// - key: kubernetes.io/hostname +// operator: In +// values: +// - hostname +// +func (p *PV) GetAffinitedNodeLabelKeyAndValue() (string, string) { + nodeAffinity := p.object.Spec.NodeAffinity + if nodeAffinity == nil { + return "", "" + } + required := nodeAffinity.Required + if required == nil { + return "", "" + } + + key := "" + value := "" + for _, selectorTerm := range required.NodeSelectorTerms { + for _, expression := range selectorTerm.MatchExpressions { + if expression.Operator == corev1.NodeSelectorOpIn { + if len(expression.Values) != 1 { + return "", "" + } + key = expression.Key + value = expression.Values[0] + break + } + } + if value != "" { + break + } + } + return key, value +} + +// IsNil is predicate to filter out nil PV +// instances +func IsNil() Predicate { + return func(p *PV) bool { + return p.IsNil() + } +} + +// ContainsName is filter function to filter pv's +// based on the name +func ContainsName(name string) Predicate { + return func(p *PV) 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 pv +// instance +func (l PredicateList) all(p *PV) bool { + for _, pred := range l { + if !pred(p) { + return false + } + } + return true +} + +// WithFilter adds filters on which the pv's has to be filtered +func (b *ListBuilder) WithFilter(pred ...Predicate) *ListBuilder { + b.filters = append(b.filters, pred...) + return b +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume_test.go b/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume_test.go new file mode 100644 index 00000000..a1ed6d89 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolume/persistentvolume_test.go @@ -0,0 +1,32 @@ +// Copyright © 2018-2020 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 persistentvolume + +import ( + corev1 "k8s.io/api/core/v1" +) + +func fakeAPIPVList(pvNames []string) *corev1.PersistentVolumeList { + if len(pvNames) == 0 { + return nil + } + list := &corev1.PersistentVolumeList{} + for _, name := range pvNames { + pv := corev1.PersistentVolume{} + pv.SetName(name) + list.Items = append(list.Items, pv) + } + return list +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build.go new file mode 100644 index 00000000..59ce73b8 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build.go @@ -0,0 +1,193 @@ +/* +Copyright 2020 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 persistentvolumeclaim + +import ( + errors "github.com/pkg/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 cstorvolumeclaim 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 +} + +// WithAccessModeRWO sets the AccessMode field in PVC with Read-Write-Once +func (b *Builder) WithAccessModeRWO() *Builder { + return b.WithAccessModes([]corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}) +} + +// 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 +} + +// WithVolumeMode sets the volumeMode field in PVC with provided arguments +func (b *Builder) WithVolumeMode(vM corev1.PersistentVolumeMode) *Builder { + + b.pvc.object.Spec.VolumeMode = &vM + 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/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build_test.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build_test.go new file mode 100644 index 00000000..ec028471 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/build_test.go @@ -0,0 +1,371 @@ +/* +Copyright 2020 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 persistentvolumeclaim + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBuilderWithName(t *testing.T) { + tests := map[string]struct { + name string + builder *Builder + expectErr bool + }{ + "Test Builder with name": { + name: "PVC1", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builder without name": { + name: "", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithName(mock.name) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithNamespace(t *testing.T) { + tests := map[string]struct { + namespace string + builder *Builder + expectErr bool + }{ + "Test Builderwith namespae": { + namespace: "jiva-ns", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout namespace": { + namespace: "", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithNamespace(mock.namespace) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAnnotations(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith annotations": { + annotations: map[string]string{"persistent-volume": "PV", "application": "percona"}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout annotations": { + annotations: map[string]string{}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAnnotations(mock.annotations) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLabels(t *testing.T) { + tests := map[string]struct { + labels map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith labels": { + labels: map[string]string{"persistent-volume": "PV", "application": "percona"}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout labels": { + labels: map[string]string{}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLabels(mock.labels) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLabelsNew(t *testing.T) { + tests := map[string]struct { + labels map[string]string + builder *Builder + expectErr bool + }{ + "Test 1 - with labels": { + labels: map[string]string{ + "persistent-volume": "PV", + "application": "percona", + }, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + + "Test 2 - with empty labels": { + labels: map[string]string{}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + + "Test 3 - with nil labels": { + labels: nil, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLabelsNew(mock.labels) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAccessModes(t *testing.T) { + tests := map[string]struct { + accessModes []corev1.PersistentVolumeAccessMode + builder *Builder + expectErr bool + }{ + "Test Builderwith accessModes": { + accessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce, corev1.ReadOnlyMany}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout accessModes": { + accessModes: []corev1.PersistentVolumeAccessMode{}, + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAccessModes(mock.accessModes) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithStorageClass(t *testing.T) { + tests := map[string]struct { + scName string + builder *Builder + expectErr bool + }{ + "Test Builderwith SC": { + scName: "single-replica", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout SC": { + scName: "", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithStorageClass(mock.scName) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithCapacity(t *testing.T) { + tests := map[string]struct { + capacity string + builder *Builder + expectErr bool + }{ + "Test Builderwith capacity": { + capacity: "5G", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: false, + }, + "Test Builderwithout capacity": { + capacity: "", + builder: &Builder{pvc: &PVC{ + object: &corev1.PersistentVolumeClaim{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithCapacity(mock.capacity) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuild(t *testing.T) { + tests := map[string]struct { + name string + capacity string + expectedPVC *corev1.PersistentVolumeClaim + expectedErr bool + }{ + "PVC with correct details": { + name: "PVC1", + capacity: "10Ti", + expectedPVC: &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "PVC1"}, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: fakeCapacity("10Ti"), + }, + }, + }, + }, + expectedErr: false, + }, + "PVC with error": { + name: "", + capacity: "500Gi", + expectedPVC: nil, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + pvcObj, err := NewBuilder().WithName(mock.name).WithCapacity(mock.capacity).Build() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !reflect.DeepEqual(pvcObj, mock.expectedPVC) { + t.Fatalf("Test %q failed: pvc mismatch", name) + } + }) + } +} + +func fakeCapacity(capacity string) resource.Quantity { + q, _ := resource.ParseQuantity(capacity) + return q +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist.go new file mode 100644 index 00000000..b0de59b8 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist.go @@ -0,0 +1,165 @@ +/* +Copyright 2020 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 persistentvolumeclaim + +import ( + errors "github.com/pkg/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/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist_test.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist_test.go new file mode 100644 index 00000000..4469dfa6 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/buildlist_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2020 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 persistentvolumeclaim + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestListBuilderWithAPIList(t *testing.T) { + tests := map[string]struct { + availablePVCs []string + expectedPVCLen int + }{ + "PVC set 1": {[]string{}, 0}, + "PVC set 2": {[]string{"pvc1"}, 1}, + "PVC set 3": {[]string{"pvc1", "pvc2"}, 2}, + "PVC set 4": {[]string{"pvc1", "pvc2", "pvc3"}, 3}, + "PVC set 5": {[]string{"pvc1", "pvc2", "pvc3", "pvc4"}, 4}, + "PVC set 6": {[]string{"pvc1", "pvc2", "pvc3", "pvc4", "pvc5"}, 5}, + "PVC set 7": {[]string{"pvc1", "pvc2", "pvc3", "pvc4", "pvc5", "pvc6"}, 6}, + "PVC set 8": {[]string{"pvc1", "pvc2", "pvc3", "pvc4", "pvc5", "pvc6", "pvc7"}, 7}, + "PVC set 9": {[]string{"pvc1", "pvc2", "pvc3", "pvc4", "pvc5", "pvc6", "pvc7", "pvc8"}, 8}, + "PVC set 10": {[]string{"pvc1", "pvc2", "pvc3", "pvc4", "pvc5", "pvc6", "pvc7", "pvc8", "pvc9"}, 9}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := ListBuilderForAPIObjects(fakeAPIPVCList(mock.availablePVCs)) + if mock.expectedPVCLen != len(b.list.items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPVCLen, len(b.list.items)) + } + }) + } +} + +func TestListBuilderWithAPIObjects(t *testing.T) { + tests := map[string]struct { + availablePVCs []string + expectedPVCLen int + expectedErr bool + }{ + "PVC set 1": {[]string{}, 0, true}, + "PVC set 2": {[]string{"pvc1"}, 1, false}, + "PVC set 3": {[]string{"pvc1", "pvc2"}, 2, false}, + "PVC set 4": {[]string{"pvc1", "pvc2", "pvc3"}, 3, false}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b, err := ListBuilderForAPIObjects(fakeAPIPVCList(mock.availablePVCs)).APIList() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !mock.expectedErr && mock.expectedPVCLen != len(b.Items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.availablePVCs, len(b.Items)) + } + }) + } +} + +func TestListBuilderAPIList(t *testing.T) { + tests := map[string]struct { + availablePVCs []string + expectedPVCLen int + expectedErr bool + }{ + "PVC set 1": {[]string{}, 0, true}, + "PVC set 2": {[]string{"pvc1"}, 1, false}, + "PVC set 3": {[]string{"pvc1", "pvc2"}, 2, false}, + "PVC set 4": {[]string{"pvc1", "pvc2", "pvc3"}, 3, false}, + "PVC set 5": {[]string{"pvc1", "pvc2", "pvc3", "pvc4"}, 4, false}, + } + for name, mock := range tests { + + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b, err := ListBuilderForAPIObjects(fakeAPIPVCList(mock.availablePVCs)).APIList() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if err == nil && mock.expectedPVCLen != len(b.Items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPVCLen, len(b.Items)) + } + }) + } +} + +func TestFilterList(t *testing.T) { + tests := map[string]struct { + availablePVCs map[string]corev1.PersistentVolumeClaimPhase + filteredPVCs []string + filters PredicateList + expectedErr bool + }{ + "PVC Set 1": { + availablePVCs: map[string]corev1.PersistentVolumeClaimPhase{"PVC5": corev1.ClaimBound, "PVC6": corev1.ClaimPending, "PVC7": corev1.ClaimLost}, + filteredPVCs: []string{"PVC5"}, + filters: PredicateList{IsBound()}, + expectedErr: false, + }, + + "PVC Set 2": { + availablePVCs: map[string]corev1.PersistentVolumeClaimPhase{"PVC3": corev1.ClaimBound, "PVC4": corev1.ClaimBound}, + filteredPVCs: []string{"PVC2", "PVC4"}, + filters: PredicateList{IsBound()}, + expectedErr: false, + }, + + "PVC Set 3": { + availablePVCs: map[string]corev1.PersistentVolumeClaimPhase{"PVC1": corev1.ClaimLost, "PVC2": corev1.ClaimPending, "PVC3": corev1.ClaimPending}, + filteredPVCs: []string{}, + filters: PredicateList{IsBound()}, + expectedErr: false, + }, + "PVC Set 4": { + availablePVCs: map[string]corev1.PersistentVolumeClaimPhase{}, + filteredPVCs: []string{}, + filters: PredicateList{IsBound()}, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + list, err := ListBuilderForAPIObjects(fakeAPIPVCListFromNameStatusMap(mock.availablePVCs)).WithFilter(mock.filters...).List() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if err == nil && len(list.items) != len(mock.filteredPVCs) { + t.Fatalf("Test %v failed: expected %v got %v", name, len(mock.filteredPVCs), len(list.items)) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes.go new file mode 100644 index 00000000..aa613705 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes.go @@ -0,0 +1,288 @@ +// Copyright © 2018-2020 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 persistentvolumeclaim + +import ( + "strings" + + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openebs/dynamic-localpv-provisioner/pkg/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 +// cstor volume replica 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/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes_test.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes_test.go new file mode 100644 index 00000000..99cdb118 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/kubernetes_test.go @@ -0,0 +1,574 @@ +// Copyright © 2018-2020 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 persistentvolumeclaim + +import ( + "testing" + + errors "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func fakeGetClientSetOk() (cli *kubernetes.Clientset, err error) { + return &kubernetes.Clientset{}, nil +} + +func fakeGetClientSetForPathOk(fakeConfigPath string) (cli *kubernetes.Clientset, err error) { + return &kubernetes.Clientset{}, nil +} + +func fakeGetClientSetForPathErr(fakeConfigPath string) (cli *kubernetes.Clientset, err error) { + return nil, errors.New("fake error") +} + +func fakeGetOk(cli *kubernetes.Clientset, name, namespace string, opts metav1.GetOptions) (*corev1.PersistentVolumeClaim, error) { + return &corev1.PersistentVolumeClaim{}, nil +} + +func fakeListOk(cli *kubernetes.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PersistentVolumeClaimList, error) { + return &corev1.PersistentVolumeClaimList{}, nil +} + +func fakeDeleteOk(cli *kubernetes.Clientset, name, namespace string, opts *metav1.DeleteOptions) error { + return nil +} + +func fakeListErr(cli *kubernetes.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PersistentVolumeClaimList, error) { + return &corev1.PersistentVolumeClaimList{}, errors.New("some error") +} + +func fakeGetErr(cli *kubernetes.Clientset, name, namespace string, opts metav1.GetOptions) (*corev1.PersistentVolumeClaim, error) { + return &corev1.PersistentVolumeClaim{}, errors.New("some error") +} + +func fakeDeleteErr(cli *kubernetes.Clientset, name, namespace string, opts *metav1.DeleteOptions) error { + return errors.New("some error") +} + +func fakeSetClientset(k *Kubeclient) { + k.clientset = &kubernetes.Clientset{} +} + +func fakeSetKubeConfigPath(k *Kubeclient) { + k.kubeConfigPath = "fake-path" +} + +func fakeSetNilClientset(k *Kubeclient) { + k.clientset = nil +} + +func fakeGetClientSetNil() (clientset *kubernetes.Clientset, err error) { + return nil, nil +} + +func fakeGetClientSetErr() (clientset *kubernetes.Clientset, err error) { + return nil, errors.New("Some error") +} + +func fakeClientSet(k *Kubeclient) {} + +func fakeCreateFnOk(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + return &corev1.PersistentVolumeClaim{}, nil +} + +func fakeCreateFnErr(cli *kubernetes.Clientset, namespace string, pvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + return nil, errors.New("failed to create PVC") +} + +func TestWithDefaultOptions(t *testing.T) { + tests := map[string]struct { + KubeClient *Kubeclient + }{ + "When all are nil": {&Kubeclient{}}, + "When clientset is nil": {&Kubeclient{ + clientset: nil, + getClientset: fakeGetClientSetOk, + getClientsetForPath: fakeGetClientSetForPathOk, + list: fakeListOk, + get: fakeGetOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When listFn nil": {&Kubeclient{ + getClientset: fakeGetClientSetOk, + list: nil, + getClientsetForPath: fakeGetClientSetForPathErr, + get: fakeGetOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When getClientsetFn nil": {&Kubeclient{ + getClientset: nil, + list: fakeListOk, + get: fakeGetOk, + getClientsetForPath: fakeGetClientSetForPathOk, + create: fakeCreateFnOk, + del: fakeDeleteOk, + delCollection: nil, + }}, + "When getFn and CreateFn are nil": {&Kubeclient{ + getClientset: fakeGetClientSetOk, + list: fakeListOk, + getClientsetForPath: fakeGetClientSetForPathErr, + get: nil, + create: nil, + del: fakeDeleteOk, + delCollection: nil, + }}, + } + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + mock.KubeClient.withDefaults() + if mock.KubeClient.getClientset == nil { + t.Fatalf("test %q failed: expected getClientset not to be empty", name) + } + if mock.KubeClient.getClientsetForPath == nil { + t.Fatalf("test %q failed: expected getClientset not to be nil", name) + } + if mock.KubeClient.list == nil { + t.Fatalf("test %q failed: expected list not to be empty", name) + } + if mock.KubeClient.get == nil { + t.Fatalf("test %q failed: expected get not to be empty", name) + } + if mock.KubeClient.create == nil { + t.Fatalf("test %q failed: expected create not to be empty", name) + } + if mock.KubeClient.del == nil { + t.Fatalf("test %q failed: expected del not to be empty", name) + } + if mock.KubeClient.delCollection == nil { + t.Fatalf("test %q failed: expected delCollection not to be empty", name) + } + }) + } +} + +func TestGetClientSetForPathOrDirect(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + isErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetForPathOrDirect() + if mock.isErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.isErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestWithClientsetBuildOption(t *testing.T) { + tests := map[string]struct { + Clientset *kubernetes.Clientset + expectKubeClientEmpty bool + }{ + "Clientset is empty": {nil, true}, + "Clientset is not empty": {&kubernetes.Clientset{}, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + h := WithClientSet(mock.Clientset) + fake := &Kubeclient{} + h(fake) + if mock.expectKubeClientEmpty && fake.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if !mock.expectKubeClientEmpty && fake.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + }) + } +} + +func TestKubeClientBuildOption(t *testing.T) { + tests := map[string]struct { + opts []KubeclientBuildOption + expectClientSet bool + expectedKubeConfigPath bool + }{ + "Positive 1": {[]KubeclientBuildOption{fakeSetClientset, fakeSetKubeConfigPath}, true, true}, + "Positive 2": {[]KubeclientBuildOption{fakeSetClientset, fakeClientSet}, true, false}, + "Positive 3": {[]KubeclientBuildOption{fakeSetClientset, fakeClientSet, fakeClientSet}, true, false}, + + "Negative 1": {[]KubeclientBuildOption{fakeSetNilClientset, fakeSetKubeConfigPath}, false, true}, + "Negative 2": {[]KubeclientBuildOption{fakeSetNilClientset, fakeClientSet, fakeSetKubeConfigPath}, false, true}, + "Negative 3": {[]KubeclientBuildOption{fakeSetNilClientset, fakeClientSet, fakeClientSet}, false, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + c := NewKubeClient(mock.opts...) + if !mock.expectClientSet && c.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if mock.expectClientSet && c.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + if mock.expectedKubeConfigPath && c.kubeConfigPath == "" { + t.Fatalf("test %q failed expected kubeConfigPath not to be empty", name) + } + if !mock.expectedKubeConfigPath && c.kubeConfigPath != "" { + t.Fatalf("test %q failed expected kubeConfigPath to be empty", name) + } + }) + } +} + +func TestGetClientOrCached(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + expectErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetOrCached() + if mock.expectErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.expectErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestKubernetesPVCList(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + list listFn + expectedErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", fakeListOk, false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListOk, false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", fakeListOk, false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", fakeListOk, false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeListOk, true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeListOk, true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", fakeListOk, true}, + "Negative 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListErr, true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + list: mock.list, + } + _, err := fc.List(metav1.ListOptions{}) + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestWithNamespaceBuildOption(t *testing.T) { + tests := map[string]struct { + namespace string + }{ + "Test 1": {""}, + "Test 2": {"alpha"}, + "Test 3": {"beta"}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + k := NewKubeClient().WithNamespace(mock.namespace) + if k.namespace != mock.namespace { + t.Fatalf("Test %q failed: expected %v got %v", name, mock.namespace, k.namespace) + } + }) + } +} + +func TestKubenetesGetPVC(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + get getFn + podName string + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeGetOk, "pod-1", true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeGetOk, "pod-1", true}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeGetOk, "pod-2", false}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fp", fakeGetErr, "pod-3", true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", fakeGetOk, "", true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + namespace: "", + get: mock.get, + } + _, err := k.Get(mock.podName, metav1.GetOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesDeletePVC(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + podName string + delete deleteFn + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", "pod-1", fakeDeleteOk, true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fake-path2", "pod-2", fakeDeleteOk, false}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", "pod-3", fakeDeleteErr, true}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", "", fakeDeleteOk, true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteOk, true}, + "Test 6": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteErr, true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &Kubeclient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + namespace: "", + del: mock.delete, + } + err := k.Delete(mock.podName, &metav1.DeleteOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesPVCCreate(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + create createFn + pvc *v1.PersistentVolumeClaim + expectErr bool + }{ + "Test 1": { + getClientSet: fakeGetClientSetErr, + getClientSetForPath: fakeGetClientSetForPathErr, + kubeConfigPath: "", + create: fakeCreateFnOk, + pvc: &v1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "PVC-1"}}, + expectErr: true, + }, + "Test 2": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "", + create: fakeCreateFnErr, + pvc: &v1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "PVC-2"}}, + expectErr: true, + }, + "Test 3": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "fake-path", + create: fakeCreateFnErr, + pvc: nil, + expectErr: true, + }, + "Test 4": { + getClientSet: fakeGetClientSetErr, + getClientSetForPath: fakeGetClientSetForPathOk, + kubeConfigPath: "fake-path", + create: fakeCreateFnOk, + pvc: nil, + expectErr: true, + }, + "Test 5": { + getClientSet: fakeGetClientSetOk, + getClientSetForPath: fakeGetClientSetForPathErr, + kubeConfigPath: "fake-path", + create: fakeCreateFnOk, + pvc: nil, + expectErr: true, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + create: mock.create, + } + _, err := fc.Create(mock.pvc) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesPVCCreateCollection(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + create createFn + pvc *v1.PersistentVolumeClaim + expectErr bool + }{ + "Test 1": { + getClientSet: fakeGetClientSetErr, + create: fakeCreateFnOk, + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "PVC-1"}, + }, + expectErr: true, + }, + "Test 2": { + getClientSet: fakeGetClientSetOk, + create: fakeCreateFnErr, + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "PVC-2"}, + }, + expectErr: true, + }, + "Test 3": { + getClientSet: fakeGetClientSetOk, + create: fakeCreateFnOk, + pvc: nil, + expectErr: true, + }, + "Test 4": { + getClientSet: fakeGetClientSetOk, + create: fakeCreateFnOk, + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "PVC-4"}, + }, + expectErr: false, + }, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &Kubeclient{ + getClientset: mock.getClientSet, + create: mock.create, + } + pvclist, _ := ListBuilderFromTemplate(mock.pvc).WithCount(10).APIList() + newlist, err := fc.CreateCollection(pvclist) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !mock.expectErr && len(newlist.Items) != 10 { + t.Fatalf("Test %q failed: expected count {%d} got {%d}", name, 10, len(newlist.Items)) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim.go new file mode 100644 index 00000000..0e11cecc --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim.go @@ -0,0 +1,116 @@ +// Copyright © 2018-2020 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 persistentvolumeclaim + +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/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim_test.go b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim_test.go new file mode 100644 index 00000000..3234ef6a --- /dev/null +++ b/pkg/kubernetes/api/core/v1/persistentvolumeclaim/persistentvolumeclaim_test.go @@ -0,0 +1,46 @@ +// Copyright © 2018-2020 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 persistentvolumeclaim + +import ( + corev1 "k8s.io/api/core/v1" +) + +func fakeAPIPVCList(pvcNames []string) *corev1.PersistentVolumeClaimList { + if len(pvcNames) == 0 { + return nil + } + list := &corev1.PersistentVolumeClaimList{} + for _, name := range pvcNames { + pvc := corev1.PersistentVolumeClaim{} + pvc.SetName(name) + list.Items = append(list.Items, pvc) + } + return list +} + +func fakeAPIPVCListFromNameStatusMap(pvcs map[string]corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaimList { + if len(pvcs) == 0 { + return nil + } + list := &corev1.PersistentVolumeClaimList{} + for k, v := range pvcs { + pvc := corev1.PersistentVolumeClaim{} + pvc.SetName(k) + pvc.Status.Phase = v + list.Items = append(list.Items, pvc) + } + return list +} diff --git a/pkg/kubernetes/api/core/v1/pod/build.go b/pkg/kubernetes/api/core/v1/pod/build.go new file mode 100644 index 00000000..72508d30 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/build.go @@ -0,0 +1,251 @@ +/* +Copyright 2019-2020 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/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/container" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/volume" + errors "github.com/pkg/errors" + 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{}}} +} + +// WithTolerationsForTaints sets the Spec.Tolerations with provided taints. +func (b *Builder) WithTolerationsForTaints(taints ...corev1.Taint) *Builder { + + tolerations := []corev1.Toleration{} + for i := range taints { + var toleration corev1.Toleration + toleration.Key = taints[i].Key + toleration.Effect = taints[i].Effect + if len(taints[i].Value) == 0 { + toleration.Operator = corev1.TolerationOpExists + } else { + toleration.Value = taints[i].Value + toleration.Operator = corev1.TolerationOpEqual + } + tolerations = append(tolerations, toleration) + } + + b.pod.object.Spec.Tolerations = append( + b.pod.object.Spec.Tolerations, + tolerations..., + ) + return b +} + +// 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 +} + +// WithNodeAffinityNew sets the NodeAffinity field of Pod with provided node label and key +func (b *Builder) WithNodeAffinityNew(key, value string) *Builder { + if len(key) == 0 || len(value) == 0 { + b.errs = append(b.errs, errors.New("failed to build Pod object: missing node label key or value")) + return b + } + nodeAffinity := &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: key, + Operator: corev1.NodeSelectorOpIn, + Values: []string{ + value, + }, + }, + }, + }, + }, + }, + } + b.pod.object.Spec.Affinity = &corev1.Affinity{ + NodeAffinity: nodeAffinity, + } + 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}) +} + +// WithServiceAccountName sets the ServiceAccountName of Pod spec with +// the provided value +func (b *Builder) WithServiceAccountName(serviceAccountName string) *Builder { + if len(serviceAccountName) == 0 { + b.errs = append( + b.errs, + errors.New("failed to build Pod object: missing Pod service account name"), + ) + return b + } + b.pod.object.Spec.ServiceAccountName = serviceAccountName + return b +} + +// 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/pkg/kubernetes/api/core/v1/pod/buildlist.go b/pkg/kubernetes/api/core/v1/pod/buildlist.go new file mode 100644 index 00000000..b509cb82 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/buildlist.go @@ -0,0 +1,82 @@ +/* +Copyright 2019-2020 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/pkg/kubernetes/api/core/v1/pod/buildlist_test.go b/pkg/kubernetes/api/core/v1/pod/buildlist_test.go new file mode 100644 index 00000000..fe5fb469 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/buildlist_test.go @@ -0,0 +1,172 @@ +// Copyright © 2019-2020 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 ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func fakeAPIPodList(podNames []string) *corev1.PodList { + if len(podNames) == 0 { + return nil + } + + list := &corev1.PodList{} + for _, name := range podNames { + pod := corev1.Pod{} + pod.SetName(name) + list.Items = append(list.Items, pod) + } + return list +} + +func fakeRunningPods(podNames []string) []*Pod { + plist := []*Pod{} + for _, podName := range podNames { + pod := corev1.Pod{} + pod.SetName(podName) + pod.Status.Phase = "Running" + plist = append(plist, &Pod{&pod}) + } + return plist +} + +func fakeAPIPodListFromStatusMap(pods map[string]string) []*Pod { + plist := []*Pod{} + for k, v := range pods { + p := &corev1.Pod{} + p.SetName(k) + p.Status.Phase = corev1.PodPhase(v) + plist = append(plist, &Pod{p}) + } + return plist +} + +func TestListBuilderForAPIList(t *testing.T) { + tests := map[string]struct { + availablePods []string + expectedPodCount int + }{ + "Pod set 1": { + availablePods: []string{}, + expectedPodCount: 0, + }, + "Pod set 2": { + availablePods: []string{"pod1"}, + expectedPodCount: 1, + }, + "Pod set 3": { + availablePods: []string{"pod1", "pod2"}, + expectedPodCount: 2, + }, + "Pod set 4": { + availablePods: []string{"pod1", "pod2", "pod3"}, + expectedPodCount: 3, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + lb := ListBuilderForAPIList(fakeAPIPodList(mock.availablePods)) + if mock.expectedPodCount != len(lb.list.items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPodCount, len(lb.list.items)) + } + }) + } +} + +func TestListBuilderForObjectList(t *testing.T) { + tests := map[string]struct { + availablePods []string + expectedPodCount int + }{ + "Pod set 1": { + availablePods: []string{}, + expectedPodCount: 0, + }, + "Pod set 2": { + availablePods: []string{"pod1"}, + expectedPodCount: 1, + }, + "Pod set 3": { + availablePods: []string{"pod1", "pod2"}, + expectedPodCount: 2, + }, + "Pod set 4": { + availablePods: []string{"pod1", "pod2", "pod3"}, + expectedPodCount: 3, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + lb := ListBuilderForObjectList(fakeRunningPods(mock.availablePods)...) + if mock.expectedPodCount != len(lb.list.items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPodCount, len(lb.list.items)) + } + }) + } +} + +func TestFilterList(t *testing.T) { + tests := map[string]struct { + availablePods map[string]string + filteredPods []string + filters PredicateList + }{ + "Pods Set 1": { + availablePods: map[string]string{"Pod 1": "Running", "Pod 2": "CrashLoopBackOff"}, + filteredPods: []string{"Pod 1"}, + filters: PredicateList{IsRunning()}, + }, + "Pods Set 2": { + availablePods: map[string]string{"Pod 1": "Running", "Pod 2": "Running"}, + filteredPods: []string{"Pod 1", "Pod 2"}, + filters: PredicateList{IsRunning()}, + }, + + "Pods Set 3": { + availablePods: map[string]string{"Pod 1": "CrashLoopBackOff", "Pod 2": "CrashLoopBackOff", "Pod 3": "CrashLoopBackOff"}, + filteredPods: []string{}, + filters: PredicateList{IsRunning()}, + }, + "Pod Set 4": { + availablePods: map[string]string{"Pod 1": "Running", "Pod 2": "Running"}, + filteredPods: []string{}, + filters: PredicateList{IsNil()}, + }, + "Pod Set 5": { + availablePods: map[string]string{"Pod 1": "Running", "Pod 2": "Running"}, + filteredPods: []string{"Pod 1", "Pod 2"}, + filters: PredicateList{}, + }, + "Pod Set 6": { + availablePods: map[string]string{"Pod 1": "Running", "Pod 2": "Running"}, + filteredPods: []string{"Pod 1", "Pod 2"}, + filters: nil, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + list := ListBuilderForObjectList(fakeAPIPodListFromStatusMap(mock.availablePods)...).WithFilter(mock.filters...).List() + if len(list.items) != len(mock.filteredPods) { + t.Fatalf("Test %v failed: expected %v got %v", name, len(mock.filteredPods), len(list.items)) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/pod/kubernetes.go b/pkg/kubernetes/api/core/v1/pod/kubernetes.go new file mode 100644 index 00000000..02c8d422 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/kubernetes.go @@ -0,0 +1,406 @@ +// Copyright © 2018-2020 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" + + client "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/client" + errors "github.com/pkg/errors" + 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 +// cstor 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/pkg/kubernetes/api/core/v1/pod/kubernetes_test.go b/pkg/kubernetes/api/core/v1/pod/kubernetes_test.go new file mode 100644 index 00000000..d7b405e0 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/kubernetes_test.go @@ -0,0 +1,470 @@ +// Copyright © 2018-2020 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 ( + "errors" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client "k8s.io/client-go/kubernetes" + clientset "k8s.io/client-go/kubernetes" +) + +func fakeGetClientSetOk() (cli *clientset.Clientset, err error) { + return &clientset.Clientset{}, nil +} + +func fakeListFnOk(cli *clientset.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PodList, error) { + return &corev1.PodList{}, nil +} + +func fakeListFnErr(cli *clientset.Clientset, namespace string, opts metav1.ListOptions) (*corev1.PodList, error) { + return &corev1.PodList{}, errors.New("some error") +} + +func fakeDeleteFnOk(cli *clientset.Clientset, namespace, name string, opts *metav1.DeleteOptions) error { + return nil +} + +func fakeDeleteFnErr(cli *clientset.Clientset, namespace, name string, opts *metav1.DeleteOptions) error { + return errors.New("some error while delete") +} + +func fakeGetFnOk(cli *clientset.Clientset, namespace, name string, opts metav1.GetOptions) (*corev1.Pod, error) { + return &corev1.Pod{}, nil +} + +func fakeGetErrfn(cli *clientset.Clientset, namespace, name string, opts metav1.GetOptions) (*corev1.Pod, error) { + return &corev1.Pod{}, errors.New("Not found") +} + +func fakeSetClientset(k *KubeClient) { + k.clientset = &client.Clientset{} +} + +func fakeSetNilClientset(k *KubeClient) { + k.clientset = nil +} + +func fakeGetClientSetNil() (clientset *clientset.Clientset, err error) { + return nil, nil +} + +func fakeGetClientSetErr() (clientset *clientset.Clientset, err error) { + return nil, errors.New("Some error") +} + +func fakeClientSet(k *KubeClient) {} + +func fakeGetClientSetForPathOk(fakeConfigPath string) (cli *clientset.Clientset, err error) { + return &clientset.Clientset{}, nil +} + +func fakeGetClientSetForPathErr(fakeConfigPath string) (cli *clientset.Clientset, err error) { + return nil, errors.New("fake error") +} + +func fakeDeleteCollectionOk(cli *clientset.Clientset, namespace string, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + return nil +} + +func fakeDeleteCollectionErr(cli *clientset.Clientset, namespace string, listOpts metav1.ListOptions, deleteOpts *metav1.DeleteOptions) error { + return errors.New("fake error") +} + +func TestWithDefaultOptions(t *testing.T) { + tests := map[string]struct { + kubeClient *KubeClient + }{ + "T1": {&KubeClient{}}, + "T2": {&KubeClient{ + clientset: nil, + getClientset: fakeGetClientSetOk, + list: fakeListFnOk, + get: fakeGetFnOk, + del: fakeDeleteFnOk, + }}, + "T3": {&KubeClient{ + getClientset: fakeGetClientSetOk, + list: nil, + get: fakeGetFnOk, + del: fakeDeleteFnOk, + }}, + "T4": {&KubeClient{ + getClientset: nil, + list: fakeListFnOk, + get: fakeGetFnOk, + del: fakeDeleteFnOk, + }}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + mock.kubeClient.withDefaults() + if mock.kubeClient.get == nil { + t.Fatalf("test %q failed: expected get not to be empty", name) + } + if mock.kubeClient.list == nil { + t.Fatalf("test %q failed: expected list not to be empty", name) + } + if mock.kubeClient.del == nil { + t.Fatalf("test %q failed: expected delete not to be empty", name) + } + if mock.kubeClient.getClientset == nil { + t.Fatalf("test %q failed: expected get clientset not to be empty", name) + } + }) + } +} + +func TestWithDefaultsForClientSetPath(t *testing.T) { + tests := map[string]struct { + getClientSetForPath getClientsetForPathFn + }{ + "T1": {nil}, + "T2": {fakeGetClientSetForPathOk}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &KubeClient{ + getClientsetForPath: mock.getClientSetForPath, + } + fc.withDefaults() + if fc.getClientsetForPath == nil { + t.Fatalf("test %q failed: expected getClientsetForPath not to be nil", name) + } + }) + } +} + +func TestGetClientSetForPathOrDirect(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + isErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + fc := &KubeClient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetForPathOrDirect() + if mock.isErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.isErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestWithClientsetBuildOption(t *testing.T) { + tests := map[string]struct { + Clientset *client.Clientset + expectKubeClientEmpty bool + }{ + "Clientset is empty": {nil, true}, + "Clientset is not empty": {&client.Clientset{}, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + h := WithClientSet(mock.Clientset) + fake := &KubeClient{} + h(fake) + if mock.expectKubeClientEmpty && fake.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if !mock.expectKubeClientEmpty && fake.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + }) + } +} + +func TestKubeClientBuildOption(t *testing.T) { + tests := map[string]struct { + opts []KubeClientBuildOption + expectClientSet bool + }{ + "Positive 1": {[]KubeClientBuildOption{fakeSetClientset, WithKubeConfigPath("fake-path")}, true}, + "Positive 2": {[]KubeClientBuildOption{fakeSetClientset, fakeClientSet}, true}, + "Positive 3": {[]KubeClientBuildOption{fakeSetClientset, fakeClientSet, WithKubeConfigPath("fake-path")}, true}, + + "Negative 1": {[]KubeClientBuildOption{fakeSetNilClientset, WithKubeConfigPath("fake-path")}, false}, + "Negative 2": {[]KubeClientBuildOption{fakeSetNilClientset, fakeClientSet}, false}, + "Negative 3": {[]KubeClientBuildOption{fakeSetNilClientset, fakeClientSet, WithKubeConfigPath("fake-path")}, false}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + c := NewKubeClient(mock.opts...) + if !mock.expectClientSet && c.clientset != nil { + t.Fatalf("test %q failed expected fake.clientset to be empty", name) + } + if mock.expectClientSet && c.clientset == nil { + t.Fatalf("test %q failed expected fake.clientset not to be empty", name) + } + }) + } +} + +func TestGetClientOrCached(t *testing.T) { + tests := map[string]struct { + getClientSet getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + expectErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", true}, + "Negative 4": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "", true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &KubeClient{ + getClientset: mock.getClientSet, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + } + _, err := fc.getClientsetOrCached() + if mock.expectErr && err == nil { + t.Fatalf("test %q failed : expected error not to be nil but got %v", name, err) + } + if !mock.expectErr && err != nil { + t.Fatalf("test %q failed : expected error be nil but got %v", name, err) + } + }) + } +} + +func TestKubernetesPodList(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + list listFn + expectErr bool + }{ + // Positive tests + "Positive 1": {fakeGetClientSetNil, fakeGetClientSetForPathOk, "fake-path", fakeListFnOk, false}, + "Positive 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListFnOk, false}, + "Positive 3": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "fake-path", fakeListFnOk, false}, + "Positive 4": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "", fakeListFnOk, false}, + + // Negative tests + "Negative 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeListFnOk, true}, + "Negative 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeListFnOk, true}, + "Negative 3": {fakeGetClientSetErr, fakeGetClientSetForPathErr, "fake-path", fakeListFnOk, true}, + "Negative 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeListFnErr, true}, + } + + for name, mock := range tests { + name := name // pin it + mock := mock // pin it + t.Run(name, func(t *testing.T) { + fc := &KubeClient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + list: mock.list, + } + _, err := fc.List(metav1.ListOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesDeletePod(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + podName string + delete deleteFn + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", "pod-1", fakeDeleteFnOk, true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fake-path2", "pod-2", fakeDeleteFnOk, false}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", "pod-3", fakeDeleteFnErr, true}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", "", fakeDeleteFnOk, true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteFnOk, true}, + "Test 6": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "pod1", fakeDeleteFnErr, true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &KubeClient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + namespace: "", + del: mock.delete, + } + err := k.Delete(mock.podName, &metav1.DeleteOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesDeleteCollection(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + listOpts string + deleteCollection deleteCollectionFn + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", "selector=selector1", fakeDeleteCollectionOk, true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fake-path2", "selector=selector1", fakeDeleteCollectionOk, false}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", "selector=selector1", fakeDeleteCollectionErr, true}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", "selector=selector1", fakeDeleteCollectionErr, true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "selector=selector1", fakeDeleteCollectionOk, true}, + "Test 6": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path2", "selector=selector1", fakeDeleteCollectionErr, true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &KubeClient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + namespace: "", + delCollection: mock.deleteCollection, + } + err := k.DeleteCollection( + metav1.ListOptions{LabelSelector: mock.listOpts}, + &metav1.DeleteOptions{}, + ) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestKubernetesGetPod(t *testing.T) { + tests := map[string]struct { + getClientset getClientsetFn + getClientSetForPath getClientsetForPathFn + kubeConfigPath string + get getFn + podName string + expectErr bool + }{ + "Test 1": {fakeGetClientSetErr, fakeGetClientSetForPathOk, "", fakeGetFnOk, "pod-1", true}, + "Test 2": {fakeGetClientSetOk, fakeGetClientSetForPathErr, "fake-path", fakeGetFnOk, "pod-1", true}, + "Test 3": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "", fakeGetFnOk, "pod-2", false}, + "Test 4": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fp", fakeGetErrfn, "pod-3", true}, + "Test 5": {fakeGetClientSetOk, fakeGetClientSetForPathOk, "fakepath", fakeGetFnOk, "", true}, + } + + for name, mock := range tests { + name := name + mock := mock + t.Run(name, func(t *testing.T) { + k := &KubeClient{ + getClientset: mock.getClientset, + getClientsetForPath: mock.getClientSetForPath, + kubeConfigPath: mock.kubeConfigPath, + namespace: "", + get: mock.get, + } + _, err := k.Get(mock.podName, metav1.GetOptions{}) + if mock.expectErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestWithBuildOption(t *testing.T) { + tests := map[string]struct { + namespace string + kubeConfigPath string + }{ + "Test 1": {"", ""}, + "Test 2": {"namespace 1", ""}, + "Test 3": {"namespace 2", "fake-path"}, + } + + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + k := NewKubeClient(WithKubeConfigPath(mock.kubeConfigPath)).WithNamespace(mock.namespace) + if k.namespace != mock.namespace { + t.Fatalf("Test %q failed: expected %v got %v", name, mock.namespace, k.namespace) + } + if k.kubeConfigPath != mock.kubeConfigPath { + t.Fatalf("Test %q failed: expected %v got %v", name, mock.namespace, k.namespace) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/pod/pod.go b/pkg/kubernetes/api/core/v1/pod/pod.go new file mode 100644 index 00000000..d5dcfd30 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/pod.go @@ -0,0 +1,183 @@ +// Copyright © 2018-2020 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/pkg/kubernetes/api/core/v1/pod/pod_test.go b/pkg/kubernetes/api/core/v1/pod/pod_test.go new file mode 100644 index 00000000..2f2fa4a7 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/pod/pod_test.go @@ -0,0 +1,105 @@ +// Copyright © 2018-2020 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 ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestListBuilderToAPIList(t *testing.T) { + tests := map[string]struct { + availablePods []string + expectedPodLen int + }{ + "Pod set 1": {[]string{}, 0}, + "Pod set 2": {[]string{"pod1"}, 1}, + "Pod set 3": {[]string{"pod1", "pod2"}, 2}, + "Pod set 4": {[]string{"pod1", "pod2", "pod3"}, 3}, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := ListBuilderForAPIList(fakeAPIPodList(mock.availablePods)).List().ToAPIList() + if mock.expectedPodLen != len(b.Items) { + t.Fatalf("Test %v failed: expected %v got %v", name, mock.expectedPodLen, len(b.Items)) + } + }) + } +} + +func TestHasLabelPredicate(t *testing.T) { + tests := map[string]struct { + availableLabels map[string]string + checkForKey, checkForVal string + hasLabels bool + }{ + "Test1": {map[string]string{"Label 1": "Key 1"}, "Label 1", "Key 1", true}, + "Test2": {map[string]string{"Label 1": "Key 1", "Label 2": "Key 2"}, "Label 1", "Key 1", true}, + "Test3": {map[string]string{"Label 1": "Key 1", "Label 2": "Key 2"}, "Label 3", "Key 3", false}, + "Test4": {map[string]string{"Label 1": "Key 1", "Label 2": "Key 2"}, "Label 1", "Key 0", false}, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + fakePod := &Pod{&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: test.availableLabels}}} + ok := HasLabel(test.checkForKey, test.checkForVal)(fakePod) + if ok != test.hasLabels { + t.Fatalf("Test %v failed, Expected %v but got %v", name, test.availableLabels, fakePod.object.GetLabels()) + } + }) + } +} + +func TestHasLabels(t *testing.T) { + tests := map[string]struct { + availableLabels map[string]string + checkLabels map[string]string + hasLabels bool + }{ + "Test1": { + availableLabels: map[string]string{"Label 1": "Key 1"}, + checkLabels: map[string]string{"Label 1": "Key 1"}, + hasLabels: true, + }, + "Test2": { + availableLabels: map[string]string{"Label 1": "Key 1", "Label 2": "Key 2", "L1": "K1", "L3": "K3"}, + checkLabels: map[string]string{"Label 1": "Key 1", "L3": "K3"}, + hasLabels: true, + }, + "Test3": { + availableLabels: map[string]string{"Label 1": "Key 1", "Label 2": "Key 2", "L1": "K1"}, + checkLabels: map[string]string{"L1": "K1", "Label 3": "Key 3"}, + hasLabels: false, + }, + "Test4": { + availableLabels: map[string]string{"Label 1": "Key 1", "Label 2": "Key 2"}, + checkLabels: map[string]string{"Label 1": "Key 0"}, + hasLabels: false, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + fakePod := &Pod{&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: test.availableLabels}}} + ok := HasLabels(test.checkLabels)(fakePod) + if ok != test.hasLabels { + t.Fatalf("Test %v failed, Expected %v but got %v", name, test.availableLabels, fakePod.object.GetLabels()) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec.go b/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec.go new file mode 100644 index 00000000..41963734 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec.go @@ -0,0 +1,507 @@ +// Copyright © 2018-2020 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 podtemplatespec + +import ( + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/container" + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/volume" + errors "github.com/pkg/errors" + 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 +} + +// WithPriorityClassName sets the PriorityClassName field of podtemplatespec +func (b *Builder) WithPriorityClassName(prorityClassName string) *Builder { + b.podtemplatespec.Object.Spec.PriorityClassName = prorityClassName + 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 +} + +func (b *Builder) WithNodeSelectorByValue(nodeselectors map[string]string) *Builder { + // 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 +} + +// WithTolerationsByValue sets pod toleration. +// If provided tolerations argument is empty it does not complain. +func (b *Builder) WithTolerationsByValue(tolerations ...corev1.Toleration) *Builder { + if len(b.podtemplatespec.Object.Spec.Tolerations) == 0 { + b.podtemplatespec.Object.Spec.Tolerations = tolerations + return b + } + + b.podtemplatespec.Object.Spec.Tolerations = append( + b.podtemplatespec.Object.Spec.Tolerations, + tolerations..., + ) + + 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/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec_test.go b/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec_test.go new file mode 100644 index 00000000..9737d6d3 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/podtemplatespec/podtemplatespec_test.go @@ -0,0 +1,421 @@ +/* +Copyright 2020 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 podtemplatespec + +import ( + "testing" + + "github.com/openebs/dynamic-localpv-provisioner/pkg/kubernetes/api/core/v1/container" + corev1 "k8s.io/api/core/v1" +) + +func TestBuilderWithName(t *testing.T) { + tests := map[string]struct { + name string + builder *Builder + expectErr bool + }{ + "Test Builder with name": { + name: "PVC1", + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without name": { + name: "", + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithName(mock.name) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithNamespace(t *testing.T) { + tests := map[string]struct { + namespace string + builder *Builder + expectErr bool + }{ + "Test Builder with namespace": { + namespace: "PVC1", + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without namespace": { + namespace: "", + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithNamespace(mock.namespace) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAnnotations(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith annotations": { + annotations: map[string]string{"persistent-volume": "PV", + "application": "percona"}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without annotations": { + annotations: map[string]string{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAnnotations(mock.annotations) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAnnotationsNew(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith annotations": { + annotations: map[string]string{"persistent-volume": "PV", + "application": "percona"}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without annotations": { + annotations: map[string]string{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAnnotationsNew(mock.annotations) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLabels(t *testing.T) { + tests := map[string]struct { + labels map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith labels": { + labels: map[string]string{"persistent-volume": "PV", + "application": "percona"}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without labels": { + labels: map[string]string{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLabels(mock.labels) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithLabelsNew(t *testing.T) { + tests := map[string]struct { + labels map[string]string + builder *Builder + expectErr bool + }{ + "Test Builderwith labels": { + labels: map[string]string{"persistent-volume": "PV", + "application": "percona"}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without labels": { + labels: map[string]string{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithLabelsNew(mock.labels) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithAffinity(t *testing.T) { + tests := map[string]struct { + affinity *corev1.Affinity + builder *Builder + expectErr bool + }{ + "Test Builder with affinity": { + affinity: &corev1.Affinity{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without affinity": { + affinity: nil, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithAffinity(mock.affinity) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithContainerBuilders(t *testing.T) { + tests := map[string]struct { + conBuilders []*container.Builder + builder *Builder + expectErr bool + }{ + "Test Builder with containerBuilders": { + conBuilders: []*container.Builder{ + container.NewBuilder(), + }, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without containerBuilders": { + conBuilders: nil, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithContainerBuilders(mock.conBuilders...) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithContainerBuildersNew(t *testing.T) { + tests := map[string]struct { + conBuilders []*container.Builder + builder *Builder + expectErr bool + }{ + "Test Builder with containerBuilders": { + conBuilders: []*container.Builder{ + container.NewBuilder(), + }, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without containerBuilders": { + conBuilders: nil, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithContainerBuildersNew(mock.conBuilders...) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithTolerations(t *testing.T) { + tests := map[string]struct { + tolerations []corev1.Toleration + builder *Builder + expectErr bool + }{ + "Test Builder with tolerations": { + tolerations: []corev1.Toleration{ + corev1.Toleration{}, + }, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without tolerations": { + tolerations: []corev1.Toleration{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithTolerations(mock.tolerations...) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilderWithTolerationsNew(t *testing.T) { + tests := map[string]struct { + tolerations []corev1.Toleration + builder *Builder + expectErr bool + }{ + "Test Builder with tolerations": { + tolerations: []corev1.Toleration{ + corev1.Toleration{}, + }, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: false, + }, + "Test Builder without tolerations": { + tolerations: []corev1.Toleration{}, + builder: &Builder{podtemplatespec: &PodTemplateSpec{ + Object: &corev1.PodTemplateSpec{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithTolerationsNew(mock.tolerations...) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/volume/build.go b/pkg/kubernetes/api/core/v1/volume/build.go new file mode 100644 index 00000000..becfc7c8 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/volume/build.go @@ -0,0 +1,195 @@ +/* +Copyright 2020 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 volume + +import ( + errors "github.com/pkg/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 +} + +//WithSecret sets the VolumeSource field of Volume with provided Secret +func (b *Builder) WithSecret(secret *corev1.Secret, defaultMode int32) *Builder { + dM := defaultMode + if secret == nil { + b.errs = append( + b.errs, + errors.New("failed to build volume object: nil ConfigMap"), + ) + return b + } + if defaultMode == 0 { + b.errs = append( + b.errs, + errors.New("failed to build volume object: missing defaultmode"), + ) + } + volumeSource := corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &dM, + SecretName: secret.Name, + }, + } + b.volume.object.VolumeSource = volumeSource + b.volume.object.Name = secret.Name + return b +} + +//WithConfigMap sets the VolumeSource field of Volume with provided ConfigMap +func (b *Builder) WithConfigMap(configMap *corev1.ConfigMap, defaultMode int32) *Builder { + dM := defaultMode + if configMap == nil { + b.errs = append( + b.errs, + errors.New("failed to build volume object: nil ConfigMap"), + ) + return b + } + if defaultMode == 0 { + b.errs = append( + b.errs, + errors.New("failed to build volume object: missing defaultmode"), + ) + } + volumeSource := corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: &dM, + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap.Name, + }, + }, + } + b.volume.object.VolumeSource = volumeSource + b.volume.object.Name = configMap.Name + 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/pkg/kubernetes/api/core/v1/volume/build_test.go b/pkg/kubernetes/api/core/v1/volume/build_test.go new file mode 100644 index 00000000..1257b0b7 --- /dev/null +++ b/pkg/kubernetes/api/core/v1/volume/build_test.go @@ -0,0 +1,207 @@ +/* +Copyright 2020 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 volume + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestBuilderWithName(t *testing.T) { + tests := map[string]struct { + name string + builder *Builder + expectErr bool + }{ + "Test Builder with name": { + name: "vol1", + builder: &Builder{volume: &Volume{ + object: &corev1.Volume{}, + }}, + expectErr: false, + }, + "Test Builder without name": { + name: "", + builder: &Builder{volume: &Volume{ + object: &corev1.Volume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithName(mock.name) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildWithHostDirectory(t *testing.T) { + tests := map[string]struct { + path string + builder *Builder + expectErr bool + }{ + "Test Builderwith hostpath": { + path: "/var/openebs/local", + builder: &Builder{volume: &Volume{ + object: &corev1.Volume{}, + }}, + expectErr: false, + }, + "Test Builderwithout hostpath": { + path: "", + builder: &Builder{volume: &Volume{ + object: &corev1.Volume{}, + }}, + expectErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := mock.builder.WithHostDirectory(mock.path) + if mock.expectErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuildHostPathVolume(t *testing.T) { + + tests := map[string]struct { + name string + path string + expectedVol *corev1.Volume + expectedErr bool + }{ + "Hostpath Volume with correct details": { + name: "PV1", + path: "/var/openebs/local/PV1", + expectedVol: &corev1.Volume{ + Name: "PV1", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/openebs/local/PV1", + }, + }, + }, + expectedErr: false, + }, + "Hostpath PV with error": { + name: "", + path: "", + expectedVol: nil, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + volObj, err := NewBuilder(). + WithName(mock.name). + WithHostDirectory(mock.path). + Build() + if mock.expectedErr && err == nil { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && err != nil { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + if !reflect.DeepEqual(volObj, mock.expectedVol) { + t.Fatalf("Test %q failed: volume mismatch", name) + } + }) + } +} + +func TestBuilerWithdHostPathandType(t *testing.T) { + sampledirtype := corev1.HostPathDirectoryOrCreate + tests := map[string]struct { + path string + dirtype *corev1.HostPathType + expectedErr bool + }{ + "Hostpath Volume with correct type": { + path: "/var/openebs/local/PV1", + dirtype: &sampledirtype, + expectedErr: false, + }, + "Hostpath Volume without type": { + path: "/var/openebs/local/PV1", + dirtype: nil, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := NewBuilder(). + WithHostPathAndType(mock.path, mock.dirtype) + if mock.expectedErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} + +func TestBuilerWithEmptyDir(t *testing.T) { + tests := map[string]struct { + path string + emptydir *corev1.EmptyDirVolumeSource + expectedErr bool + }{ + "Volume with empty dir": { + path: "/var/openebs/local/PV1", + emptydir: &corev1.EmptyDirVolumeSource{}, + expectedErr: false, + }, + "Volume without empty dir": { + path: "/var/openebs/local/PV1", + emptydir: nil, + expectedErr: true, + }, + } + for name, mock := range tests { + name, mock := name, mock + t.Run(name, func(t *testing.T) { + b := NewBuilder(). + WithEmptyDir(mock.emptydir) + if mock.expectedErr && len(b.errs) == 0 { + t.Fatalf("Test %q failed: expected error not to be nil", name) + } + if !mock.expectedErr && len(b.errs) > 0 { + t.Fatalf("Test %q failed: expected error to be nil", name) + } + }) + } +} diff --git a/pkg/kubernetes/api/core/v1/volume/volume.go b/pkg/kubernetes/api/core/v1/volume/volume.go new file mode 100644 index 00000000..9c81ee2f --- /dev/null +++ b/pkg/kubernetes/api/core/v1/volume/volume.go @@ -0,0 +1,71 @@ +// Copyright © 2018-2020 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 volume + +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/pkg/kubernetes/client/client.go b/pkg/kubernetes/client/client.go new file mode 100644 index 00000000..48ead57c --- /dev/null +++ b/pkg/kubernetes/client/client.go @@ -0,0 +1,329 @@ +/* +Copyright 2019-2020 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 client + +import ( + "strings" + "sync" + + env "github.com/openebs/maya/pkg/env/v1alpha1" + "github.com/pkg/errors" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + // K8sMasterIPEnvironmentKey is the environment variable + // key to provide kubernetes master IP address + K8sMasterIPEnvironmentKey env.ENVKey = "OPENEBS_IO_K8S_MASTER" + + // KubeConfigEnvironmentKey is the environment variable + // key to provide kubeconfig path + KubeConfigEnvironmentKey env.ENVKey = "OPENEBS_IO_KUBE_CONFIG" +) + +// getInClusterConfigFn is a typed function +// to abstract getting kubernetes incluster config +// +// NOTE: +// typed function makes it simple to mock +type getInClusterConfigFn func() (*rest.Config, error) + +// buildConfigFromFlagsFn is a typed function +// to abstract getting a kubernetes config from +// provided flags +// +// NOTE: +// typed function makes it simple to mock +type buildConfigFromFlagsFn func(string, string) (*rest.Config, error) + +// getKubeMasterIPFromENVFn is a typed function +// to abstract getting kubernetes master IP +// address from environment variable +// +// NOTE: +// typed function makes it simple to mock +type getKubeMasterIPFromENVFn func(env.ENVKey) string + +// getKubeConfigPathFromENVFn is a typed function to +// abstract getting kubernetes config path from +// environment variable +// +// NOTE: +// typed function makes it simple to mock +type getKubeConfigPathFromENVFn func(env.ENVKey) string + +// getKubeDynamicClientFn is a typed function to +// abstract getting dynamic kubernetes clientset +// +// NOTE: +// typed function makes it simple to mock +type getKubeDynamicClientFn func(*rest.Config) (dynamic.Interface, error) + +// getKubeClientsetFn is a typed function +// to abstract getting kubernetes clientset +// +// NOTE: +// typed function makes it simple to mock +type getKubeClientsetFn func(*rest.Config) (*kubernetes.Clientset, error) + +// Client provides Kubernetes client operations +type Client struct { + // IsInCluster flags if this client points + // to its own cluster + IsInCluster bool + + // KubeConfigPath to get kubernetes clientset + KubeConfigPath string + + // handle to get in-cluster config + getInClusterConfig getInClusterConfigFn + + // handle to get kubernetes config + // from flags + buildConfigFromFlags buildConfigFromFlagsFn + + // handle to get kubernetes clienset + getKubeClientset getKubeClientsetFn + + // handle to get kubernetes dynamic clientset + getKubeDynamicClient getKubeDynamicClientFn + + // handle to get kubernetes master IP from + // environment variable + getKubeMasterIPFromENV getKubeMasterIPFromENVFn + + // handle to get kubernetes config path + // from environment variable + getKubeConfigPathFromENV getKubeConfigPathFromENVFn +} + +// OptionFn is a typed function to abstract +// any operation against the provided client +// instance +// +// NOTE: +// This is the basic building block to create +// functional operations against the client +// instance +type OptionFn func(*Client) + +// New returns a new instance of client +func New(opts ...OptionFn) *Client { + c := &Client{} + for _, o := range opts { + o(c) + } + + withDefaults(c) + return c +} + +var ( + instance *Client + once sync.Once +) + +// Instance returns a singleton instance of +// this client +func Instance(opts ...OptionFn) *Client { + once.Do(func() { + instance = New(opts...) + }) + + return instance +} + +// withDefaults sets the provided instance of +// client with necessary defaults +func withDefaults(c *Client) { + for _, def := range defaultFns { + def(c) + } +} + +var defaultFns = []OptionFn{ + withDefaultGetInClusterConfigFn(), + withDefaultBuildConfigFromFlagsFn(), + withDefaultGetKubeClientsetFn(), + withDefaultGetKubeDynamicClientFn(), + withDefaultGetKubeMasterIPFromENVFn(), + withDefaultGetKubeConfigPathFromENVFn(), +} + +// withDefaultGetInClusterConfigFn sets the default logic +// to get in-cluster config +func withDefaultGetInClusterConfigFn() OptionFn { + return func(c *Client) { + if c.getInClusterConfig == nil { + c.getInClusterConfig = rest.InClusterConfig + } + } +} + +// withDefaultBuildConfigFromFlagsFn sets the default logic +// to build config from flags +func withDefaultBuildConfigFromFlagsFn() OptionFn { + return func(c *Client) { + if c.buildConfigFromFlags == nil { + c.buildConfigFromFlags = clientcmd.BuildConfigFromFlags + } + } +} + +// withDefaultGetKubeClientsetFn sets the default logic +// to get kubernetes clientset +func withDefaultGetKubeClientsetFn() OptionFn { + return func(c *Client) { + if c.getKubeClientset == nil { + c.getKubeClientset = kubernetes.NewForConfig + } + } +} + +// withDefaultGetKubeDynamicClientFn sets the default logic +// to get kubernetes dynamic client instance +func withDefaultGetKubeDynamicClientFn() OptionFn { + return func(c *Client) { + if c.getKubeDynamicClient == nil { + c.getKubeDynamicClient = dynamic.NewForConfig + } + } +} + +// withDefaultGetKubeMasterIPFromENVFn sets the default logic +// to get kubernetes master IP address from environment +// variable +func withDefaultGetKubeMasterIPFromENVFn() OptionFn { + return func(c *Client) { + if c.getKubeMasterIPFromENV == nil { + c.getKubeMasterIPFromENV = env.Get + } + } +} + +// withDefaultGetKubeConfigPathFromENVFn sets the default logic +// to get kubeconfig path from environment variable +func withDefaultGetKubeConfigPathFromENVFn() OptionFn { + return func(c *Client) { + if c.getKubeConfigPathFromENV == nil { + c.getKubeConfigPathFromENV = env.Get + } + } +} + +// InCluster enables IsInCluster flag +func InCluster() OptionFn { + return func(c *Client) { + c.IsInCluster = true + } +} + +// WithKubeConfigPath sets kubeconfig path +// against this client instance +func WithKubeConfigPath(kubeConfigPath string) OptionFn { + return func(c *Client) { + c.KubeConfigPath = kubeConfigPath + } +} + +// GetConfig returns Kubernetes config instance +// from the provided client +func GetConfig(c *Client) (*rest.Config, error) { + if c == nil { + return nil, errors.New("failed to get kubernetes config: nil client provided") + } + + return c.GetConfigForPathOrDirect() +} + +// GetConfigForPathOrDirect returns Kubernetes config +// instance from kubeconfig path or without it +func (c *Client) GetConfigForPathOrDirect() (config *rest.Config, err error) { + if c.KubeConfigPath != "" { + return c.ConfigForPath(c.KubeConfigPath) + } + + return c.Config() +} + +// ConfigForPath returns Kubernetes config instance +// based on KubeConfig path +func (c *Client) ConfigForPath(kubeConfigPath string) (config *rest.Config, err error) { + return c.buildConfigFromFlags("", kubeConfigPath) +} + +// Config returns Kubernetes config instance +// based on set criteria +func (c *Client) Config() (config *rest.Config, err error) { + // IsInCluster flag holds the top most priority + if c.IsInCluster { + return c.getInClusterConfig() + } + + // ENV holds second priority + if strings.TrimSpace(c.getKubeMasterIPFromENV(K8sMasterIPEnvironmentKey)) != "" || + strings.TrimSpace(c.getKubeConfigPathFromENV(KubeConfigEnvironmentKey)) != "" { + return c.getConfigFromENV() + } + + // Defaults to InClusterConfig + return c.getInClusterConfig() +} + +func (c *Client) getConfigFromENV() (config *rest.Config, err error) { + k8sMaster := c.getKubeMasterIPFromENV(K8sMasterIPEnvironmentKey) + kubeConfig := c.getKubeConfigPathFromENV(KubeConfigEnvironmentKey) + + if strings.TrimSpace(k8sMaster) == "" && + strings.TrimSpace(kubeConfig) == "" { + return nil, errors.Errorf( + "failed to get kubernetes config: missing ENV: atleast one should be set: {%s} or {%s}", + K8sMasterIPEnvironmentKey, + KubeConfigEnvironmentKey, + ) + } + + return c.buildConfigFromFlags(k8sMaster, kubeConfig) +} + +// Clientset returns a new instance of Kubernetes clientset +func (c *Client) Clientset() (*kubernetes.Clientset, error) { + config, err := c.GetConfigForPathOrDirect() + if err != nil { + return nil, errors.Wrapf(err, + "failed to get kubernetes clientset: IsInCluster {%t}: KubeConfigPath {%s}", + c.IsInCluster, + c.KubeConfigPath, + ) + } + + return c.getKubeClientset(config) +} + +// Dynamic returns a kubernetes dynamic client capable +// of invoking operations against kubernetes resources +func (c *Client) Dynamic() (dynamic.Interface, error) { + config, err := c.GetConfigForPathOrDirect() + if err != nil { + return nil, errors.Wrap(err, "failed to get dynamic client") + } + + return c.getKubeDynamicClient(config) +} diff --git a/pkg/kubernetes/client/client_test.go b/pkg/kubernetes/client/client_test.go new file mode 100644 index 00000000..be1093d2 --- /dev/null +++ b/pkg/kubernetes/client/client_test.go @@ -0,0 +1,318 @@ +/* +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 client + +import ( + "testing" + + env "github.com/openebs/maya/pkg/env/v1alpha1" + "github.com/pkg/errors" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +func fakeGetClientsetOk(c *rest.Config) (*kubernetes.Clientset, error) { + return &kubernetes.Clientset{}, nil +} + +func fakeGetClientsetErr(c *rest.Config) (*kubernetes.Clientset, error) { + return nil, errors.New("fake error") +} + +func fakeInClusterConfigOk() (*rest.Config, error) { + return &rest.Config{}, nil +} + +func fakeInClusterConfigErr() (*rest.Config, error) { + return nil, errors.New("fake error") +} + +func fakeBuildConfigFromFlagsOk(kubemaster string, kubeconfig string) (*rest.Config, error) { + return &rest.Config{}, nil +} + +func fakeBuildConfigFromFlagsErr(kubemaster string, kubeconfig string) (*rest.Config, error) { + return nil, errors.New("fake error") +} + +func fakeGetKubeConfigPathOk(e env.ENVKey) string { + return "fake" +} + +func fakeGetKubeConfigPathNil(e env.ENVKey) string { + return "" +} + +func fakeGetKubeMasterIPOk(e env.ENVKey) string { + return "fake" +} + +func fakeGetKubeMasterIPNil(e env.ENVKey) string { + return "" +} + +func fakeGetDynamicClientSetOk(c *rest.Config) (dynamic.Interface, error) { + return dynamic.NewForConfig(c) +} + +func fakeGetDynamicClientSetNil(c *rest.Config) (dynamic.Interface, error) { + return nil, nil +} + +func fakeGetDynamicClientSetErr(c *rest.Config) (dynamic.Interface, error) { + return nil, errors.New("fake error") +} + +func TestNewInCluster(t *testing.T) { + c := New(InCluster()) + if !c.IsInCluster { + t.Fatalf("test failed: expected IsInCluster as 'true' actual '%t'", c.IsInCluster) + } +} + +func TestConfig(t *testing.T) { + tests := map[string]struct { + isInCluster bool + kubeConfigPath string + getInClusterConfig getInClusterConfigFn + getKubeMasterIP getKubeMasterIPFromENVFn + getKubeConfigPath getKubeConfigPathFromENVFn + getConfigFromENV buildConfigFromFlagsFn + isErr bool + }{ + "t1": {true, "", fakeInClusterConfigOk, nil, nil, nil, false}, + "t2": {true, "", fakeInClusterConfigErr, nil, nil, nil, true}, + "t3": {false, "", fakeInClusterConfigErr, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, true}, + "t4": {false, "", fakeInClusterConfigOk, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, false}, + "t5": {false, "fakeKubeConfigPath", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathNil, fakeBuildConfigFromFlagsOk, false}, + "t6": {false, "", nil, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, false}, + "t7": {false, "", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, false}, + "t8": {false, "fakeKubeConfigPath", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsErr, true}, + "t9": {false, "fakeKubeConfigpath", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, false}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + IsInCluster: mock.isInCluster, + KubeConfigPath: mock.kubeConfigPath, + getInClusterConfig: mock.getInClusterConfig, + getKubeMasterIPFromENV: mock.getKubeMasterIP, + getKubeConfigPathFromENV: mock.getKubeConfigPath, + buildConfigFromFlags: mock.getConfigFromENV, + } + _, err := c.Config() + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected no error actual '%s'", name, err) + } + }) + } +} + +func TestGetConfigFromENV(t *testing.T) { + tests := map[string]struct { + getKubeMasterIP getKubeMasterIPFromENVFn + getKubeConfigPath getKubeConfigPathFromENVFn + getConfigFromENV buildConfigFromFlagsFn + isErr bool + }{ + "t1": {fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, true}, + "t2": {fakeGetKubeMasterIPNil, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, false}, + "t3": {fakeGetKubeMasterIPOk, fakeGetKubeConfigPathNil, fakeBuildConfigFromFlagsOk, false}, + "t4": {fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, false}, + "t5": {fakeGetKubeMasterIPNil, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsErr, true}, + "t6": {fakeGetKubeMasterIPOk, fakeGetKubeConfigPathNil, fakeBuildConfigFromFlagsErr, true}, + "t7": {fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsErr, true}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + getKubeMasterIPFromENV: mock.getKubeMasterIP, + getKubeConfigPathFromENV: mock.getKubeConfigPath, + buildConfigFromFlags: mock.getConfigFromENV, + } + _, err := c.getConfigFromENV() + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected error actual no error", name) + } + if !mock.isErr && err != nil { + t.Fatalf("test '%s' failed: expected no error actual '%s'", name, err) + } + }) + } +} + +func TestGetConfigFromPathOrDirect(t *testing.T) { + tests := map[string]struct { + kubeConfigPath string + getConfigFromFlags buildConfigFromFlagsFn + getInClusterConfig getInClusterConfigFn + isErr bool + }{ + "T1": {"", fakeBuildConfigFromFlagsErr, fakeInClusterConfigOk, false}, + "T2": {"fake-path", fakeBuildConfigFromFlagsOk, fakeInClusterConfigErr, false}, + "T3": {"fake-path", fakeBuildConfigFromFlagsErr, fakeInClusterConfigOk, true}, + "T4": {"", fakeBuildConfigFromFlagsOk, fakeInClusterConfigErr, true}, + "T5": {"fake-path", fakeBuildConfigFromFlagsErr, fakeInClusterConfigErr, true}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + KubeConfigPath: mock.kubeConfigPath, + buildConfigFromFlags: mock.getConfigFromFlags, + getInClusterConfig: mock.getInClusterConfig, + getKubeMasterIPFromENV: fakeGetKubeMasterIPNil, + getKubeConfigPathFromENV: fakeGetKubeConfigPathNil, + } + _, err := c.GetConfigForPathOrDirect() + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected error actual no error", name) + } + if !mock.isErr && err != nil { + t.Fatalf("test '%s' failed: expected no error actual '%s'", name, err) + } + }) + } +} + +func TestClientset(t *testing.T) { + tests := map[string]struct { + isInCluster bool + kubeConfigPath string + getInClusterConfig getInClusterConfigFn + getKubeMasterIP getKubeMasterIPFromENVFn + getKubeConfigPath getKubeConfigPathFromENVFn + getConfigFromENV buildConfigFromFlagsFn + getKubernetesClientset getKubeClientsetFn + isErr bool + }{ + "t10": {true, "", fakeInClusterConfigOk, nil, nil, nil, fakeGetClientsetOk, false}, + "t11": {true, "", fakeInClusterConfigOk, nil, nil, nil, fakeGetClientsetErr, true}, + "t12": {true, "", fakeInClusterConfigErr, nil, nil, nil, fakeGetClientsetOk, true}, + + "t21": {false, "", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathNil, fakeBuildConfigFromFlagsOk, fakeGetClientsetOk, false}, + "t22": {false, "", nil, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, fakeGetClientsetOk, false}, + "t23": {false, "", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, fakeGetClientsetOk, false}, + "t24": {false, "fake-path", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsErr, fakeGetClientsetOk, true}, + "t25": {false, "", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, fakeGetClientsetErr, true}, + "t26": {false, "fakePath", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsErr, fakeGetClientsetOk, true}, + + "t30": {false, "", fakeInClusterConfigOk, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, fakeGetClientsetOk, false}, + "t31": {false, "", fakeInClusterConfigOk, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, fakeGetClientsetErr, true}, + "t32": {false, "", fakeInClusterConfigErr, fakeGetKubeMasterIPNil, fakeGetKubeConfigPathNil, nil, nil, true}, + "t33": {false, "fakePath", nil, fakeGetKubeMasterIPOk, fakeGetKubeConfigPathOk, fakeBuildConfigFromFlagsOk, fakeGetClientsetOk, false}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + IsInCluster: mock.isInCluster, + KubeConfigPath: mock.kubeConfigPath, + getInClusterConfig: mock.getInClusterConfig, + getKubeMasterIPFromENV: mock.getKubeMasterIP, + getKubeConfigPathFromENV: mock.getKubeConfigPath, + buildConfigFromFlags: mock.getConfigFromENV, + getKubeClientset: mock.getKubernetesClientset, + } + _, err := c.Clientset() + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected error actual no error", name) + } + if !mock.isErr && err != nil { + t.Fatalf("test '%s' failed: expected no error actual '%s'", name, err) + } + }) + } +} + +func TestDynamic(t *testing.T) { + tests := map[string]struct { + getKubeMasterIP getKubeMasterIPFromENVFn + getInClusterConfig getInClusterConfigFn + getKubernetesDynamicClientSet getKubeDynamicClientFn + kubeConfigPath string + getConfigFromENV buildConfigFromFlagsFn + getKubeConfigPath getKubeConfigPathFromENVFn + isErr bool + }{ + "t1": {fakeGetKubeMasterIPNil, fakeInClusterConfigErr, fakeGetDynamicClientSetOk, "fake-path", fakeBuildConfigFromFlagsOk, fakeGetKubeConfigPathNil, false}, + "t2": {fakeGetKubeMasterIPNil, fakeInClusterConfigErr, fakeGetDynamicClientSetErr, "fake-path", fakeBuildConfigFromFlagsOk, fakeGetKubeConfigPathOk, true}, + "t3": {fakeGetKubeMasterIPNil, fakeInClusterConfigErr, fakeGetDynamicClientSetOk, "fake-path", fakeBuildConfigFromFlagsErr, fakeGetKubeConfigPathOk, true}, + "t4": {fakeGetKubeMasterIPOk, fakeInClusterConfigOk, fakeGetDynamicClientSetOk, "", fakeBuildConfigFromFlagsOk, fakeGetKubeConfigPathOk, false}, + "t5": {fakeGetKubeMasterIPOk, fakeInClusterConfigErr, fakeGetDynamicClientSetErr, "", fakeBuildConfigFromFlagsOk, fakeGetKubeConfigPathOk, true}, + "t6": {fakeGetKubeMasterIPNil, fakeInClusterConfigOk, fakeGetDynamicClientSetErr, "", fakeBuildConfigFromFlagsErr, fakeGetKubeConfigPathNil, true}, + "t7": {fakeGetKubeMasterIPNil, fakeInClusterConfigErr, fakeGetDynamicClientSetOk, "", fakeBuildConfigFromFlagsErr, fakeGetKubeConfigPathNil, true}, + "t8": {fakeGetKubeMasterIPNil, fakeInClusterConfigErr, fakeGetDynamicClientSetErr, "", fakeBuildConfigFromFlagsErr, fakeGetKubeConfigPathNil, true}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + getKubeMasterIPFromENV: mock.getKubeMasterIP, + KubeConfigPath: mock.kubeConfigPath, + getInClusterConfig: mock.getInClusterConfig, + buildConfigFromFlags: mock.getConfigFromENV, + getKubeConfigPathFromENV: mock.getKubeConfigPath, + getKubeDynamicClient: mock.getKubernetesDynamicClientSet, + } + _, err := c.Dynamic() + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected error actual no error", name) + } + if !mock.isErr && err != nil { + t.Fatalf("test '%s' failed: expected no error but got '%v'", name, err) + } + }) + } +} + +func TestConfigForPath(t *testing.T) { + tests := map[string]struct { + kubeConfigPath string + getConfigFromPath buildConfigFromFlagsFn + isErr bool + }{ + "T1": {"", fakeBuildConfigFromFlagsErr, true}, + "T2": {"fake-path", fakeBuildConfigFromFlagsOk, false}, + } + for name, mock := range tests { + name, mock := name, mock // pin It + t.Run(name, func(t *testing.T) { + c := &Client{ + KubeConfigPath: mock.kubeConfigPath, + buildConfigFromFlags: mock.getConfigFromPath, + } + _, err := c.ConfigForPath(mock.kubeConfigPath) + if mock.isErr && err == nil { + t.Fatalf("test '%s' failed: expected error actual no error", name) + } + if !mock.isErr && err != nil { + t.Fatalf("test '%s' failed: expected no error but got '%v'", name, err) + } + }) + } +} + +func TestInstance(t *testing.T) { + c := Instance() + if c == nil { + t.Fatalf("test failed: expected non nil client instance got nil") + } +}