mingrammer / diagrams

:art: Diagram as Code for prototyping cloud system architectures
https://diagrams.mingrammer.com
MIT License
37.36k stars 2.44k forks source link

How to have overlapping clusters #852

Open samuelkdavis-vector opened 1 year ago

samuelkdavis-vector commented 1 year ago

image

I would like to have overlapping groups/clusters. I cannot find an example of how to do this. In the linked example - the Public Subnet is in Availability Zone A, but also in a separate group for all public subnets sharing a route table

kevinvalleau commented 1 year ago

I tried something like this, like mentioned in #111 :

from diagrams import Diagram, Cluster, Edge, Node

#graph_attr = {
 #   "layout":"neato",
  #  }
graph_attr = {
    "layout":"neato",
    "compound":"true",
    "splines":"spline",
    }

scaling_clus_attr = {
    "bgcolor":"transparent",
    "pencolor":"blue",
    "penwidth":"4.0",
    "fillcolor" : "green"
    }

with Diagram("\n\nOverlapping Clusters", show=False, graph_attr=graph_attr) as diag:

    with Cluster("SubnetA","TB",{"bgcolor": "green"}):
        A_UpLf = Node("", shape="plaintext", pin="true", pos="0,4")
        A_LwRt = Node("", shape="plaintext", pin="true", pos="4,0")
    with Cluster("SubnetB","TB"):
        B_UpLf = Node("", shape="plaintext", pin="true", pos="6,4")
        B_LwRt = Node("", shape="plaintext", pin="true", pos="10,0")
    with Cluster("Scaling Set", "LR", graph_attr=scaling_clus_attr):
        SS_UpLf = Node("", shape="plaintext", pin="true", pos="2,3")
        SS_LwRt = Node("", shape="plaintext", pin="true", pos="8,1")
        n1 = Node("node 1", shape="circle", labelloc="c", pin="true", pos="3,2")
        n1 = Node("node 2", shape="box",    labelloc="c", pin="true", pos="7,2")

But it doesn't work. I just get a circle, a square and a text, with no background. image

clayms commented 1 year ago

@kevinvalleau Your code works on my system. Likely a version difference issue.

$ dot -V
dot - graphviz version 7.1.0 (0)

$ pip freeze | grep -E 'diagrams|graphviz'
diagrams==0.20.0
graphviz==0.16

$ hostnamectl | grep Kernel
Kernel: Linux 6.1.7-arch1-1

image

kevinvalleau commented 1 year ago
dot - graphviz version 7.1.0 (20230121.1956)`
diagrams==0.23.3
graphviz==0.20.1

And it still does not work but I am on Windows. I'll try with WSL2.

=> Works on a WSL2 debian.

=> After a few tests, it seems clusters are not working with neato on Windows with my setup. The pure graphviz code here https://graphviz.readthedocs.io/en/stable/examples.html#cluster-py works. But even a small code with one cluster with diagrams does not work.

samuelkdavis-vector commented 1 year ago

Can this only be done with exact positioning?

The reason I wanted to use diagrams as code was to gain the benefit of clusters automatic grouping. Pixel perfect diagramming is quite painful when adding an item into an availability zone in a networking diagram which requires resizing subnets, vpcs, accounts, and any arbitrary cross-cutting grouping.

This solution would remove the need for pixel perfect drag-drop resizing, but would require specifically defining where each individual item appears in a grid.

clayms commented 1 year ago

@samuelkdavis-vector

Can this only be done with exact positioning? I don't think so. I am pretty sure this is not a limitation of this library, but a limitation of the dot rendering engine.

clayms commented 1 year ago

have a look at https://graphviz.org/docs/layouts/

Anddd7 commented 9 months ago

I use a loop to calculate the pos for each subnet, looks fine ... But it's hard to add nodes - you need to calculate the pos as well. 😒

I tried 2 ways, both big effort ...

image

graph_attr = {"layout": "neato", "compound": "true", "splines": "spline"}
with Diagram("dist/demo", show=False, graph_attr=graph_attr) as diag:
    # input
    subnet_area = 3
    az_count = 3
    az_names = ["1a", "1b", "1c", "1d", "1e", "1f"]
    layer_count = 3
    layer_types = [Cluster, Cluster, Cluster]
    # layer_types = [ClusterPublicSubnet, ClusterPrivateSubnet, ClusterIntraSubnet]
    layer_names = ["public", "private", "intra"]

    # param
    spacer = 2.5  # η›΄εΎ„ 1.9
    vpc_padding = 1
    az_padding = 0.4
    subnet_padding = 0.2
    vpc_width = subnet_area * az_count + spacer * (az_count - 1)
    vpc_height = subnet_area * layer_count + spacer * (layer_count - 1)

    # cluster cache, add nodes later
    subnets = [[{}] * layer_count for _ in range(az_count)]

    with ClusterVPC("vpc") as vpc:
        p_tl = Node("", shape="none", pin="true", pos=f"{-vpc_padding},{vpc_height+vpc_padding}")
        p_br = Node("", shape="none", pin="true", pos=f"{vpc_width+vpc_padding},{-vpc_padding}")

    for ia in range(az_count):
        x = (subnet_area + spacer) * ia
        y = 0
        for il, type in enumerate(layer_types):
            y = (subnet_area + spacer) * il
            name = f"sn-{layer_names[il]}-{az_names[ia]}"
            with type(name) as instance:
                p_tl = Node("", shape="none", pin="true", pos=f"{x},{y+subnet_area}")
                p_br = Node("", shape="none", pin="true", pos=f"{x+subnet_area},{y}")

                subnets[ia][il] = {"subnet": instance, "x": x + subnet_padding, "y": y + subnet_padding}

        with Cluster(
            f"az-{az_names[ia]}",
            graph_attr={
                "bgcolor": "transparent",
                "penwidth": "2.0",
                "style": "rounded,dashed",
            },
        ):
            p_tl = Node("", shape="none", pin="true", pos=f"{x-az_padding},{y+subnet_area+az_padding}")
            p_br = Node("", shape="none", pin="true", pos=f"{x+subnet_area+az_padding},{-az_padding}")
Anddd7 commented 9 months ago

optimize a litter bit, looks better πŸ˜„

image

    with vpc:
        igw = InternetGateway(pin="true", pos=f"{vpc_width/2},{-3}")
    with subnets[1][0]["subnet"]:
        x, y = subnets[1][0]["x"], subnets[1][0]["y"]
        nat = NATGateway(pin="true", pos=f"{x},{y}")
        elb = ElasticLoadBalancing(pin="true", pos=f"{x+spacer},{y}")
    with subnets[1][1]["subnet"]:
        x, y = subnets[1][1]["x"], subnets[1][1]["y"]
        eks = EKS(pin="true", pos=f"{x},{y}")
    with subnets[1][2]["subnet"]:
        x, y = subnets[1][2]["x"], subnets[1][2]["y"]
        rds = RDSInstance(pin="true", pos=f"{x},{y}")

    igw >> elb >> eks >> rds
    eks >> nat >> igw