Skip to content

Vault

El VaultContainer es un componente de la librería CodeDesignPlus.Net.xUnit que proporciona un contenedor Docker para ejecutar HashiCorp Vault, una herramienta para gestionar secretos y proteger información sensible. Este componente facilita la creación de pruebas unitarias que requieren una instancia de Vault, ofreciendo un entorno de pruebas aislado y reproducible.

¿Cómo Funciona?


El VaultContainer utiliza Docker Compose para definir y gestionar el contenedor Vault. Al inicializar una instancia de esta clase, se genera un archivo docker-compose.yml temporal con la configuración necesaria para ejecutar el contenedor. Luego, el contenedor se inicia y se detiene automáticamente al final de la ejecución de las pruebas.

El proceso general de funcionamiento es el siguiente:

  1. Configuración del Contenedor: El método Build() define la configuración específica del contenedor Vault, incluyendo la ruta al archivo docker-compose.yml, opciones de reinicio forzado, eliminación de contenedores huérfanos, parada al finalizar y un nombre de servicio alternativo generado dinámicamente para evitar conflictos. También se genera un identificador unico para el archivo de configuración.
  2. Creación del Contenedor: Se crea una instancia de DockerComposeCompositeService usando la configuración definida.
  3. Inicio y Detención del Contenedor: El contenedor se inicia cuando se crea una instancia de la clase VaultContainer y se detiene automáticamente cuando la instancia se elimina, asegurando la limpieza del entorno.
  4. Obtención de Credenciales: Una vez el contenedor esta inicializado se procede a la lectura del archivo generado con las credenciales de Vault.
  5. Deserialización de Credenciales: Las credenciales son deserializadas a la clase VaultCredentials para su uso posterior.

Container


El VaultContainer hereda de la clase DockerCompose, la cual proporciona la lógica base para interactuar con Docker Compose. La clase utiliza la clase DockerComposeCompositeService de la librería Testcontainers para iniciar y detener contenedores docker. La configuración del contenedor se define en el método Build() que crea un archivo docker-compose.yml con los parámetros necesarios para la ejecución del contenedor Vault. El archivo docker-compose.yml se encuentra dentro del directorio Containers/VaultContainer.

namespace CodeDesignPlus.Net.xUnit.Containers.VaultContainer;
/// <summary>
/// Represents a Docker container for SQL Server, managed using Docker Compose.
/// </summary>
public class VaultContainer : DockerCompose
{
public VaultCredentials Credentials { get; private set; }
private readonly string id = Guid.NewGuid().ToString("N");
public VaultContainer() : base()
{
}
/// <summary>
/// Builds the Docker Compose service configuration for the SQL Server container.
/// </summary>
/// <returns>An <see cref="ICompositeService"/> representing the Docker Compose service.</returns>
protected override ICompositeService Build()
{
var file = Path.Combine(Directory.GetCurrentDirectory(), "Containers", "VaultContainer", "docker-compose.yml");
var dockerCompose = new DockerComposeConfig
{
ComposeFilePath = [file],
ForceRecreate = true,
RemoveOrphans = true,
StopOnDispose = true,
AlternativeServiceName = "vault_" + this.id,
EnvironmentNameValue = new Dictionary<string, string>
{
{ "FILE_CREDENTIAL", this.id },
}
};
this.EnableGetPort = true;
this.InternalPort = 8200;
this.ContainerName = $"{dockerCompose.AlternativeServiceName}-vault";
var compose = new DockerComposeCompositeService(base.DockerHost, dockerCompose);
return compose;
}
protected override void OnContainerInitialized()
{
// Wait for the vault container to be ready (RabbitMQ)
Thread.Sleep(30000);
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Containers", "VaultContainer", "shared", "vault-config", $"{this.id}-credentials.json");
if (!File.Exists(filePath))
throw new FileNotFoundException("The file with the credentials was not found.", filePath);
string json = File.ReadAllText(filePath);
this.Credentials = JsonSerializer.Deserialize<VaultCredentials>(json);
}
}
public class VaultCredentials
{
[Newtonsoft.Json.JsonProperty("role_id")]
public string RoleId { get; set; }
[Newtonsoft.Json.JsonProperty("secret_id")]
public string SecretId { get; set; }
}

La clase VaultCollectionFixture se utiliza como un fixture de xUnit, que proporciona una forma de compartir el contenedor entre las pruebas de una colección.

namespace CodeDesignPlus.Net.xUnit.Containers.VaultContainer;
public sealed class VaultCollectionFixture : IDisposable
{
public const string Collection = "Vault Collection";
public VaultContainer Container { get; }
public VaultCollectionFixture()
{
this.Container = new VaultContainer();
}
public void Dispose()
{
this.Container.StopInstance();
}
}

Ejemplo de Uso


Este ejemplo es una prueba unitaria en C# utilizando xUnit para verificar la conexión a un servidor Vault y la correcta recuperación de secretos. La prueba configura un cliente de Vault usando AppRoleAuthMethodInfo para la autenticación, lee un secreto específico y verifica que contiene una clave y un valor esperados.

using CodeDesignPlus.Net.xUnit.Containers.VaultContainer;
using VaultSharp;
using VaultSharp.V1.AuthMethods;
using VaultSharp.V1.AuthMethods.AppRole;
namespace CodeDesignPlus.Net.xUnit.Test;
[Collection(VaultCollectionFixture.Collection)]
public class VaultContainerTest(VaultCollectionFixture fixture)
{
[Fact]
public async Task CheckConnectionService()
{
var authMethod = new AppRoleAuthMethodInfo(fixture.Container.Credentials.RoleId, fixture.Container.Credentials.SecretId);
var vaultClientSettings = new VaultClientSettings($"http://localhost:{fixture.Container.Port}", authMethod);
var vaultClient = new VaultClient(vaultClientSettings);
var kv1Secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync("my-app", mountPoint: "unit-test-keyvalue");
Assert.True(kv1Secret.Data.Data.ContainsKey("Security:ClientId"));
Assert.Equal("a74cb192-598c-4757-95ae-b315793bbbca", kv1Secret.Data.Data["Security:ClientId"].ToString());
}
}

Conclusiones


  • El VaultContainer facilita la creación de pruebas unitarias que requieren una instancia de HashiCorp Vault.
  • Utiliza Docker Compose para la gestión del contenedor, proporcionando una forma sencilla de definir la configuración.
  • Proporciona las credenciales de Vault a través de la propiedad Credentials.
  • La clase VaultCollectionFixture permite compartir un contenedor entre todas las pruebas de una misma colección.
  • El uso del VaultContainer mejora la reproducibilidad y el aislamiento de las pruebas unitarias.
  • El contenedor Vault se inicia automaticamente al comienzo de cada prueba y se detiene una vez termina, esto permite un ambiente de pruebas limpio.

Referencias Adicionales