datahaven/test/cli/handlers/deploy/storagehub.ts
Steve Degosserie 5988691a2f
feat: Add deployment charts for StorageHub MSP, BSP & Indexer nodes (Local & Stagenet envs) (#160)
## Summary

This PR adds comprehensive Kubernetes deployment infrastructure for
StorageHub components, enabling deployment of the full StorageHub
network stack (MSP, BSP, Indexer, and Fisherman nodes) alongside
DataHaven nodes in both local and stagenet environments.

### What's Added

**1. New Helm Chart: StorageHub MSP Backend API**
(`deploy/charts/backend/`)
- REST API service for StorageHub operations
- Connects to PostgreSQL database for indexed blockchain data
- Connects to RPC nodes for real-time blockchain queries
- Configurable via TOML configuration file
- Supports environment-specific overrides
- Includes comprehensive documentation

**2. StorageHub Node Deployment Charts**
(`deploy/charts/node/storagehub/`)
- **MSP Node** (`sh-mspnode`): Main Service Provider nodes with charging
capabilities
- **BSP Node** (`sh-bspnode`): Backup Service Provider nodes for
redundancy
- **Indexer Node** (`sh-idxnode`): Full indexing node with PostgreSQL
integration
- **Fisherman Node** (`sh-fisherman`): Network monitoring and
verification node

**3. Environment Configurations**
- **Local environment** (`deploy/environments/local/`): Development
setup with hostpath storage
- **Stagenet environment** (`deploy/environments/stagenet/`):
Production-like setup with AWS EBS
- PostgreSQL database configurations for Indexer and Fisherman nodes
- Proper service discovery and network configuration

**4. Enhanced CLI Tooling** (`test/cli/`)
- New `deploy storagehub` command for deploying StorageHub components
- Updated `launch storagehub` command for local testing
- Interactive deployment with environment selection
- Automatic database provisioning via Bitnami PostgreSQL charts

**5. Node Configuration Improvements**
- Fork-aware transaction pool for DH boot & validator nodes
- Unsafe RPC methods exposed on MSP nodes (for provider operations)
- JWT secret support for MSP Backend authentication
- ECDSA key scheme for StorageHub BCSV keys (DataHaven compatibility)

### Architecture

```
StorageHub Stack:
├── MSP Nodes (2 replicas) → Storage providers with charging
├── BSP Nodes (2 replicas) → Backup storage providers
├── Indexer Node → Database indexing + PostgreSQL
├── Fisherman Node → Monitoring + PostgreSQL (shared with Indexer)
└── MSP Backend API → REST API for StorageHub operations
```

### Testing

**Local Testing**:
```bash
cd test
bun cli launch storagehub  # Interactive launcher
# or
bun cli deploy storagehub  # Deploy via Helm
```

**Stagenet Deployment**:
```bash
cd deploy
helm install sh-mspnode ./charts/node \
  -f ./charts/node/storagehub/sh-mspnode.yaml \
  -f ./environments/stagenet/sh-mspnode.yaml \
  -n datahaven-stagenet
```

### Breaking Changes

None - This is purely additive infrastructure.

### Migration Notes

For existing deployments:
1. DataHaven nodes now use `--pool-type fork-aware` flag
2. Bootnode and validator node configs updated accordingly
3. No action required for existing DataHaven-only deployments
2025-10-21 23:18:50 +03:00

216 lines
6.8 KiB
TypeScript

import path from "node:path";
import { $ } from "bun";
import invariant from "tiny-invariant";
import { logger, printDivider, printHeader } from "utils";
import { waitFor } from "utils/waits";
import { isNetworkReady } from "../../../launcher/datahaven";
import type { LaunchedNetwork } from "../../../launcher/types/launchedNetwork";
import { forwardPort } from "../common/kubernetes";
import type { DeployOptions } from ".";
/**
* Deploys StorageHub components (MSP, BSP, Indexer, Fisherman nodes and databases) in a Kubernetes namespace.
*
* @param options - Configuration options for launching the network.
* @param launchedNetwork - An instance of LaunchedNetwork to track the network's state.
* @returns A promise that resolves when all StorageHub components are deployed.
*/
export const deployStorageHubComponents = async (
options: DeployOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
if (options.skipStorageHub) {
logger.info("🏳️ Skipping StorageHub components deployment");
printDivider();
return;
}
printHeader("Deploying StorageHub Components");
invariant(options.datahavenImageTag, "❌ DataHaven image tag not defined");
if (!options.dockerUsername) {
await checkTagExists(options.datahavenImageTag);
}
// Deploy StorageHub Indexer database first (Indexer PostgreSQL database)
await deployStorageHubDatabase(options, launchedNetwork);
// Deploy StorageHub nodes (MSP, BSP, Indexer, Fisherman)
await deployStorageHubNodes(options, launchedNetwork);
// Deploy StorageHub MSP Backend API
await deployStorageHubBackend(options, launchedNetwork);
await registerStorageHubNodes(launchedNetwork);
printDivider();
};
/**
* Deploys StorageHub PostgreSQL databases for Indexer and Fisherman nodes.
*/
const deployStorageHubDatabase = async (
options: DeployOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
logger.info("🗄️ Deploying StorageHub PostgreSQL database...");
const deployDatabase = async (name: string, component: string) => {
const timeout = "3m";
const args = [
"upgrade",
"--install",
name,
"oci://registry-1.docker.io/bitnamicharts/postgresql",
"-f",
`environments/${options.environment}/${component}-db.yaml`,
"-n",
launchedNetwork.kubeNamespace,
"--wait",
"--timeout",
timeout
];
logger.info(`📦 Deploying ${name} database...`);
logger.debug(await $`helm ${args}`.cwd(path.join(process.cwd(), "../deploy")).text());
logger.success(`${name} database deployed successfully`);
};
// Deploy Indexer database
await deployDatabase("sh-indexer-db", "sh-idxnode");
};
/**
* Deploys StorageHub nodes (MSP, BSP, Indexer, Fisherman).
*/
const deployStorageHubNodes = async (
options: DeployOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
logger.info("🚀 Deploying StorageHub nodes...");
const deployNode = async (name: string, component: string) => {
const timeout = "5m";
const args = [
"upgrade",
"--install",
name,
"charts/node",
"-f",
`charts/node/storagehub/${component}.yaml`,
"-f",
`environments/${options.environment}/${component}.yaml`,
"-n",
launchedNetwork.kubeNamespace,
"--wait",
"--timeout",
timeout
];
logger.info(`🏗️ Deploying ${name}...`);
logger.debug(await $`helm ${args}`.cwd(path.join(process.cwd(), "../deploy")).text());
logger.success(`${name} deployed successfully`);
};
// Deploy StorageHub nodes in dependency order
await deployNode("sh-mspnode", "sh-mspnode");
await deployNode("sh-bspnode", "sh-bspnode");
await deployNode("sh-idxnode", "sh-idxnode");
await deployNode("sh-fisherman", "sh-fisherman");
};
/**
* Deploys StorageHub MSP Backend API.
*/
const deployStorageHubBackend = async (
options: DeployOptions,
launchedNetwork: LaunchedNetwork
): Promise<void> => {
logger.info("🚀 Deploying StorageHub MSP Backend API...");
const timeout = "3m";
const args = [
"upgrade",
"--install",
"sh-mspbackend",
"charts/backend",
"-f",
"charts/backend/storagehub/sh-mspbackend.yaml",
"-f",
`environments/${options.environment}/sh-mspbackend.yaml`,
"-n",
launchedNetwork.kubeNamespace,
"--wait",
"--timeout",
timeout
];
logger.debug(await $`helm ${args}`.cwd(path.join(process.cwd(), "../deploy")).text());
logger.success("StorageHub MSP Backend API deployed successfully");
};
/**
* Waits for StorageHub Indexer node to be ready and registers nodes in LaunchedNetwork.
*/
const registerStorageHubNodes = async (launchedNetwork: LaunchedNetwork): Promise<void> => {
// Forward port from indexer node to localhost for health checks
const indexerPort = 9944;
const { cleanup: indexerPortForwardCleanup } = await forwardPort(
"sh-idxnode-0",
indexerPort,
indexerPort + 100, // Use different local port to avoid conflicts
launchedNetwork
);
// Wait for the StorageHub Indexer to start
logger.info("⌛️ Waiting for StorageHub Indexer to start...");
const timeoutMs = 5000; // 5 second timeout
const delayMs = 5000; // 5 second delay between iterations
await waitFor({
lambda: async () => {
logger.info(`📡 Checking if StorageHub Indexer is ready (timeout: ${timeoutMs / 1000}s)...`);
const isReady = await isNetworkReady(indexerPort + 100, timeoutMs);
if (!isReady) {
logger.info(
`⌛️ StorageHub Indexer not ready, waiting ${delayMs / 1000}s to check again...`
);
}
return isReady;
},
iterations: 12, // 12 iterations of 5 + 5 = 2 minutes
delay: delayMs,
errorMessage: "StorageHub Indexer not ready"
});
logger.success("StorageHub Indexer is ready");
// Clean up the port forwarding
await indexerPortForwardCleanup();
// Register StorageHub nodes in LaunchedNetwork
launchedNetwork.addContainer("sh-mspnode-0", { ws: 9944 });
launchedNetwork.addContainer("sh-bspnode-0", { ws: 9944 });
launchedNetwork.addContainer("sh-idxnode-0", { ws: 9944 });
launchedNetwork.addContainer("sh-fisherman-0", { ws: 9944 });
logger.info("📝 StorageHub nodes successfully registered in launchedNetwork.");
};
/**
* Checks if an image exists in Docker Hub.
*
* @param tag - The tag of the image to check.
* @returns A promise that resolves when the image is found.
*/
const checkTagExists = async (tag: string) => {
const cleanTag = tag.trim();
logger.debug(`Checking if image ${cleanTag} is available on Docker Hub`);
const result = await $`docker manifest inspect ${cleanTag}`.nothrow().quiet();
invariant(
result.exitCode === 0,
`❌ Image ${tag} not found.\n Does this image exist?\n Are you logged and have access to the repository?`
);
logger.success(`Image ${cleanTag} found on Docker Hub`);
};