Skip to content

Commit 5fe9009

Browse files
committed
inital fablo export-topology
Signed-off-by: OsamaRab3 <osrab3@gmail.com>
1 parent 23c82cd commit 5fe9009

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { FabloConfigExtended, OrdererGroup } from "../../types/FabloConfigExtended";
2+
3+
const safeId = (id: string): string => id.replace(/[^a-zA-Z0-9_]/g, "_");
4+
const ordererGroupId = (g: OrdererGroup): string => safeId(`ord_group_${g.name}_${g.orderers?.[0].address}`);
5+
const channelId = (channelName: string): string => safeId(`channel_${channelName}`);
6+
const chaincodeId = (ccName: string): string => safeId(`chaincode_${ccName}`);
7+
8+
export function generateMermaidDiagram(config: FabloConfigExtended): string {
9+
const lines: string[] = ["graph LR"];
10+
lines.push("");
11+
lines.push("classDef subgraph_padding fill:none,stroke:none");
12+
13+
// Add organization subgraphs with orderer groups, CA, and peers
14+
config.orgs?.forEach((org) => {
15+
const orgId = safeId(org.domain);
16+
lines.push(`\n subgraph ${orgId} [Organization: ${org.name}<br>${org.domain}]`);
17+
const orgPaddingId = `${orgId}_padding`;
18+
lines.push(` subgraph ${orgPaddingId} [ ]`);
19+
lines.push(" direction RL");
20+
21+
// Orderer groups (nested inside org)
22+
org.ordererGroups?.forEach((group) => {
23+
if (group.orderers && group.orderers.length > 0) {
24+
const consensusLabel = group.consensus ? group.consensus : "";
25+
const groupId = ordererGroupId(group);
26+
lines.push(` subgraph ${groupId} [Orderer Group: ${group.name}<br>${consensusLabel}]`);
27+
const groupPaddingId = `${groupId}_padding`;
28+
lines.push(` subgraph ${groupPaddingId} [ ]`);
29+
lines.push(" direction RL");
30+
group.orderers.forEach((orderer) => {
31+
lines.push(` ${safeId(orderer.address)}[${orderer.address}]`);
32+
});
33+
lines.push(` end`);
34+
lines.push(` class ${groupPaddingId} subgraph_padding`);
35+
lines.push(" end");
36+
}
37+
});
38+
39+
// CA (at same level as orderer groups)
40+
if (org.ca) {
41+
const caAddress = org.ca.address;
42+
const caLabel = org.ca.db ? `${caAddress}<br>${org.ca.db}` : `${caAddress}`;
43+
lines.push(` ${safeId(caAddress)}([${caLabel}])`);
44+
}
45+
46+
// Peers (at same level as orderer groups)
47+
org.peers?.forEach((peer) => {
48+
const peerLabel = `${peer.address}<br>${peer.db.type}`;
49+
lines.push(` ${safeId(peer.address)}[${peerLabel}]`);
50+
});
51+
52+
lines.push(" end");
53+
lines.push(` class ${orgPaddingId} subgraph_padding`);
54+
lines.push(" end");
55+
});
56+
57+
// Add channel subgraphs with chaincodes
58+
config.channels?.forEach((channel) => {
59+
const chId = channelId(channel.name);
60+
lines.push(`\n subgraph ${chId} [Channel: ${channel.name}]`);
61+
const chPaddingId = `${chId}_padding`;
62+
lines.push(` subgraph ${chPaddingId} [ ]`);
63+
64+
// Add chaincodes for this channel (using cylinder shape)
65+
const channelChaincodes = config.chaincodes?.filter((cc) => cc.channel?.name === channel.name) ?? [];
66+
channelChaincodes.forEach((cc) => {
67+
lines.push(` ${chaincodeId(cc.name)}[[Chaincode: ${cc.name}]]`);
68+
});
69+
70+
// Add dummy invisible node for empty channels to ensure visibility
71+
if (channelChaincodes.length === 0) {
72+
const emptyNodeId = `${chId}_empty`;
73+
lines.push(` ${emptyNodeId}[" "]`);
74+
lines.push(` style ${emptyNodeId} fill:#ffffff00,stroke:#ffffff00`);
75+
}
76+
77+
lines.push(" end");
78+
lines.push(` class ${chPaddingId} subgraph_padding`);
79+
lines.push(" end");
80+
});
81+
82+
// Add connections
83+
lines.push("\n %% Connections");
84+
85+
// Connect peers to channels
86+
config.channels?.forEach((channel) => {
87+
const channelIdStr = channelId(channel.name);
88+
89+
channel.orgs?.forEach((orgOnChannel) => {
90+
orgOnChannel.peers?.forEach((peer) => {
91+
lines.push(` ${safeId(peer.address)} --> ${channelIdStr}`);
92+
});
93+
});
94+
});
95+
96+
// Connect channels to orderer groups (reversed direction)
97+
config.channels?.forEach((channel) => {
98+
const channelIdStr = channelId(channel.name);
99+
const ogId = ordererGroupId(channel.ordererGroup);
100+
lines.push(` ${channelIdStr} --> ${ogId}`);
101+
});
102+
103+
return lines.join("\n");
104+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Args, Command } from '@oclif/core'
2+
import parseFabloConfig from "../../utils/parseFabloConfig";
3+
import extendConfig from "../extend-config/extendConfig";
4+
import { generateMermaidDiagram } from "./generateMermaidDiagram";
5+
import { FabloConfigExtended } from "../../types/FabloConfigExtended";
6+
import * as fs from "fs";
7+
import * as path from "path";
8+
9+
export default class ExportTopology extends Command {
10+
static override description = 'export-network-topology '
11+
private fabloConfigPath: string = "";
12+
private outputFile: string = "";
13+
14+
static override args = {
15+
config: Args.string({ description: "Fablo config file path",
16+
required: false,
17+
default: 'fablo-config.json'
18+
}),
19+
output: Args.string({ description: "Output Mermaid file path",
20+
required: false,
21+
default: 'network-topology.mmd'
22+
}),
23+
24+
}
25+
async writing(): Promise<void> {
26+
try {
27+
if (!fs.existsSync(this.fabloConfigPath)) {
28+
throw new Error(`Configuration file not found: ${this.fabloConfigPath}`);
29+
}
30+
31+
const configContent = fs.readFileSync(this.fabloConfigPath, 'utf-8');
32+
if (!configContent) {
33+
throw new Error(`Failed to read configuration file: ${this.fabloConfigPath}`);
34+
}
35+
36+
const json = parseFabloConfig(configContent);
37+
const configExtended: FabloConfigExtended = extendConfig(json);
38+
const mermaidDiagram = generateMermaidDiagram(configExtended);
39+
const outputDir = path.dirname(this.outputFile);
40+
if (!fs.existsSync(outputDir)) {
41+
fs.mkdirSync(outputDir, { recursive: true });
42+
}
43+
fs.writeFileSync(this.outputFile, mermaidDiagram);
44+
this.log(`✅ Network topology exported to ${this.outputFile}`);
45+
46+
} catch (error: unknown) {
47+
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
48+
this.log(`❌ Error: ${errorMessage}`);
49+
throw error;
50+
}
51+
}
52+
53+
public async run(): Promise<void> {
54+
const { args } = await this.parse(ExportTopology)
55+
const arg0 = args.config!;
56+
const arg1 = args.output!;
57+
58+
this.fabloConfigPath = path.isAbsolute(arg0) ? arg0 : path.resolve(process.cwd(), arg0);
59+
this.outputFile = path.isAbsolute(arg1) ? arg1 : path.resolve(process.cwd(), arg1);
60+
61+
await this.writing();
62+
}
63+
}

0 commit comments

Comments
 (0)