Skip to content

Commit

Permalink
Merge pull request #1287 from kaisoz/add-k8s-persistent-volume-claim-…
Browse files Browse the repository at this point in the history
…support

Add support for Persistent Volume Claims
  • Loading branch information
denis256 authored May 18, 2023
2 parents bf85889 + 9461fc6 commit ee5aa0e
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 16 deletions.
48 changes: 32 additions & 16 deletions modules/k8s/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,6 @@ type UnknownServiceType struct {
service *corev1.Service
}

// PersistentVolumeNotInStatus is returned when a Kubernetes PersistentVolume is not in the expected status phase
type PersistentVolumeNotInStatus struct {
pv *corev1.PersistentVolume
pvStatusPhase *corev1.PersistentVolumePhase
}

// Error is a simple function to return a formatted error message as a string
func (err PersistentVolumeNotInStatus) Error() string {
return fmt.Sprintf("Pv %s is not '%s'", err.pv.Name, *err.pvStatusPhase)
}

// NewPersistentVolumeNotInStatusError returns a PersistentVolumeNotInStatus struct when the given Persistent Volume is not in the expected status phase
func NewPersistentVolumeNotInStatusError(pv *corev1.PersistentVolume, pvStatusPhase *corev1.PersistentVolumePhase) PersistentVolumeNotInStatus {
return PersistentVolumeNotInStatus{pv, pvStatusPhase}
}

// Error is a simple function to return a formatted error message as a string
func (err UnknownServiceType) Error() string {
return fmt.Sprintf("Service %s has an unknown service type", err.service.Name)
Expand All @@ -174,6 +158,38 @@ func NewUnknownServicePortError(service *corev1.Service, port int32) UnknownServ
return UnknownServicePort{service, port}
}

// PersistentVolumeNotInStatus is returned when a Kubernetes PersistentVolume is not in the expected status phase
type PersistentVolumeNotInStatus struct {
pv *corev1.PersistentVolume
pvStatusPhase *corev1.PersistentVolumePhase
}

// Error is a simple function to return a formatted error message as a string
func (err PersistentVolumeNotInStatus) Error() string {
return fmt.Sprintf("Pv %s is not '%s'", err.pv.Name, *err.pvStatusPhase)
}

// NewPersistentVolumeNotInStatusError returns a PersistentVolumeNotInStatus struct when the given Persistent Volume is not in the expected status phase
func NewPersistentVolumeNotInStatusError(pv *corev1.PersistentVolume, pvStatusPhase *corev1.PersistentVolumePhase) PersistentVolumeNotInStatus {
return PersistentVolumeNotInStatus{pv, pvStatusPhase}
}

// PersistentVolumeClaimNotInStatus is returned when a Kubernetes PersistentVolumeClaim is not in the expected status phase
type PersistentVolumeClaimNotInStatus struct {
pvc *corev1.PersistentVolumeClaim
pvcStatusPhase *corev1.PersistentVolumeClaimPhase
}

// Error is a simple function to return a formatted error message as a string
func (err PersistentVolumeClaimNotInStatus) Error() string {
return fmt.Sprintf("PVC %s is not '%s'", err.pvc.Name, *err.pvcStatusPhase)
}

// NewPersistentVolumeClaimNotInStatusError returns a PersistentVolumeClaimNotInStatus struct when the given PersistentVolumeClaim is not in the expected status phase
func NewPersistentVolumeClaimNotInStatusError(pvc *corev1.PersistentVolumeClaim, pvcStatusPhase *corev1.PersistentVolumeClaimPhase) PersistentVolumeClaimNotInStatus {
return PersistentVolumeClaimNotInStatus{pvc, pvcStatusPhase}
}

// NoNodesInKubernetes is returned when the Kubernetes cluster has no nodes registered.
type NoNodesInKubernetes struct{}

Expand Down
98 changes: 98 additions & 0 deletions modules/k8s/persistent_volume_claim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package k8s

import (
"context"
"fmt"
"time"

"github.com/stretchr/testify/require"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/gruntwork-io/terratest/modules/logger"
"github.com/gruntwork-io/terratest/modules/retry"
"github.com/gruntwork-io/terratest/modules/testing"
)

// ListPersistentVolumeClaims will look for PersistentVolumeClaims in the given namespace that match the given filters and return them. This will fail the
// test if there is an error.
func ListPersistentVolumeClaims(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.PersistentVolumeClaim {
pvcs, err := ListPersistentVolumeClaimsE(t, options, filters)
require.NoError(t, err)
return pvcs
}

// ListPersistentVolumeClaimsE will look for PersistentVolumeClaims in the given namespace that match the given filters and return them.
func ListPersistentVolumeClaimsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.PersistentVolumeClaim, error) {
clientset, err := GetKubernetesClientFromOptionsE(t, options)
if err != nil {
return nil, err
}

resp, err := clientset.CoreV1().PersistentVolumeClaims(options.Namespace).List(context.Background(), filters)
if err != nil {
return nil, err
}
return resp.Items, nil
}

