datahaven/test/cli/handlers/deploy/cleanup.ts
Facundo Farall 9b311e00ef
test: 🏗️ Setup e2e testing framework (#104)
## Implement E2E Testing Framework with Isolated Networks

### Summary
Refactors the existing E2E testing infrastructure to provide isolated
test environments with parallel execution support. Each test suite now
runs in its own network namespace, preventing resource conflicts.

### Key Changes
- **New Testing Framework** (`test/framework/`): Base classes for test
lifecycle management with automatic setup/teardown
- **Launcher Module** (`test/launcher/`): Extracted network
orchestration logic from CLI handlers for reusability
- **Parallel Execution**: Added `test-parallel.ts` script with
concurrency limits to prevent resource exhaustion
- **Test Isolation**: Each suite gets unique network IDs (format:
`suiteName-timestamp`) and Docker networks
- **Improved Test Organization**: Migrated tests to new framework,
deprecated old test structure

### Test Improvements
- Added 4 new test suites demonstrating framework usage. :
  - `contracts.test.ts` - Smart contract deployment/interaction
  - `datahaven-substrate.test.ts` - Substrate API operations  
  - `cross-chain.test.ts` - Snowbridge cross-chain messaging
  - `ethereum-basic.test.ts` - Ethereum network operations

> [!WARNING]
The test suites themselves are bad and shouldn't be consider examples of
good tests. They were AI generated just to test the concurrency of test
runners

### Documentation
- Added comprehensive framework overview (`E2E_FRAMEWORK_OVERVIEW.md`)
- Updated README with parallel testing commands
- Added test patterns and best practices

### Breaking Changes
- Old test suites moved to `e2e - DEPRECATED/` directory
- Test execution now requires extending `BaseTestSuite` class

### Testing
Run tests with: `bun test:e2e` or `bun test:e2e:parallel` (with
concurrency limits)

### TODO
- [ ] Implement good test examples.
- [ ] Implement useful test utils (like waiting for an event to show up
in DataHaven or Ethereum).
- [ ] Enforce tests with CI (currently cannot be done due to
intermittent error when sending a transaction with PAPI).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
2025-07-16 18:51:07 +02:00

169 lines
6.9 KiB
TypeScript

import { $ } from "bun";
import invariant from "tiny-invariant";
import { logger, printDivider, printHeader } from "utils";
import { waitFor } from "utils/waits";
import { checkKurtosisEnclaveRunning } from "../../../launcher/kurtosis";
import type { LaunchedNetwork } from "../../../launcher/types/launchedNetwork";
import type { DeployOptions } from ".";
export const cleanup = async (
options: DeployOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
printHeader("Cleaning up");
if (options.skipCleanup) {
logger.info("🏳️ Skipping cleanup");
printDivider();
return;
}
if (options.isPrivateNetwork) {
await checkAndCleanKurtosisDeployment(options);
}
await checkAndCleanHelmReleases(launchedNetwork);
printDivider();
};
/**
* Checks for existing Kurtosis deployment and removes the specified enclave if found.
*
* This function performs a cleanup operation before deployment by:
* 1. Verifying that the Kurtosis gateway process is running (required for Kubernetes integration)
* 2. Listing all running Kurtosis enclaves
* 3. Checking if the specified enclave exists
* 4. Removing the enclave if found to ensure a clean deployment environment
*
* The function ensures that any existing Kurtosis enclave with the same name is properly
* cleaned up before starting a new deployment, preventing conflicts and stale resources.
*
* @param options - Deployment configuration options
* @param options.kurtosisEnclaveName - The name of the Kurtosis enclave to check for and remove.
* Must be defined in the options object.
*
* @returns Promise<void> - Resolves when all cleanup operations are complete
*
* @throws {Error} Throws if:
* - The Kurtosis gateway process is not running (required for Kubernetes integration)
* - Kurtosis commands fail (e.g., Kurtosis not installed, insufficient permissions)
* - Network connectivity issues prevent Kurtosis API access
*/
const checkAndCleanKurtosisDeployment = async (options: DeployOptions): Promise<void> => {
logger.info("☸️ Checking for existing Kurtosis deployment in Kubernetes...");
// Check if the Kurtosis gateway process is running.
const { exitCode, stdout } = await $`pgrep -f "kurtosis gateway"`.nothrow().quiet();
if (exitCode !== 0) {
logger.error(
"❌ `kurtosis gateway` process not found running. This is required for Kurtosis to work with Kubernetes."
);
throw new Error("Kurtosis gateway process not found running.");
}
logger.debug(`Kurtosis gateway process found running: ${stdout}`);
// Check if Kurtosis enclave is running.
if (await checkKurtosisEnclaveRunning(options.kurtosisEnclaveName)) {
logger.info(`🔎 Found Kurtosis enclave ${options.kurtosisEnclaveName} running.`);
} else {
logger.info(`🤷‍ No Kurtosis enclave ${options.kurtosisEnclaveName} found running.`);
return;
}
logger.info("🪓 Removing Kurtosis enclave...");
logger.debug(await $`kurtosis enclave rm ${options.kurtosisEnclaveName} -f`.text());
// Wait for the underlying Kubernetes namespace to be fully deleted
const kubernetesNamespace = `kt-${options.kurtosisEnclaveName}`;
await waitForNamespaceDeletion(kubernetesNamespace);
logger.success(`Kurtosis enclave ${options.kurtosisEnclaveName} removed successfully.`);
};
/**
* Checks for existing DataHaven Helm releases in the specified Kubernetes namespace and removes them.
*
* This function performs a cleanup operation before deployment by:
* 1. Listing all Helm releases in the target namespace
* 2. Identifying any existing DataHaven releases
* 3. Uninstalling each release individually
* 4. Logging the progress and results of each operation
*
* The function ensures a clean deployment environment by removing any conflicting
* or stale Helm releases that might interfere with the new deployment.
*
* @param options - Deployment configuration options
* @param options.kubeNamespace - The Kubernetes namespace to check for Helm releases.
* Must be defined or the function will throw an error.
*
* @returns Promise<void> - Resolves when all cleanup operations are complete
*
* @throws {Error} Throws if:
* - The kubeNamespace is not defined in options
* - Helm commands fail (e.g., Helm not installed, insufficient permissions)
* - Network connectivity issues prevent Kubernetes API access
*/
const checkAndCleanHelmReleases = async (launchedNetwork: LaunchedNetwork): Promise<void> => {
logger.info("☸️ Checking for existing DataHaven Helm releases in Kubernetes...");
invariant(launchedNetwork.kubeNamespace, "❌ Kubernetes namespace not defined");
try {
const releaseListOutput = await $`helm list -q -n ${launchedNetwork.kubeNamespace}`.text();
const releases = releaseListOutput
.trim()
.split("\n")
.filter((r) => r.length > 0);
if (releases.length > 0) {
logger.info(
`🔎 Found existing DataHaven Helm releases: ${releases.join(", ")}. Uninstalling...`
);
for (const release of releases) {
logger.info(`🪓 Uninstalling Helm release: ${release} in namespace datahaven...`);
await $`helm uninstall ${release} -n ${launchedNetwork.kubeNamespace}`.text();
logger.success(`Helm release ${release} uninstalled successfully.`);
}
} else {
logger.info("👍 No existing DataHaven Helm releases found in namespace datahaven.");
}
} catch (error) {
logger.error(
`❌ Failed to check or clean Kubernetes Helm releases: ${error}. This may be expected if Helm is not installed or not configured. Proceeding...`
);
throw error;
}
};
/**
* Waits for a Kubernetes namespace to be fully deleted.
* This is necessary because namespace deletion in Kubernetes is asynchronous
* and Kurtosis may fail to create a new enclave if the namespace is still being deleted.
*
* @param namespaceName - The name of the Kubernetes namespace to wait for deletion
* @returns Promise<void> - Resolves when the namespace is fully deleted or doesn't exist
*/
const waitForNamespaceDeletion = async (namespaceName: string): Promise<void> => {
logger.info(`⌛️ Waiting for Kubernetes namespace ${namespaceName} to be fully deleted...`);
await waitFor({
lambda: async () => {
try {
const { exitCode } = await $`kubectl get namespace ${namespaceName}`.nothrow().quiet();
// If kubectl get namespace returns non-zero exit code, the namespace doesn't exist
return exitCode !== 0;
} catch (error) {
// If kubectl command fails, assume namespace is deleted or kubectl is not available
logger.debug(`kubectl command failed: ${error}. Assuming namespace is deleted.`);
return true;
}
},
iterations: 120, // Wait up to 2 minutes
delay: 1000, // 1 second between checks
errorMessage: "Kubernetes namespace not deleted"
});
logger.success(`Kubernetes namespace ${namespaceName} fully deleted.`);
};