Skip to content

Diagram enhancements #578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ jobs:
run: |
bin/hhfab init -v --dev -m ${{ matrix.fabricmode }} --include-onie=${{ matrix.includeonie }} --gateway=${{ matrix.gateway }}
bin/hhfab vlab gen -v
bin/hhfab diagram -f mermaid
export HHFAB_JOIN_TOKEN=$(openssl rand -base64 24)
bin/hhfab vlab up -v --ready inspect --ready setup-vpcs --ready test-connectivity --ready exit --mode=${{ matrix.buildmode }}

Expand All @@ -328,6 +329,13 @@ jobs:
name: vlab--${{ env.slug }}--registry
path: .zot/log

- name: Upload mermaid diagram
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: vlab--${{ env.slug }}--mermaid
path: result/diagram.mmd

- name: Setup tmate session for debug
if: ${{ failure() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
uses: mxschmitt/action-tmate@v3
Expand Down Expand Up @@ -554,6 +562,7 @@ jobs:
run: |
source "./lab-ci/envs/$KUBE_NODE/source.sh"
bin/hhfab init -v --dev --include-onie=${{ matrix.includeonie }} -w "./lab-ci/envs/$KUBE_NODE/wiring.yaml"
bin/hhfab diagram -f mermaid

# TODO: make controls restricted again when we figure out how to get NTP upstream working for isolated VMs
bin/hhfab vlab up -v --ready switch-reinstall --ready inspect --ready setup-vpcs --ready test-connectivity --ready exit --mode=${{ matrix.buildmode }} --controls-restricted=false
Expand All @@ -572,6 +581,13 @@ jobs:
name: hlab--${{ env.slug }}--registry
path: .zot/log

- name: Upload mermaid diagram
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: hlab--${{ env.slug }}--mermaid
path: result/diagram.mmd

- name: Setup tmate session for debug
if: ${{ failure() && github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
uses: mxschmitt/action-tmate@v3
Expand Down
7 changes: 6 additions & 1 deletion cmd/hhfab/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,11 @@ func Run(ctx context.Context) error {
func(item diagram.StyleType, _ int) string { return string(item) }), ", "),
Value: string(diagram.StyleTypeDefault),
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "output file path for the generated diagram (default: result/diagram.{format})",
},
&cli.BoolFlag{
Name: "live",
Usage: "load resources from actually running API instead of the config file (fab.yaml and include/*)",
Expand All @@ -614,7 +619,7 @@ func Run(ctx context.Context) error {
Action: func(c *cli.Context) error {
format := diagram.Format(strings.ToLower(c.String("format")))
styleType := diagram.StyleType(c.String("style"))
if err := hhfab.Diagram(ctx, workDir, cacheDir, c.Bool("live"), format, styleType); err != nil {
if err := hhfab.Diagram(ctx, workDir, cacheDir, c.Bool("live"), format, styleType, c.String("output")); err != nil {
return fmt.Errorf("failed to generate %s diagram: %w", format, err)
}

Expand Down
121 changes: 121 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,124 @@ _localreg: _zot
run cmd *args:
@echo "Running: {{cmd}} {{args}} (run gen manually if needed)"
@go run {{go_base_flags}} ./cmd/{{cmd}} {{args}}

#
# Generate diagrams for multiple environments in different formats and styles
#
test-diagram y="":
@mkdir -p test-diagram
@echo "==============================================="
@echo "Diagram generation test - various topologies, formats, and styles"
@echo "==============================================="

# Check if a VLAB is actually running
@echo "=== Checking for running VLAB ==="
@VLAB_PIDS=$(pgrep -f "[h]hfab vlab up" 2>/dev/null || echo ""); \
if [ -n "$VLAB_PIDS" ] && [ -f "vlab/kubeconfig" ]; then \
echo "=== Detected running VLAB, generating live diagrams ==="; \
bin/hhfab diagram --format drawio --style default --live --output test-diagram/live-drawio-default.drawio || echo "Failed to generate live DrawIO diagram"; \
bin/hhfab diagram --format drawio --style cisco --live --output test-diagram/live-drawio-cisco.drawio || echo "Failed to generate live DrawIO cisco diagram"; \
bin/hhfab diagram --format drawio --style hedgehog --live --output test-diagram/live-drawio-hedgehog.drawio || echo "Failed to generate live DrawIO hedgehog diagram"; \
bin/hhfab diagram --format dot --live --output test-diagram/live-dot.dot || echo "Failed to generate live DOT diagram"; \
if command -v dot >/dev/null 2>&1 && [ -f "test-diagram/live-dot.dot" ]; then \
dot -Tpng test-diagram/live-dot.dot -o test-diagram/live-dot.png; \
fi; \
bin/hhfab diagram --format mermaid --live --output test-diagram/live-mermaid.mermaid || echo "Failed to generate live Mermaid diagram"; \
if [ -f "test-diagram/live-mermaid.mermaid" ]; then \
echo '# Live Network Diagram' > test-diagram/live-mermaid.md; \
echo '```mermaid' >> test-diagram/live-mermaid.md; \
cat test-diagram/live-mermaid.mermaid >> test-diagram/live-mermaid.md; \
echo '```' >> test-diagram/live-mermaid.md; \
fi; \
else \
echo "No running VLAB detected, skipping live diagrams"; \
fi

# Skip prompt if -y flag is provided
@if [ "{{y}}" = "-y" ]; then \
true; \
else \
echo -n "This will generate diagrams from multiple environments. Continue? [y/N] " && read ans && [ "$ans" = "y" -o "$ans" = "Y" ]; \
fi

@echo "=== Generating diagrams for default VLAB topology ==="
bin/hhfab init -f --dev --gw
bin/hhfab vlab gen

# Generate all formats and styles for default topology
bin/hhfab diagram --format drawio --style default --output test-diagram/default-drawio-default.drawio
bin/hhfab diagram --format drawio --style cisco --output test-diagram/default-drawio-cisco.drawio
bin/hhfab diagram --format drawio --style hedgehog --output test-diagram/default-drawio-hedgehog.drawio
bin/hhfab diagram --format dot --output test-diagram/default-dot.dot
bin/hhfab diagram --format mermaid --output test-diagram/default-mermaid.mermaid

@echo "=== Generating diagrams for variant 3-spine topology ==="
bin/hhfab vlab gen --spines-count 3 --mclag-leafs-count 2 --orphan-leafs-count 1 --eslag-leaf-groups 2

# Generate all formats and styles for 3-spine topology
bin/hhfab diagram --format drawio --style default --output test-diagram/3spine-drawio-default.drawio
bin/hhfab diagram --format drawio --style cisco --output test-diagram/3spine-drawio-cisco.drawio
bin/hhfab diagram --format drawio --style hedgehog --output test-diagram/3spine-drawio-hedgehog.drawio
bin/hhfab diagram --format dot --output test-diagram/3spine-dot.dot
bin/hhfab diagram --format mermaid --output test-diagram/3spine-mermaid.mermaid

@echo "=== Generating diagrams for 4-mclag-2-orphan topology ==="
bin/hhfab vlab gen --mclag-leafs-count 4 --orphan-leafs-count 2

# Generate all formats and styles for 4-mclag-2-orphan topology
bin/hhfab diagram --format drawio --style default --output test-diagram/4mclag2orphan-drawio-default.drawio
bin/hhfab diagram --format drawio --style cisco --output test-diagram/4mclag2orphan-drawio-cisco.drawio
bin/hhfab diagram --format drawio --style hedgehog --output test-diagram/4mclag2orphan-drawio-hedgehog.drawio
bin/hhfab diagram --format dot --output test-diagram/4mclag2orphan-dot.dot
bin/hhfab diagram --format mermaid --output test-diagram/4mclag2orphan-mermaid.mermaid

@echo "=== Generating diagrams for collapsed core topology ==="
bin/hhfab init -f --dev --registry-repo localhost:30000 --fabric-mode collapsed-core
bin/hhfab vlab gen

# Generate all formats and styles for collapsed core topology
bin/hhfab diagram --format drawio --style default --output test-diagram/collapsed-core-drawio-default.drawio
bin/hhfab diagram --format drawio --style cisco --output test-diagram/collapsed-core-drawio-cisco.drawio
bin/hhfab diagram --format drawio --style hedgehog --output test-diagram/collapsed-core-drawio-hedgehog.drawio
bin/hhfab diagram --format dot --output test-diagram/collapsed-core-dot.dot
bin/hhfab diagram --format mermaid --output test-diagram/collapsed-core-mermaid.mermaid

# Convert DOT files to PNG if GraphViz is installed
@echo "=== Converting DOT files to PNG if GraphViz is installed ==="
@if command -v dot >/dev/null 2>&1; then \
for DOT_FILE in test-diagram/*-dot.dot; do \
PNG_FILE="${DOT_FILE%.dot}.png"; \
echo "Converting $DOT_FILE to $PNG_FILE"; \
dot -Tpng "$DOT_FILE" -o "$PNG_FILE"; \
done; \
else \
echo "GraphViz dot not installed, skipping PNG conversion"; \
fi

# Create markdown files with embedded mermaid diagrams
@echo "=== Creating Markdown files with embedded Mermaid diagrams ==="
@for MERMAID_FILE in test-diagram/*-mermaid.mermaid; do \
MD_FILE="${MERMAID_FILE%.mermaid}.md"; \
BASE_NAME=$(basename "$MERMAID_FILE" -mermaid.mermaid); \
echo "Creating $MD_FILE"; \
echo "# $BASE_NAME Network Diagram" > "$MD_FILE"; \
echo '```mermaid' >> "$MD_FILE"; \
cat "$MERMAID_FILE" >> "$MD_FILE"; \
echo '```' >> "$MD_FILE"; \
done

@echo ""
@echo "All diagrams generated in test-diagram/ directory"
@ls -la test-diagram/
@echo ""
@echo "Summary of generated files:"
@echo "- Default VLAB topology: default-*"
@echo "- 3-spine VLAB topology: 3spine-*"
@echo "- 4-mclag-2-orphan topology: 4mclag2orphan-*"
@echo "- Collapsed core topology: collapsed-core-*"
@echo "- Live diagrams (if VLAB running): live-*"
@echo ""
@echo "For each topology, these formats are available:"
@echo "- DrawIO: *-drawio-{default,cisco,hedgehog}.drawio"
@echo "- DOT: *-dot.dot (and PNG if GraphViz was installed)"
@echo "- Mermaid: *-mermaid.mermaid (and embedded in markdown *.md)"
4 changes: 2 additions & 2 deletions pkg/hhfab/cmddiagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
)

func Diagram(ctx context.Context, workDir, cacheDir string, live bool, format diagram.Format, style diagram.StyleType) error {
func Diagram(ctx context.Context, workDir, cacheDir string, live bool, format diagram.Format, style diagram.StyleType, outputPath string) error {
resultDir := filepath.Join(workDir, ResultDir)
if err := os.MkdirAll(resultDir, 0o755); err != nil {
return fmt.Errorf("creating result directory: %w", err)
Expand Down Expand Up @@ -44,7 +44,7 @@ func Diagram(ctx context.Context, workDir, cacheDir string, live bool, format di
client = kube
}

if err := diagram.Generate(ctx, resultDir, client, format, style); err != nil {
if err := diagram.Generate(ctx, resultDir, client, format, style, outputPath); err != nil {
return fmt.Errorf("generating diagram: %w", err)
}

Expand Down
73 changes: 59 additions & 14 deletions pkg/hhfab/diagram/diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var Formats = []Format{
FormatMermaid,
}

func Generate(ctx context.Context, resultDir string, client kclient.Reader, format Format, style StyleType) error {
func Generate(ctx context.Context, resultDir string, client kclient.Reader, format Format, style StyleType, outputPath string) error {
if !slices.Contains(Formats, format) {
return fmt.Errorf("unsupported diagram format: %s", format) //nolint:goerr113
}
Expand All @@ -40,39 +40,84 @@ func Generate(ctx context.Context, resultDir string, client kclient.Reader, form
return fmt.Errorf("getting topology: %w", err)
}

var filePath string
var displayPath string

switch format {
case FormatDrawio:
slog.Debug("Generating draw.io diagram", "style", style)
if err := GenerateDrawio(resultDir, topo, style); err != nil {
if err := GenerateDrawio(resultDir, topo, style, outputPath); err != nil {
return fmt.Errorf("generating draw.io diagram: %w", err)
}
filePath := filepath.Join("result", DrawioFilename)
slog.Info("Generated draw.io diagram", "file", filePath, "style", style)
if outputPath != "" {
filePath = outputPath
} else {
filePath = filepath.Join(resultDir, DrawioFilename)
}

displayPath = filePath
workDir, err := filepath.Abs(".")
if err == nil {
rel, err := filepath.Rel(workDir, filePath)
if err == nil {
displayPath = rel
}
}

slog.Info("Generated draw.io diagram", "file", displayPath, "style", style)
fmt.Printf("To use this diagram:\n")
fmt.Printf("1. Open with https://app.diagrams.net/ or the desktop Draw.io application\n")
fmt.Printf("2. You can edit the diagram and export to PNG, SVG, PDF or other formats\n")
case FormatDot:
slog.Debug("Generating DOT diagram")
if err := GenerateDOT(resultDir, topo); err != nil {
if err := GenerateDOT(resultDir, topo, outputPath); err != nil {
return fmt.Errorf("generating DOT diagram: %w", err)
}
filePath := filepath.Join("result", DotFilename)
slog.Info("Generated graphviz diagram", "file", filePath)
if outputPath != "" {
filePath = outputPath
} else {
filePath = filepath.Join(resultDir, DotFilename)
}

displayPath = filePath
workDir, err := filepath.Abs(".")
if err == nil {
rel, err := filepath.Rel(workDir, filePath)
if err == nil {
displayPath = rel
}
}

slog.Info("Generated graphviz diagram", "file", displayPath)
fmt.Printf("To render this diagram with Graphviz:\n")
fmt.Printf("1. Install Graphviz: https://graphviz.org/download/\n")
fmt.Printf("2. Convert to PNG: dot -Tpng %s -o diagram.png\n", filePath)
fmt.Printf("3. Convert to SVG: dot -Tsvg %s -o diagram.svg\n", filePath)
fmt.Printf("4. Convert to PDF: dot -Tpdf %s -o diagram.pdf\n", filePath)
fmt.Printf("2. Convert to PNG: dot -Tpng %s -o diagram.png\n", displayPath)
fmt.Printf("3. Convert to SVG: dot -Tsvg %s -o diagram.svg\n", displayPath)
fmt.Printf("4. Convert to PDF: dot -Tpdf %s -o diagram.pdf\n", displayPath)
case FormatMermaid:
slog.Debug("Generating Mermaid diagram")
if err := GenerateMermaid(resultDir, topo); err != nil {
if err := GenerateMermaid(resultDir, topo, outputPath); err != nil {
return fmt.Errorf("generating Mermaid diagram: %w", err)
}
filePath := filepath.Join("result", MermaidFilename)
slog.Info("Generated Mermaid diagram", "file", filePath)
if outputPath != "" {
filePath = outputPath
} else {
filePath = filepath.Join(resultDir, MermaidFilename)
}

displayPath = filePath
workDir, err := filepath.Abs(".")
if err == nil {
rel, err := filepath.Rel(workDir, filePath)
if err == nil {
displayPath = rel
}
}

slog.Info("Generated Mermaid diagram", "file", displayPath)
fmt.Printf("To render this diagram with Mermaid:\n")
fmt.Printf("1. Visit https://mermaid.live/ or use a Markdown editor with Mermaid support\n")
fmt.Printf("2. Copy the contents of %s into the editor\n", filePath)
fmt.Printf("2. Copy the contents of %s into the editor\n", displayPath)
default:
return fmt.Errorf("unsupported diagram format: %s", format) //nolint:goerr113
}
Expand Down
Loading
Loading