// GetPersistentVolumeClaim returns a Kubernetes PersistentVolumeClaim resource in the provided namespace with the given name. This will
// fail the test if there is an error.
func GetPersistentVolumeClaim(t testing.TestingT, options *KubectlOptions, pvcName string) *corev1.PersistentVolumeClaim {
pvc, err := GetPersistentVolumeClaimE(t, options, pvcName)
require.NoError(t, err)
return pvc
}

// GetPersistentVolumeClaimE returns a Kubernetes PersistentVolumeClaim resource in the provided namespace with the given name.
func GetPersistentVolumeClaimE(t testing.TestingT, options *KubectlOptions, pvcName string) (*corev1.PersistentVolumeClaim, error) {
clientset, err := GetKubernetesClientFromOptionsE(t, options)
if err != nil {
return nil, err
}
return clientset.CoreV1().PersistentVolumeClaims(options.Namespace).Get(context.Background(), pvcName, metav1.GetOptions{})
}

// WaitUntilPersistentVolumeClaimInStatus waits until the given PersistentVolumeClaim is the given status phase,
// retrying the check for the specified amount of times, sleeping
// for the provided duration between each try.
// This will fail the test if there is an error.
func WaitUntilPersistentVolumeClaimInStatus(t testing.TestingT, options *KubectlOptions, pvcName string, pvcStatusPhase *corev1.PersistentVolumeClaimPhase, retries int, sleepBetweenRetries time.Duration) {
require.NoError(t, WaitUntilPersistentVolumeClaimInStatusE(t, options, pvcName, pvcStatusPhase, retries, sleepBetweenRetries))
}

// WaitUntilPersistentVolumeClaimInStatusE waits until the given PersistentVolumeClaim is the given status phase,
// retrying the check for the specified amount of times, sleeping
// for the provided duration between each try.
// This will fail the test if there is an error.
func WaitUntilPersistentVolumeClaimInStatusE(t testing.TestingT, options *KubectlOptions, pvcName string, pvcStatusPhase *corev1.PersistentVolumeClaimPhase, retries int, sleepBetweenRetries time.Duration) error {
statusMsg := fmt.Sprintf("Wait for PersistentVolumeClaim %s to be '%s'.", pvcName, *pvcStatusPhase)
message, err := retry.DoWithRetryE(
t,
statusMsg,
retries,
sleepBetweenRetries,
func() (string, error) {
pvc, err := GetPersistentVolumeClaimE(t, options, pvcName)
if err != nil {
return "", err
}
if !IsPersistentVolumeClaimInStatus(pvc, pvcStatusPhase) {
return "", NewPersistentVolumeClaimNotInStatusError(pvc, pvcStatusPhase)
}
return fmt.Sprintf("PersistentVolumeClaim is now '%s'", *pvcStatusPhase), nil
},
)
if err != nil {
logger.Default.Logf(t, "Timeout waiting for PersistentVolumeClaim to be '%s': %s", *pvcStatusPhase, err)
return err
}
logger.Default.Logf(t, message)
return nil
}

// IsPersistentVolumeClaimInStatus returns true if the given PersistentVolumeClaim is in the given status phase
func IsPersistentVolumeClaimInStatus(pvc *corev1.PersistentVolumeClaim, pvcStatusPhase *corev1.PersistentVolumeClaimPhase) bool {
return pvc != nil && pvc.Status.Phase == *pvcStatusPhase
}
173 changes: 173 additions & 0 deletions modules/k8s/persistent_volume_claim_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//go:build kubeall || kubernetes
// +build kubeall kubernetes

// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube
// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with
// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm
// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine. We
// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.

package k8s

import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
_ "k8s.io/client-go/plugin/pkg/client/auth"

"github.com/gruntwork-io/terratest/modules/random"
)

func TestListPersistentVolumeClaimsReturnsPersistentVolumeClaimsInNamespace(t *testing.T) {
t.Parallel()

pvcName := "test-dummy-pvc"
namespace := strings.ToLower(random.UniqueId())
options := NewKubectlOptions("", "", namespace)
configData := renderFixtureYamlTemplate(namespace, pvcName)
defer KubectlDeleteFromString(t, options, configData)
KubectlApplyFromString(t, options, configData)

pvcs := ListPersistentVolumeClaims(t, options, metav1.ListOptions{})
require.Equal(t, len(pvcs), 1)
pvc := pvcs[0]
require.Equal(t, pvc.Name, pvcName)
require.Equal(t, pvc.Namespace, namespace)
}

