GenerateMermaidChart.cs•9.93 kB
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Immutable;
using System.Text;
using Azure.ResourceManager.Network.Models;
using AzureMcp.Deploy.Options;
using Microsoft.Extensions.ObjectPool;
namespace AzureMcp.Deploy.Commands;
public static class GenerateMermaidChart
{
    // used to create a subgraph for AKS cluster in the chart
    private const string aksClusterInternalName = "akscluster";
    private const string aksClusterName = "Azure Kubernetes Service (AKS) Cluster";
    private const string acaEnvInternalName = "acaenvironment";
    private const string acaEnvName = "Azure Container Apps Environment";
    public static string GenerateChart(string workspaceFolder, AppTopology appTopology)
    {
        var chartComponents = new List<string>();
        chartComponents.Add("graph TD");
        if (appTopology.Services.Any(s => s.AzureComputeHost == "aks"))
        {
            chartComponents.Add("classDef cluster fill:#ffffd0,stroke:#333,stroke-width:2px,color:#000");
        }
        var services = new List<string> { "%% Services" };
        var resources = new List<string> { "%% Compute Resources" };
        var dependencyResources = new List<string> { "%% Binding Resources" };
        var relationships = new List<string> { "%% Relationships" };
        foreach (var service in appTopology.Services)
        {
            var serviceName = new List<string> { $"Name: {service.Name}" };
            if (!string.IsNullOrWhiteSpace(workspaceFolder))
            {
                var projectRelativePath = Path.GetRelativePath(workspaceFolder, string.IsNullOrWhiteSpace(service.Path) ? workspaceFolder : service.Path);
                serviceName.Add($"Path: {projectRelativePath}");
            }
            serviceName.Add($"Language: {service.Language}");
            serviceName.Add($"Port: {service.Port}");
            if (service.DockerSettings != null &&
                string.Equals(service.AzureComputeHost, "azurecontainerapp", StringComparison.OrdinalIgnoreCase))
            {
                serviceName.Add($"DockerFile: {service.DockerSettings.DockerFilePath}");
                serviceName.Add($"Docker Context: {service.DockerSettings.DockerContext}");
            }
            var serviceInternalName = $"svc-{service.Name}";
            services.Add(CreateComponentName(serviceInternalName, string.Join("\n", serviceName), NodeShape.Rectangle));
            relationships.Add(CreateRelationshipString(serviceInternalName, $"{FlattenServiceType(service.AzureComputeHost)}_{service.Name}", "hosted on", ArrowType.Solid));
        }
        var aksClusterExists = false;
        var containerAppEnvExists = false;
        foreach (var service in appTopology.Services)
        {
            string serviceResourceInternalName = $"{FlattenServiceType(service.AzureComputeHost)}_{service.Name}";
            if (service.AzureComputeHost == "aks")
            {
                if (!aksClusterExists)
                {
                    // Add AKS cluster as a subgraph
                    resources.Add($"subgraph {aksClusterInternalName} [\"{aksClusterName}\"]");
                    // containerized services share the same AKS cluster
                    foreach (var aksservice in appTopology.Services.Where(s => s.AzureComputeHost == "aks"))
                    {
                        resources.Add(CreateComponentName($"{aksservice.AzureComputeHost}_{aksservice.Name}", $"{aksservice.Name} (Containerized Service)", NodeShape.RoundedRectangle));
                    }
                    resources.Add("end");
                    resources.Add($"{aksClusterInternalName}:::cluster");
                    aksClusterExists = true;
                }
            }
            else if (service.AzureComputeHost == "containerapp")
            {
                if (!containerAppEnvExists)
                {
                    // Add Container App Environment as a subgraph
                    resources.Add($"subgraph {acaEnvInternalName} [\"{acaEnvName}\"]");
                    // containerized services share the same Container App Environment
                    foreach (var containerAppService in appTopology.Services.Where(s => s.AzureComputeHost == "containerapp"))
                    {
                        resources.Add(CreateComponentName($"{containerAppService.AzureComputeHost}_{containerAppService.Name}", $"{containerAppService.Name} (Container App)", NodeShape.RoundedRectangle));
                    }
                    resources.Add("end");
                    containerAppEnvExists = true;
                }
            }
            // each service should have a compute resource type
            else if (!resources.Any(r => r.Contains(serviceResourceInternalName)))
            {
                resources.Add(CreateComponentName(serviceResourceInternalName, $"{service.Name} ({GetFormalName(service.AzureComputeHost)})", NodeShape.RoundedRectangle));
            }
            foreach (var dependency in service.Dependencies)
            {
                var instanceInternalName = $"{FlattenServiceType(dependency.ServiceType)}.{dependency.Name}";
                var instanceName = $"{dependency.Name} ({GetFormalName(dependency.ServiceType)})";
                if (IsComputeResourceType(dependency.ServiceType))
                {
                    if (!resources.Any(r => r.Contains(EnsureUrlFriendlyName(instanceInternalName))))
                    {
                        resources.Add(CreateComponentName(instanceInternalName, instanceName, NodeShape.RoundedRectangle));
                    }
                }
                else
                {
                    dependencyResources.Add(CreateComponentName(instanceInternalName, instanceName, NodeShape.Rectangle));
                }
                relationships.Add(CreateRelationshipString(serviceResourceInternalName, instanceInternalName, dependency.ConnectionType, ArrowType.Dotted));
            }
        }
        chartComponents.AddRange(services);
        chartComponents.AddRange(resources);
        chartComponents.Add("subgraph \"Compute Resources\"");
        chartComponents.AddRange(resources);
        chartComponents.Add("end");
        chartComponents.Add("subgraph \"Dependency Resources\"");
        chartComponents.AddRange(dependencyResources);
        chartComponents.Add("end");
        chartComponents.AddRange(relationships);
        return string.Join("\n", chartComponents);
    }
    private static string CreateComponentName(string internalName, string name, NodeShape nodeShape)
    {
        var nodeShapeBrackets = GetNodeShapeBrackets(nodeShape);
        return $"{EnsureUrlFriendlyName(internalName)}{nodeShapeBrackets[0]}\"`{name}`\"{nodeShapeBrackets[1]}";
    }
    private static string CreateRelationshipString(string sourceName, string targetName, string connectionDescription, ArrowType arrowType)
    {
        var arrowSymbol = GetArrowSymbol(arrowType);
        return $"{EnsureUrlFriendlyName(sourceName)} {arrowSymbol} |\"{connectionDescription}\"| {EnsureUrlFriendlyName(targetName)}";
    }
    private static string EnsureUrlFriendlyName(string name)
    {
        return name.Replace('.', '_')
                  .Replace(" ", "_")
                  .Trim()
                  .ToLowerInvariant();
    }
    private static string[] GetNodeShapeBrackets(NodeShape nodeShape)
    {
        return nodeShape switch
        {
            NodeShape.Rectangle => ["[", "]"],
            NodeShape.Circle => ["((", "))"],
            NodeShape.RoundedRectangle => ["(", ")"],
            NodeShape.Cylinder => ["[(", ")]"],
            NodeShape.Hexagon => ["{{", "}}"],
            _ => ["[", "]"]
        };
    }
    private static string GetArrowSymbol(ArrowType arrowType)
    {
        return arrowType switch
        {
            ArrowType.Solid => "-->",
            ArrowType.Open => "->",
            ArrowType.Dotted => "-.->",
            _ => "-->"
        };
    }
    private static string GetFormalName(string name)
    {
        if (ResourceTypeConverter.TryGetValue(name, out var formalName))
        {
            return formalName;
        }
        return name;
    }
    private static string FlattenServiceType(string serviceType)
    {
        return serviceType.ToLowerInvariant().Replace("azure", "");
    }
    private static bool IsComputeResourceType(string serviceType)
    {
        return Enum.GetNames<AzureServiceConstants.AzureComputeServiceType>().Contains(serviceType, StringComparer.OrdinalIgnoreCase);
    }
    private static IDictionary<string, string> ResourceTypeConverter = new Dictionary<string, string>
    {
        { "appservice", "Azure App Service" },
        { "containerapp", "Azure Container Apps" },
        { "functionapp", "Azure Functions" },
        { "staticwebapp", "Azure Static Web Apps" },
        { "aks", "Azure Kubernetes Services" },
        { "azureaisearch", "Azure AI Search" },
        { "azureaiservices", "Azure AI Services" },
        { "azureapplicationinsights", "Azure Application Insights" },
        { "azurebotservice", "Azure Bot Service" },
        { "azurecosmosdb", "Azure Cosmos DB" },
        { "azurekeyvault", "Azure Key Vault" },
        { "azuredatabaseformysql", "Azure Database for MySQL" },
        { "azureopenai", "Azure OpenAI" },
        { "azuredatabaseforpostgresql", "Azure Database for PostgreSQL" },
        { "azuresqldatabase", "Azure SQL Database" },
        { "azurecacheforredis", "Azure Cache For Redis"},
        { "azurestorageaccount", "Azure Storage Account" },
        { "azureservicebus", "Azure Service Bus" },
        { "azurewebpubsub", "Azure Web PubSub"}
    };
}
public enum NodeShape
{
    Rectangle,
    Circle,
    RoundedRectangle,
    Cylinder,
    Hexagon
}
public enum ArrowType
{
    Solid,
    Open,
    Dotted
}