Skip to content

Commit

Permalink
Merge pull request #1284 from kaisoz/add-k8s-persistent-volume-support
Browse files Browse the repository at this point in the history
Add support for Persistent Volumes
  • Loading branch information
denis256 authored May 10, 2023
2 parents 56940d5 + 69a9ee9 commit bf85889
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
16 changes: 16 additions & 0 deletions modules/k8s/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ 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 Down
103 changes: 103 additions & 0 deletions modules/k8s/persistent_volume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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"
)

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

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

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

// GetPersistentVolume returns a Kubernetes PersistentVolume resource with the given name. This will fail the test if there is an error.
func GetPersistentVolume(t testing.TestingT, options *KubectlOptions, name string) *corev1.PersistentVolume {
pv, err := GetPersistentVolumeE(t, options, name)
require.NoError(t, err)
return pv
}

// GetPersistentVolumeE returns a Kubernetes PersistentVolume resource with the given name.
func GetPersistentVolumeE(t testing.TestingT, options *KubectlOptions, name string) (*corev1.PersistentVolume, error) {
clientset, err := GetKubernetesClientFromOptionsE(t, options)
if err != nil {
return nil, err
}
return clientset.CoreV1().PersistentVolumes().Get(context.Background(), name, metav1.GetOptions{})
}

// WaitUntilPersistentVolumeInStatus waits until the given Persistent Volume 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 WaitUntilPersistentVolumeInStatus(t testing.TestingT, options *KubectlOptions, pvName string, pvStatusPhase *corev1.PersistentVolumePhase, retries int, sleepBetweenRetries time.Duration) {
require.NoError(t, WaitUntilPersistentVolumeInStatusE(t, options, pvName, pvStatusPhase, retries, sleepBetweenRetries))
}

// WaitUntilPersistentVolumeInStatusE waits until the given PersistentVolume is in the given status phase,
// retrying the check for the specified amount of times, sleeping
// for the provided duration between each try.
func WaitUntilPersistentVolumeInStatusE(
t testing.TestingT,
options *KubectlOptions,
pvName string,
pvStatusPhase *corev1.PersistentVolumePhase,
retries int,
sleepBetweenRetries time.Duration,
) error {
statusMsg := fmt.Sprintf("Wait for Persistent Volume %s to be '%s'", pvName, *pvStatusPhase)
message, err := retry.DoWithRetryE(
t,
statusMsg,
retries,
sleepBetweenRetries,
func() (string, error) {
pv, err := GetPersistentVolumeE(t, options, pvName)
if err != nil {
return "", err
}
if !IsPersistentVolumeInStatus(pv, pvStatusPhase) {
return "", NewPersistentVolumeNotInStatusError(pv, pvStatusPhase)
}
return fmt.Sprintf("Persistent Volume is now '%s'", *pvStatusPhase), nil
},
)
if err != nil {
logger.Logf(t, "Timeout waiting for PersistentVolume to be '%s': %s", *pvStatusPhase, err)
return err
}
logger.Logf(t, message)
return nil
}

// IsPersistentVolumeInStatus returns true if the given PersistentVolume is in the given status phase
func IsPersistentVolumeInStatus(pv *corev1.PersistentVolume, pvStatusPhase *corev1.PersistentVolumePhase) bool {
return pv != nil && pv.Status.Phase == *pvStatusPhase
}
109 changes: 109 additions & 0 deletions modules/k8s/persistent_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//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 (
"fmt"
"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 TestListPersistentVolumesReturnsAllPersistentVolumes(t *testing.T) {
t.Parallel()

numPvFound := 0
pvNames := map[string]struct{}{
strings.ToLower(random.UniqueId()): {},
strings.ToLower(random.UniqueId()): {},
strings.ToLower(random.UniqueId()): {},
}

options := NewKubectlOptions("", "", "")
for pvName := range pvNames {
pv := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)
defer KubectlDeleteFromString(t, options, pv)
KubectlApplyFromString(t, options, pv)
}

pvs := ListPersistentVolumes(t, options, metav1.ListOptions{})
for _, pv := range pvs {
if _, ok := pvNames[pv.Name]; ok {
numPvFound++
}
}

require.Equal(t, numPvFound, len(pvNames))
}

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

options := NewKubectlOptions("", "", "")
pvs := ListPersistentVolumes(t, options, metav1.ListOptions{})
require.Equal(t, 0, len(pvs))
}

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

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

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

pvName := strings.ToLower(random.UniqueId())
options := NewKubectlOptions("", "", "")
configData := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)
defer KubectlDeleteFromString(t, options, configData)
KubectlApplyFromString(t, options, configData)

pv := GetPersistentVolume(t, options, pvName)
require.Equal(t, pv.Name, pvName)
}

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

pvName := strings.ToLower(random.UniqueId())
pvAvailableStatusPhase := corev1.VolumeAvailable

options := NewKubectlOptions("", "", pvName)
configData := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)
KubectlApplyFromString(t, options, configData)
defer KubectlDeleteFromString(t, options, configData)

WaitUntilPersistentVolumeInStatus(t, options, pvName, &pvAvailableStatusPhase, 60, 1*time.Second)
}

const PvFixtureYamlTemplate = `---
apiVersion: v1
kind: PersistentVolume
metadata:
name: %s
spec:
capacity:
storage: 10Mi
accessModes:
- ReadWriteOnce
hostPath:
path: "/tmp/%s"
`

0 comments on commit bf85889

Please sign in to comment.