diff --git a/modules/k8s/errors.go b/modules/k8s/errors.go index 9bd0d7a03..da87bb640 100644 --- a/modules/k8s/errors.go +++ b/modules/k8s/errors.go @@ -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) @@ -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{} diff --git a/modules/k8s/persistent_volume_claim.go b/modules/k8s/persistent_volume_claim.go new file mode 100644 index 000000000..ca8b5a1b3 --- /dev/null +++ b/modules/k8s/persistent_volume_claim.go @@ -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 +} diff --git a/modules/k8s/persistent_volume_claim_test.go b/modules/k8s/persistent_volume_claim_test.go new file mode 100644 index 000000000..5c426640a --- /dev/null +++ b/modules/k8s/persistent_volume_claim_test.go @@ -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) +}