Go: Testing de aplicaciones Kubernetes con EnvTest
Introducción
Cuando desarrollamos aplicaciones que interactúan con un cluster Kubernetes como operadores o controladores Kubernetes, es esencial asegurarnos de que la lógica de la aplicación se comporte como se espera mediante tests robustos. Los enfoques tradicionales incluyen el uso de un cluster de desarrollo en un entorno de pruebas, un cluster local independiente como Minikube, o incluso aprovechar TestContainers para crear un cluster local bajo demanda.
No obstante, usar un cluster completo requiere muchos recursos, o no es posible ejecutarlo en pipelines de determinados sistemas CI/CD. En muchas ocasiones, no necesitamos un cluster completo para probar nuestra aplicación, especialmente si nuestra aplicación solo se ocupa de la parte declarativa de Kubernetes, creando, actualizando o eliminando recursos. Para estos escenarios, podemos usar el paquete EnvTest de controller-runtime para crear un Kubernetes API server y una instancia de etcd.
En esta publicación, te mostraré cómo usar EnvTest para testear un operador Kubernetes sencillo. También te daré algunos ejemplos prácticos implementando tests para el plugin kubectl kill namespace que escribí en una publicación anterior.
¿Qué es EnvTest?
EnvTest, un paquete del proyecto controller-runtime simplifica el testing de integración para aplicaciones Kubernetes. Proporciona un Kubernetes API server local y una instancia de etcd, lo que permite realizar tests exhaustivos de la aplicación sin necesidad de toda la infraestructura del cluster Kubernetes. Esta flexibilidad permite realizar tests rápidos en entornos locales o integración sin problemas en pipelines CI/CD, todo ello conservando recursos.
Sin embargo, es importante tener en cuenta que EnvTest no es adecuado para aplicaciones que dependen de componentes de Kubernetes como el controller-manager o el kubelet. Por ejemplo, las aplicaciones que necesitan que determinados Pods se programen y estén operativos dentro de un cluster Kubernetes no son adecuadas para este entorno.
Configurando EnvTest
La mayoría de las guías online se basan en que los binarios de EnvTest ya estén instalados en el sistema. Algunas de ellas te muestran cómo descargarlos usando un Makefile, mientras que otras ni siquiera mencionan cómo obtenerlos.
En esta sección, te mostraré cómo usar el paquete EnvTest directamente desde tu código Go, sin depender de ninguna herramienta externa.
Comencemos creando una función setupEnvTest
encargada de configurar el entorno EnvTest:
func setupEnvTest() *envtest.Environment {
// Download EnvTest binaries
// Create a new instance of the EnvTest environment
}
Esta función es responsable de descargar si es necesario los binarios de EnvTest, y de crear una nueva instancia del entorno EnvTest para dichos binarios.
Centrémonos en la primera parte, la descarga de los binarios.
func setupEnvTest() *envtest.Environment {
envTestDir, err := store.DefaultStoreDir()
if err != nil {
panic(err)
}
envTest := &env.Env{
FS: afero.Afero{Fs: afero.NewOsFs()},
Out: os.Stdout,
Client: &remote.Client{
Bucket: "kubebuilder-tools",
Server: "storage.googleapis.com",
},
Platform: versions.PlatformItem{
Platform: versions.Platform{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
},
},
Version: versions.AnyVersion,
Store: store.NewAt(envTestDir),
}
envTest.CheckCoherence()
workflows.Use{}.Do(envTest)
// Create a new instance of the EnvTest environment
}
Vamos a analizar el código paso a paso:
- Primero, obtenemos el directorio por defecto de EnvTest usando la función
store.DefaultStoreDir()
. De esta forma, podemos reutilizar cualquier binario que ya haya sido descargado por otros tests o herramientas externas. - A continuación, creamos una nueva instancia de la estructura
env.Env
. Esta estructura proporciona toda la información necesaria como el sistema operativo, la arquitectura, la versión, etc. para descargar los binarios. - Después, validamos e inicializamos el entorno usando la función
envTest.CheckCoherence()
. - Por último, usamos la función
workflows.Use{}.Do(envTest)
para descargar los binarios si es necesario.
La siguiente parte de la lógica de la función se encarga de crear una nueva instancia del entorno EnvTest:
func setupEnvTest() *envtest.Environment {
// Download EnvTest binaries
versionDir := envTest.Platform.Platform.BaseName(*envTest.Version.AsConcrete())
return &envtest.Environment{
BinaryAssetsDirectory: filepath.Join(envTestDir, "k8s", versionDir),
}
}
Empleamos la función envTest.Platform.Platform.BaseName(*envTest.Version.AsConcrete())
para obtener el directorio que contiene los binarios de la versión de EnvTest que hemos descargado.
A continuación, creamos una nueva instancia de la estructura envtest.Environment
, pasando el directorio donde se encuentran los binarios.
Ahora que ya tenemos la función setupEnvTest
lista, continuemos viendo cómo usarla en nuestros tests.
Usando EnvTest
En esta sección, te mostraré cómo usar la función setupEnvTest
que creamos en la sección anterior para testear el plugin kubectl kill namespace que analicé en una publicación anterior.
Puedes encontrar el código completo de los tests en el archivo killns_test.go
del proyecto.
Ya que el plugin kubectl kill namespace se encarga de eliminar Namespaces, voy a centrarme en testear la lógica de eliminación de Namespaces.
Para ello utilizaré subtests para iniciar el entorno EnvTest una sola vez y ejecutar cada escenario de test en un subtest diferente.
El siguiente fragmento de código muestra las secciones de configuración y de limpieza de la función TestKillNamespace
:
func TestKillNamespace(t *testing.T) {
envTest := setupEnvTest()
envTestConfig, err := envTest.Start()
if err != nil {
t.Errorf("Error starting test environment: %s", err)
return
}
defer func() {
if stopErr := envTest.Stop(); stopErr != nil {
panic(stopErr)
}
}()
// Test with no namespace
// Test with existent namespace
// Test with existent namespace with finalizer
}
Veamos paso a paso el código:
- Primero, llamamos a la función
setupEnvTest
que creamos en la sección anterior para inicializar el entorno EnvTest. - A continuación, iniciamos el entorno llamando a la función
envTest.Start()
. Esta función devuelve una estructurarest.Config
que podemos usar más adelante para inicializar un cliente Kubernetes. - Después, comprobamos si se ha producido algún error al iniciar el entorno. Si es así, fallamos el test y salimos.
- Por último, usamos una sentencia
defer
para detener y eliminar el entorno una vez que finalice el test. De esta forma, nos aseguramos de que el entorno siempre se detiene, incluso si el test falla.
Analicemos ahora el escenario del subtest With existent namespace with finalizer
para ver cómo llegados a este punto usar EnvTest es equivalente a usar un cluster Kubernetes real para operaciones de la API de Kubernetes.
func TestKillNamespace(t *testing.T) {
// Setup EnvTest environment
t.Run("With existent namespace with finalizer", func(t *testing.T) {
client, _ := kubernetes.NewForConfig(envTestConfig)
client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "finalizer",
},
Spec: corev1.NamespaceSpec{Finalizers: []corev1.FinalizerName{"kubernetes"}},
}, metav1.CreateOptions{})
KillNamespace(envTestConfig, "finalizer")
_, err := client.CoreV1().Namespaces().Get(context.TODO(), "finalizer", metav1.GetOptions{})
if err.Error() != "namespaces \"finalizer\" not found" {
t.Errorf("Namespace should have been deleted, but it wasn't")
}
})
}
Veamos las diferentes partes del código:
- Primero, inicializamos un cliente client-go de Kubernetes pasando la estructura
rest.Config
que obtuvimos de la funciónenvTest.Start()
. - A continuación, creamos un nuevo Namespace con un finalizer usando el cliente (assemble/given).
- Después, llamamos a la función
KillNamespace
que queremos testear (act/when). - Por último, comprobamos que el Namespace se ha eliminado intentando obtenerlo usando el cliente (assert/then).
Conclusión
En esta publicación, te he mostrado cómo usar el paquete EnvTest de controller-runtime para testear aplicaciones Kubernetes. He empezado por introducir EnvTest y explicar cuándo es adecuado usarlo. Después, te he mostrado cómo configurar el entorno EnvTest descargando los binarios directamente desde tu código Go. Por último, te he mostrado cómo usar el entorno EnvTest para testear una aplicación Kubernetes implementando tests para el plugin kubectl kill namespace que analicé en una publicación anterior.
Con estos conocimientos ya deberías de ser capaz de usar EnvTest para testear tus aplicaciones Kubernetes.
Puedes encontrar el código fuente completo de los ejemplos que te he mostrado en este artículo en GitHub.