Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
*.drawio*
*.json
*.exe
*.dot
!example/example.json
!example/example.drawio
!example/example.dot
.cloudsketch
142 changes: 125 additions & 17 deletions cmd/cloudsketch.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package cmd

import (
"cloudsketch/internal/drawio"
"cloudsketch/internal/drawio/models"
"cloudsketch/internal/datastructures/build_graph"
"cloudsketch/internal/frontends"
"cloudsketch/internal/frontends/dot"
"cloudsketch/internal/frontends/drawio"
frontendModels "cloudsketch/internal/frontends/models"
"cloudsketch/internal/list"
"cloudsketch/internal/marshall"
"cloudsketch/internal/providers"
"cloudsketch/internal/providers/azure"
"context"
"errors"
Expand All @@ -14,6 +19,16 @@ import (
"github.com/urfave/cli/v3"
)

var (
frontendmap map[string]frontends.Frontend = map[string]frontends.Frontend{
"drawio": drawio.New(),
"dot": dot.New(),
}
providermap map[string]providers.Provider = map[string]providers.Provider{
"azure": azure.NewProvider(),
}
)

func newCloudsketch(_ context.Context, command *cli.Command) error {
args := command.Args().Slice()

Expand All @@ -23,38 +38,58 @@ func newCloudsketch(_ context.Context, command *cli.Command) error {

fileOrSubscriptionId := args[0]

frontendString := command.String("frontend")

frontend, ok := frontendmap[frontendString]

if !ok {
return fmt.Errorf("unknown frontend %s", frontendString)
}

providerString := command.String("provider")

provider, ok := providermap[providerString]

if !ok {
return fmt.Errorf("unknown frontend %s", frontendString)
}

log.Printf("target frontend is %s\n", frontendString)
log.Printf("target provider is %s\n", providerString)

var resources []*providers.Resource
var filename string

// command can either be a subscription id or a file name
if strings.HasSuffix(fileOrSubscriptionId, ".json") {
// if the file ends in .json, assume its a valid json file that contains previously populated Azure resources
file := fileOrSubscriptionId
existingResources, existingFilename, err := useExistingFile(fileOrSubscriptionId, frontendString)

log.Printf("using existing file %s\n", file)
if err != nil {
return err
}

resources, err := marshall.UnmarshallResources[[]*models.Resource](file)
resources = existingResources
filename = existingFilename
} else {
// otherwise treat it as a subscription id
existingResources, existingFilename, err := createNewFile(fileOrSubscriptionId, frontendString, provider)

if err != nil {
return err
}

outFile := strings.ReplaceAll(file, ".json", ".drawio")

return drawio.New(*resources).WriteDiagram(outFile)
resources = existingResources
filename = existingFilename
}

// otherwise treat it as a subscription id
subscriptionId := fileOrSubscriptionId

provider := azure.NewProvider()

resources, filename, err := provider.FetchResources(subscriptionId)
frontendResources, err := mapToDomainModels(resources)

if err != nil {
return err
}

filename = fmt.Sprintf("%s.drawio", filename)

if err := drawio.New(resources).WriteDiagram(filename); err != nil {
if err := frontend.WriteDiagram(frontendResources, filename); err != nil {
return err
}

Expand All @@ -63,3 +98,76 @@ func newCloudsketch(_ context.Context, command *cli.Command) error {

return nil
}

func useExistingFile(file, frontendString string) ([]*providers.Resource, string, error) {
log.Printf("using existing file %s\n", file)

resources, err := marshall.UnmarshallResources[[]*providers.Resource](file)

if err != nil {
return nil, "", err
}

outFile := strings.ReplaceAll(file, ".json", fmt.Sprintf(".%s", frontendString))

return *resources, outFile, nil
}

func createNewFile(subscriptionId, frontendString string, provider providers.Provider) ([]*providers.Resource, string, error) {
resources, filename, err := provider.FetchResources(subscriptionId)

if err != nil {
return nil, "", err
}

// cache resources for next run
err = marshall.MarshallResources(fmt.Sprintf("%s.json", filename), resources)

filename = fmt.Sprintf("%s.%s", filename, frontendString)

return resources, filename, err
}

func mapToDomainModels(resources []*providers.Resource) ([]*frontendModels.Resource, error) {
resource_map := &map[string]*frontendModels.Resource{}

tasks := list.Map(resources, func(r *providers.Resource) *build_graph.Task {
return build_graph.NewTask(r.Id, r.DependsOn, []string{}, []string{}, func() { addDependenciesFromIds(r, resource_map) })
})

bg, err := build_graph.NewGraph(tasks)

if err != nil {
return nil, fmt.Errorf("error during construction of dependency graph: %+v", err)
}

for _, task := range tasks {
bg.Resolve(task)
}

domainResources := []*frontendModels.Resource{}
for _, v := range *resource_map {
domainResources = append(domainResources, v)
}

return domainResources, nil
}

func addDependenciesFromIds(resource *providers.Resource, resource_map *map[string]*frontendModels.Resource) {
if (*resource_map)[resource.Id] != nil {
// resource already registered
return
}

dependencies := list.Map(resource.DependsOn, func(d string) *frontendModels.Resource {
return (*resource_map)[d]
})

(*resource_map)[resource.Id] = &frontendModels.Resource{
Id: resource.Id,
Type: resource.Type,
Name: resource.Name,
DependsOn: dependencies,
Properties: resource.Properties,
}
}
32 changes: 32 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package cmd

import (
"cloudsketch/internal/list"
"context"
"fmt"
"log"
"os"
"strings"

"github.com/urfave/cli/v3"
)
Expand All @@ -16,6 +18,24 @@ func Execute() {
Usage: "Azure to DrawIO",
UsageText: fmt.Sprintf("%s <subscription id>", name),
Description: "convert a Azure subscription to a DrawIO diagram",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "frontend",
Usage: "visualization target",
Value: "drawio",
Validator: func(frontend string) error {
return isValidInput([]string{"drawio", "dot"}, frontend)
},
},
&cli.StringFlag{
Name: "provider",
Usage: "resource source",
Value: "azure",
Validator: func(provider string) error {
return isValidInput([]string{"azure"}, provider)
},
},
},
Commands: []*cli.Command{
newVersion(),
},
Expand All @@ -26,3 +46,15 @@ func Execute() {
log.Fatal(err)
}
}

func isValidInput(validInputs []string, input string) error {
valid := list.Contains(validInputs, func(validProvider string) bool {
return input == validProvider
})

if !valid {
return fmt.Errorf("%s is not a valid value. Valid target are %s", input, strings.Join(validInputs, ","))
}

return nil
}
11 changes: 11 additions & 0 deletions example/example.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
digraph exampleexample {
examplevm -> examplenic;
examplevm -> examplesubscription;
examplesnet -> examplevnet;
examplesnet -> examplesubscription;
examplevmss -> examplesnet;
examplevmss -> examplesubscription;
examplenic -> examplesnet;
examplenic -> examplesubscription;
examplevnet -> examplesubscription;
}
26 changes: 13 additions & 13 deletions example/example.drawio
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/25.0.1 Chrome/128.0.6613.186 Electron/32.2.6 Safari/537.36" version="25.0.1">
<diagram name="Page-1" id="df6dcf3fde5f44a79e4922e9022aae63">
<diagram name="Page-1" id="ba8f05104d0a41428c1f7563f33250bb">
<mxGraphModel dx="2074" dy="1196" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell value="" style="rounded=0;whiteSpace=wrap;html=1;" id="d3e23033167c45d790609aa3f4c4c007" parent="1" vertex="1">
<mxCell value="" style="rounded=0;whiteSpace=wrap;html=1;" id="3865c29500164c2fa29b77b676752d10" parent="1" vertex="1">
<mxGeometry x="0" y="0" width="469" height="364" as="geometry" />
</mxCell>
<mxCell parent="1" vertex="1" value="" style="rounded=0;whiteSpace=wrap;html=1;;fillColor=#dae8fc;strokeColor=#6c8ebf" id="465bb9ac515a46a58c9ab3ea63ea2bd6">
<mxCell id="dd37dea9c7af4857a12192f914493a59" parent="1" vertex="1" value="" style="rounded=0;whiteSpace=wrap;html=1;;fillColor=#dae8fc;strokeColor=#6c8ebf">
<mxGeometry x="50" y="50" width="369" height="264" as="geometry" />
</mxCell>
<mxCell id="b691220305c049159460ef20aa529087" parent="465bb9ac515a46a58c9ab3ea63ea2bd6" vertex="1" value="" style="rounded=0;whiteSpace=wrap;html=1;;fillColor=#7EA6E0;strokeColor=#6c8ebf;opacity=50;">
<mxCell parent="dd37dea9c7af4857a12192f914493a59" vertex="1" value="" style="rounded=0;whiteSpace=wrap;html=1;;fillColor=#7EA6E0;strokeColor=#6c8ebf;opacity=50;" id="7510180fd43b4ff9b9179295b011c08c">
<mxGeometry x="50" y="50" width="269" height="164" as="geometry" />
</mxCell>
<mxCell value="" style="group" connectable="0" id="122107d720d44006a983c69f96cef034" parent="b691220305c049159460ef20aa529087" vertex="1">
<mxCell id="8d5ae61461a54b69821e60fb23f78f30" parent="7510180fd43b4ff9b9179295b011c08c" vertex="1" value="" style="group" connectable="0">
<mxGeometry x="50" y="50" width="69" height="64" as="geometry" />
</mxCell>
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/compute/Virtual_Machine.svg;labelBackgroundColor=none;" value="example-vm" id="a7eae8c56bb44bf487ea4b017a721dd9" parent="122107d720d44006a983c69f96cef034" vertex="1">
<mxGeometry x="0" y="0" width="69" height="64" as="geometry" />
<mxCell value="example-vmss" id="02f28d23d612443a8bca8889a428399a" parent="7510180fd43b4ff9b9179295b011c08c" vertex="1" style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/compute/VM_Scale_Sets.svg;labelBackgroundColor=none;">
<mxGeometry x="169" y="57" width="50" height="50" as="geometry" />
</mxCell>
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Subnet.svg;labelBackgroundColor=none;" value="example-snet/22" id="e36c035f411f4205be7cf684bc613a70" parent="b691220305c049159460ef20aa529087" vertex="1">
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Subnet.svg;labelBackgroundColor=none;" value="example-snet/22" id="0e46f0409c24421091fa1ed6a4f4dfe1" parent="7510180fd43b4ff9b9179295b011c08c" vertex="1">
<mxGeometry x="-34" y="-20" width="68" height="41" as="geometry" />
</mxCell>
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Virtual_Networks.svg;labelBackgroundColor=none;" value="example-vnet/21" id="4c8e9051ca7b465cb561d23c1c92f000" parent="465bb9ac515a46a58c9ab3ea63ea2bd6" vertex="1">
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Virtual_Networks.svg;labelBackgroundColor=none;" value="example-vnet/21" id="32f99c9713cb4f4cacdf7cfbb27d33b5" parent="dd37dea9c7af4857a12192f914493a59" vertex="1">
<mxGeometry x="-33" y="244" width="67" height="40" as="geometry" />
</mxCell>
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/general/Subscriptions.svg;labelBackgroundColor=none;" value="example-subscription" id="34e53258770b4ae0b16e230f1769fe52" parent="d3e23033167c45d790609aa3f4c4c007" vertex="1">
<mxCell id="4ae11a15e81d42c28962bd89d6e1912a" parent="3865c29500164c2fa29b77b676752d10" vertex="1" style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/general/Subscriptions.svg;labelBackgroundColor=none;" value="example-subscription">
<mxGeometry x="-34" y="-34" width="68" height="68" as="geometry" />
</mxCell>
<mxCell id="cc206ca3af784c0081113801b80208ed" parent="b691220305c049159460ef20aa529087" vertex="1" style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/compute/VM_Scale_Sets.svg;labelBackgroundColor=none;" value="example-vmss">
<mxGeometry x="169" y="57" width="50" height="50" as="geometry" />
<mxCell parent="8d5ae61461a54b69821e60fb23f78f30" vertex="1" style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/compute/Virtual_Machine.svg;labelBackgroundColor=none;" value="example-vm" id="a0bc0698aa154b3fa1a9e7a5bacd2a8e">
<mxGeometry x="0" y="0" width="69" height="64" as="geometry" />
</mxCell>
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Network_Interfaces.svg;labelBackgroundColor=none;" value="" id="dba504c73a284ea1bcda28f8c3e1d726" parent="122107d720d44006a983c69f96cef034" vertex="1">
<mxCell style="image;aspect=fixed;html=1;points=[];align=center;fontSize=12;image=img/lib/azure2/networking/Network_Interfaces.svg;labelBackgroundColor=none;" value="" id="d06217131b5348c5bb48f7b7b2ecb7ce" parent="8d5ae61461a54b69821e60fb23f78f30" vertex="1">
<mxGeometry x="52" y="-15" width="34" height="30" as="geometry" />
</mxCell>
</root>
Expand Down
18 changes: 9 additions & 9 deletions example/example.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,40 @@
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000",
"Type": "SUBSCRIPTION",
"Name": "example-subscription",
"ResourceGroup": "",
"DependsOn": null,
"Properties": null
},
{
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/virtualnetworks/example-vnet",
"Type": "VIRTUAL_NETWORK",
"Name": "example-vnet",
"ResourceGroup": "",
"DependsOn": [
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"Properties": {
"size": "21"
"size": [
"21"
]
}
},
{
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/virtualnetworks/example-vnet/subnets/example-snet",
"Type": "SUBNET",
"Name": "example-snet",
"ResourceGroup": "",
"DependsOn": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/virtualnetworks/example-vnet",
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"Properties": {
"size": "22"
"size": [
"22"
]
}
},
{
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.compute/virtualmachinescalesets/example-vmss",
"Type": "VIRTUAL_MACHINE_SCALE_SET",
"Name": "example-vmss",
"ResourceGroup": "",
"DependsOn": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/virtualnetworks/example-vnet/subnets/example-snet",
"/subscriptions/00000000-0000-0000-0000-000000000000"
Expand All @@ -47,20 +47,20 @@
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/networkinterfaces/example-nic",
"Type": "NETWORK_INTERFACE",
"Name": "example-nic",
"ResourceGroup": "",
"DependsOn": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/virtualnetworks/example-vnet/subnets/example-snet",
"/subscriptions/00000000-0000-0000-0000-000000000000"
],
"Properties": {
"attachedTo": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.compute/virtualmachines/example-vm"
"attachedTo": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.compute/virtualmachines/example-vm"
]
}
},
{
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.compute/virtualmachines/example-vm",
"Type": "VIRTUAL_MACHINE",
"Name": "example-vm",
"ResourceGroup": "",
"DependsOn": [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/example-resource-group/providers/microsoft.network/networkinterfaces/example-nic",
"/subscriptions/00000000-0000-0000-0000-000000000000"
Expand Down
Loading