func TestListPersistentVolumeClaimsReturnsZeroPersistentVolumeClaimsIfNoneCreated(t *testing.T) {
t.Parallel()

namespace := strings.ToLower(random.UniqueId())
options := NewKubectlOptions("", "", namespace)
CreateNamespace(t, options, namespace)
defer DeleteNamespace(t, options, namespace)

pvcs := ListPersistentVolumeClaims(t, options, metav1.ListOptions{})
require.Equal(t, len(pvcs), 0)
}

func TestGetPersistentVolumeClaimEReturnsErrorForNonExistantPersistentVolumeClaim(t *testing.T) {
t.Parallel()

options := NewKubectlOptions("", "", "default")
_, err := GetPersistentVolumeClaimE(t, options, "non-existent")
require.Error(t, err)
}

func TestGetPersistentVolumeClaimReturnsCorrectPersistentVolumeClaimInCorrectNamespace(t *testing.T) {
t.Parallel()

pvcName := "test-dummy-pvc"
namespace := strings.ToLower(random.UniqueId())
options := NewKubectlOptions("", "", namespace)
configData := renderFixtureYamlTemplate(namespace, pvcName)
defer KubectlDeleteFromString(t, options, configData)
KubectlApplyFromString(t, options, configData)

pvc := GetPersistentVolumeClaim(t, options, pvcName)
require.Equal(t, pvc.Name, pvcName)
require.Equal(t, pvc.Namespace, namespace)
}

func TestWaitUntilPersistentVolumeClaimInGivenStatusPhase(t *testing.T) {
t.Parallel()

pvcName := "test-dummy-pvc"
namespace := strings.ToLower(random.UniqueId())
pvcBoundStatusPhase := corev1.ClaimBound
options := NewKubectlOptions("", "", namespace)
configData := renderFixtureYamlTemplate(namespace, pvcName)
defer KubectlDeleteFromString(t, options, configData)
KubectlApplyFromString(t, options, configData)

WaitUntilPersistentVolumeClaimInStatus(t, options, pvcName, &pvcBoundStatusPhase, 60, 1*time.Second)
}

func TestWaitUntilPersistentVolumeClaimInStatusEReturnsErrorWhenWaitingForAnUnexistentPvc(t *testing.T) {
t.Parallel()

pvcBoundStatusPhase := corev1.ClaimBound
options := NewKubectlOptions("", "", "default")
err := WaitUntilPersistentVolumeClaimInStatusE(t, options, "non-existent", &pvcBoundStatusPhase, 3, 1*time.Second)
require.NotEqual(t, err, nil)
}

func TestWaitUntilPersistentVolumeClaimInStatusEReturnsErrorWhenTimesOut(t *testing.T) {
t.Parallel()

pvcName := "test-dummy-pvc"
pvcLostStatusPhase := corev1.ClaimLost
namespace := strings.ToLower(random.UniqueId())
options := NewKubectlOptions("", "", namespace)
configData := renderFixtureYamlTemplate(namespace, pvcName)
defer KubectlDeleteFromString(t, options, configData)
KubectlApplyFromString(t, options, configData)

err := WaitUntilPersistentVolumeClaimInStatusE(t, options, pvcName, &pvcLostStatusPhase, 5, 1*time.Second)
require.NotEqual(t, err, nil)
}

func TestIsPersistentVolumeClaimInStatusReturnsFalseIfPvcIsNil(t *testing.T) {
t.Parallel()

result := IsPersistentVolumeClaimInStatus(nil, nil)
require.Equal(t, result, false)
}

const pvcFixtureYamlTemplate = `---
apiVersion: v1
kind: Namespace
metadata:
name: __namespace__
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: __namespace__
spec:
capacity:
storage: 10Mi
accessModes:
- ReadWriteOnce
hostPath:
path: "/tmp/__namespace__"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: __namespace__
name: __pvcName__
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
---
apiVersion: v1
kind: Pod
metadata:
name: test-pvc-pod
namespace: __namespace__
spec:
volumes:
- name: test-pvc-volume
persistentVolumeClaim:
claimName: __pvcName__
containers:
- name: test-pvc-image
image: nginx
volumeMounts:
- mountPath: "/tmp/foo"
name: test-pvc-volume
`

func renderFixtureYamlTemplate(namespace, pvcName string) string {
return strings.Replace(strings.Replace(pvcFixtureYamlTemplate, "__namespace__", namespace, -1), "__pvcName__", pvcName, -1)
}

0 comments on commit ee5aa0e

Please sign in to comment.