func/test/e2e/scenario_config-volumes_tes...

306 lines
10 KiB
Go

//go:build e2e && !windows
package e2e
import (
"context"
"fmt"
"path"
"path/filepath"
"runtime"
"testing"
"time"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"gotest.tools/v3/assert"
"knative.dev/func/test/common"
"knative.dev/func/test/testhttp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"knative.dev/func/pkg/k8s"
)
// setupConfigVolumesTest add to cluster config maps and secrets that will be used as volumes
// during tests
func setupConfigVolumesTest(t *testing.T) {
config, err := k8s.GetClientConfig().ClientConfig()
if err != nil {
t.Fatal(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
namespace, _, _ := k8s.GetClientConfig().Namespace()
// Add Config Map
configMap := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test-cm-volume"},
Data: map[string]string{
"config-key1": "Hi",
"config-key2": "Hello",
},
}
_, err = clientset.CoreV1().ConfigMaps(namespace).Create(context.Background(), &configMap, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
// Add Secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret-volume"},
Data: map[string][]byte{
"secret-key1": []byte("pw1"),
"secret-key2": []byte("pw2"),
},
}
_, err = clientset.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
}
// tearDownConfigVolumesTest removes cluster config maps and secrets used by the test
func tearDownConfigVolumesTest() {
config, _ := k8s.GetClientConfig().ClientConfig()
clientset, _ := kubernetes.NewForConfig(config)
namespace, _, _ := k8s.GetClientConfig().Namespace()
_ = clientset.CoreV1().ConfigMaps(namespace).Delete(context.Background(), "test-cm-volume", metav1.DeleteOptions{})
_ = clientset.CoreV1().Secrets(namespace).Delete(context.Background(), "test-secret-volume", metav1.DeleteOptions{})
}
// ConfigVolumesAdd generates a go function to test `func config volumes add` with user input
func ConfigVolumesAdd(knFunc *common.TestInteractiveCmd) func(userInput ...string) {
return PrepareInteractiveCommand(knFunc, "config", "volumes", "add")
}
// ConfigVolumesRemove generates a go function to test `func config volumes remove` with user input
func ConfigVolumesRemove(knFunc *common.TestInteractiveCmd) func(userInput ...string) {
return PrepareInteractiveCommand(knFunc, "config", "volumes", "remove")
}
// TestConfigVolumes verifies configMaps and secrets were properly mounted as volumes and accessible to the function
// Test consist reproduce the user experience to add volumes (both config and secrets) and deploy a function that
// makes use of the data/
// It setup "configMaps" and "secrets" on the cluster. A custom kn Function template (from a remote repository)
// is used to validate the data can be accessed from the deployed function perspective.
func TestConfigVolumes(t *testing.T) {
setupConfigVolumesTest(t)
defer tearDownConfigVolumesTest()
knFunc := common.NewTestShellInteractiveCmd(t)
knFunc.TestCmd.ShouldDumpOnSuccess = false
knFunc.CommandSleepInterval = time.Millisecond * 1500
// On When...
funcName := "test-config-volumes"
funcPath := filepath.Join(t.TempDir(), funcName)
_, thisfile, _, _ := runtime.Caller(0)
testTemplateFolder := path.Join(path.Dir(thisfile), "..", "templates")
knFunc.TestCmd.Exec("create",
"--language", "go",
"--template", "testvolumes",
"--repository", "file://"+testTemplateFolder,
funcPath)
knFunc.TestCmd.SourceDir = funcPath
/*
? What do you want to mount as a Volume? [Use arrows to move, type to filter]
> ConfigMap
Secret
*/
configVolumesAdd := ConfigVolumesAdd(knFunc)
configVolumesAdd(
enter, // > ConfigMap
"test-cm-volume", enter, // Which "ConfigMap" do you want to mount?
"/test/cm-volume", enter) // Please specify the path where the ConfigMap should be mounted:
configVolumesAdd(
arrowDown, enter, // > Secret
"test-secret-volume", enter, // Which "Secret" do you want to mount?
"/test/secret-volume", enter) // Please specify the path where the Secret should be mounted:
// Adding unwanted volume entries (to simulate user mistakes)
configVolumesAdd(
enter,
"test-cm-volume", enter,
"/test/bad-cm", enter)
configVolumesAdd(
arrowDown, enter,
"test-secret-volume", enter,
"/test/bad-secret", enter)
// Delete unwanted entries
configVolumesRemove := ConfigVolumesRemove(knFunc)
configVolumesRemove("/bad-secret", enter)
configVolumesRemove("/bad-cm", enter)
// Deploy
knFunc.TestCmd.Exec("deploy", "--registry", common.GetRegistry())
defer knFunc.TestCmd.Exec("delete")
_, functionUrl := common.WaitForFunctionReady(t, funcName)
// Validate
// The function template used by this test will return
// file content for the file specified as a query parameter named 'v'
expectedMap := map[string]string{
"/test/cm-volume/config-key1": "Hi",
"/test/cm-volume/config-key2": "Hello",
"/test/secret-volume/secret-key1": "pw1",
"/test/secret-volume/secret-key2": "pw2",
"/test/bad-cm/config-key1": "",
"/test/bad-secret/secret-key1": "",
}
//functionRespValidator := FuncResponsivenessValidator{}
for expectedVolumeEntry, expectedFileContent := range expectedMap {
targetUrl := fmt.Sprintf("%s?v=%s", functionUrl, expectedVolumeEntry)
_, funcResponse := testhttp.TestGet(t, targetUrl)
assert.Assert(t, funcResponse == expectedFileContent)
}
}
// enableKnativeVolumeExtentions ensures EmptyDir and PersitentVolumeClaim can be used with knative/functions. More at:
// https://knative.dev/docs/serving/configuration/feature-flags/#kubernetes-emptydir-volume
// https://knative.dev/docs/serving/configuration/feature-flags/#kubernetes-persistentvolumeclaim-pvc
func enableKnativeVolumeExtension(t *testing.T) {
config, _ := k8s.GetClientConfig().ClientConfig()
client, _ := kubernetes.NewForConfig(config)
namespace := "knative-serving"
// Enable EmptyDir extended feature for Knative
_, err := client.CoreV1().ConfigMaps(namespace).Patch(context.Background(), "config-features", types.MergePatchType,
[]byte(`{"data":{"kubernetes.podspec-volumes-emptydir":"enabled"}}`),
metav1.PatchOptions{})
if err != nil {
t.Fatal(err)
}
// Enable Persistent Volumes Claim extended feature for Knative
_, err = client.CoreV1().ConfigMaps(namespace).Patch(context.Background(), "config-features", types.MergePatchType,
[]byte(`{"data":{"kubernetes.podspec-persistent-volume-claim":"enabled","kubernetes.podspec-persistent-volume-write":"enabled"}}`),
metav1.PatchOptions{})
if err != nil {
t.Fatal(err)
}
t.Log("Enabled Knative PVC and EmptyDir extensions")
}
// setupTestPvc adds a test Persistent Volume Claim used by PVC test
func setupTestPvc(t *testing.T, pvcName string) {
config, _ := k8s.GetClientConfig().ClientConfig()
client, _ := kubernetes.NewForConfig(config)
namespace, _, _ := k8s.GetClientConfig().Namespace()
// Add Testing PVC
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: pvcName},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{},
},
},
}
pvc.Spec.Resources.Requests[corev1.ResourceStorage] = resource.MustParse("1Mi")
_, err := client.CoreV1().PersistentVolumeClaims(namespace).Create(context.Background(), pvc, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
t.Log("Created test PVC " + pvcName)
t.Cleanup(func() {
client.CoreV1().PersistentVolumeClaims(namespace).Delete(context.Background(), pvcName, metav1.DeleteOptions{})
})
}
// TestConfigVolumesPvcEmptyDir verifies PersistentVolumeClaim and EmptyDir Volumes can be added and can be accessible
// by the function by writing and reading an arbitrary file on the volumes
func TestConfigVolumesPvcEmptyDir(t *testing.T) {
enableKnativeVolumeExtension(t)
pvcName := "test-pvc-" + rand.String(5)
setupTestPvc(t, pvcName)
knFunc := common.NewTestShellInteractiveCmd(t)
knFunc.TestCmd.ShouldDumpOnSuccess = false
knFunc.CommandSleepInterval = time.Millisecond * 1500
// On When...
funcName := "test-config-vol-pvc"
funcPath := filepath.Join(t.TempDir(), funcName)
_, thisfile, _, _ := runtime.Caller(0)
testTemplateFolder := path.Join(path.Dir(thisfile), "..", "templates")
knFunc.TestCmd.Exec("create",
"--language", "go",
"--template", "testvolumes",
"--repository", "file://"+testTemplateFolder,
funcPath)
knFunc.TestCmd.SourceDir = funcPath
/*
? What do you want to mount as a Volume? [Use arrows to move, type to filter]
*/
configVolumesAdd := ConfigVolumesAdd(knFunc)
configVolumesAdd(
"PersistentVolumeClaim", enter,
pvcName, enter, // ? Which "PersistentVolumeClaim" do you want to mount?
"/test/pvc", enter, // ? Please specify the path where the PersistentVolumeClaim should be mounted:
"N", enter) // ? Is this volume read-only? (y/N)
configVolumesAdd(
"EmptyDir", enter,
"/test/empty-dir", enter) // ? Please specify the path where the EmptyDir should be mounted:
// Deploy
knFunc.TestCmd.Exec("deploy", "--registry", common.GetRegistry())
t.Cleanup(func() {
knFunc.TestCmd.Exec("delete")
})
_, functionUrl := common.WaitForFunctionReady(t, funcName)
// Validation
// The function template used by this test will help by read/write a file to the volume
filesPath := []string{"/test/pvc/a.txt", "/test/empty-dir/a.txt"}
fileContentToWrite := "A_CONTENT"
for _, filePath := range filesPath {
// - Write to volume test
targetUrl := fmt.Sprintf("%s?v=%s&w=%s", functionUrl, filePath, fileContentToWrite)
_, funcResponse := testhttp.TestGet(t, targetUrl)
assert.Assert(t, funcResponse == fileContentToWrite, "Write volume test failed writing to file %s", filePath)
// - Read from volume test
targetUrl = fmt.Sprintf("%s?v=%s", functionUrl, filePath)
_, funcResponse = testhttp.TestGet(t, targetUrl)
assert.Assert(t, funcResponse == fileContentToWrite,
"Read volume test failed. File %s content should be %s but it is %s", filePath, fileContentToWrite, funcResponse)
}
}