├── cli-output.png ├── assets ├── icons │ ├── bst.png │ ├── cdn.png │ ├── cli.png │ ├── def.png │ ├── dns.png │ ├── doc.png │ ├── fst.png │ ├── fun.png │ ├── gtw.png │ ├── kub.png │ ├── lba.png │ ├── mem.png │ ├── msg.png │ ├── ost.png │ ├── que.png │ ├── rdb.png │ ├── ser.png │ ├── usr.png │ ├── waf.png │ └── web.png ├── aws.csv ├── google.csv ├── default.csv └── azure.csv ├── examples ├── clients.png ├── compute.png ├── database.png ├── security.png ├── storage.png ├── connections.png ├── networking.png ├── impl-example.png ├── token-manager.png ├── backend-for-frontend.png ├── message-bus-pattern.png ├── token-manager-google.png ├── s3-upload-presigned-url.png ├── cognito-custom-auth-flow.png ├── auth0-custom-db-connection-with-jwt.png ├── security.yml ├── clients.yml ├── database.yml ├── storage.yml ├── networking.yml ├── message-bus-pattern.yml ├── compute.yml ├── s3-upload-presigned-url.yml ├── token-manager.yml ├── connections.yml ├── backend-for-frontend.yml ├── token-manager-google.yml ├── cognito-custom-auth-flow.yml ├── README.md └── auth0-custom-db-connection-with-jwt.yml ├── go.mod ├── .gitignore ├── cmd ├── .gitignore ├── draft-all.sh ├── draft-all copy.sh └── main.go ├── kinds_test.go ├── pkg ├── graph │ ├── graph_test.go │ └── graph.go ├── edge │ ├── edge_test.go │ └── edge.go ├── cluster │ ├── cluster.go │ └── cluster_test.go └── node │ ├── node.go │ └── node_test.go ├── LICENSE ├── draft_test.go ├── impl.go ├── impl_test.go ├── config_test.go ├── kinds.go ├── CHANGELOG.md ├── config.go ├── icon.go ├── icon_test.go ├── draft.go └── README.md /cli-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/cli-output.png -------------------------------------------------------------------------------- /assets/icons/bst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/bst.png -------------------------------------------------------------------------------- /assets/icons/cdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/cdn.png -------------------------------------------------------------------------------- /assets/icons/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/cli.png -------------------------------------------------------------------------------- /assets/icons/def.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/def.png -------------------------------------------------------------------------------- /assets/icons/dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/dns.png -------------------------------------------------------------------------------- /assets/icons/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/doc.png -------------------------------------------------------------------------------- /assets/icons/fst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/fst.png -------------------------------------------------------------------------------- /assets/icons/fun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/fun.png -------------------------------------------------------------------------------- /assets/icons/gtw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/gtw.png -------------------------------------------------------------------------------- /assets/icons/kub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/kub.png -------------------------------------------------------------------------------- /assets/icons/lba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/lba.png -------------------------------------------------------------------------------- /assets/icons/mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/mem.png -------------------------------------------------------------------------------- /assets/icons/msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/msg.png -------------------------------------------------------------------------------- /assets/icons/ost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/ost.png -------------------------------------------------------------------------------- /assets/icons/que.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/que.png -------------------------------------------------------------------------------- /assets/icons/rdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/rdb.png -------------------------------------------------------------------------------- /assets/icons/ser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/ser.png -------------------------------------------------------------------------------- /assets/icons/usr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/usr.png -------------------------------------------------------------------------------- /assets/icons/waf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/waf.png -------------------------------------------------------------------------------- /assets/icons/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/assets/icons/web.png -------------------------------------------------------------------------------- /examples/clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/clients.png -------------------------------------------------------------------------------- /examples/compute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/compute.png -------------------------------------------------------------------------------- /examples/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/database.png -------------------------------------------------------------------------------- /examples/security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/security.png -------------------------------------------------------------------------------- /examples/storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/storage.png -------------------------------------------------------------------------------- /examples/connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/connections.png -------------------------------------------------------------------------------- /examples/networking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/networking.png -------------------------------------------------------------------------------- /examples/impl-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/impl-example.png -------------------------------------------------------------------------------- /examples/token-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/token-manager.png -------------------------------------------------------------------------------- /examples/backend-for-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/backend-for-frontend.png -------------------------------------------------------------------------------- /examples/message-bus-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/message-bus-pattern.png -------------------------------------------------------------------------------- /examples/token-manager-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/token-manager-google.png -------------------------------------------------------------------------------- /examples/s3-upload-presigned-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/s3-upload-presigned-url.png -------------------------------------------------------------------------------- /examples/cognito-custom-auth-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/cognito-custom-auth-flow.png -------------------------------------------------------------------------------- /examples/auth0-custom-db-connection-with-jwt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/draft/HEAD/examples/auth0-custom-db-connection-with-jwt.png -------------------------------------------------------------------------------- /examples/security.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: waf 6 | outline: Security 7 | label: 'kind: waf' 8 | 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasepe/draft 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/emicklei/dot v0.11.0 7 | github.com/mitchellh/go-homedir v1.1.0 8 | github.com/rakyll/statik v0.1.7 9 | gopkg.in/yaml.v2 v2.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /assets/aws.csv: -------------------------------------------------------------------------------- 1 | bst,EBS 2 | cdn,Cloudfront 3 | cli,CLI App 4 | dns,Route 53 5 | doc,DynamoDB 6 | fst,EFS 7 | fun,Lambda 8 | gtw,API Gateway 9 | kub,EKS 10 | lba,ELB 11 | mem,ElastiCache 12 | msg,SNS 13 | ost,S3 14 | que,SQS 15 | rdb,RDS 16 | ser,μService 17 | usr,User 18 | waf,WAF 19 | web,Browser -------------------------------------------------------------------------------- /assets/google.csv: -------------------------------------------------------------------------------- 1 | bst,Persistent Disk 2 | cdn,CDN 3 | cli,CLI App 4 | dns,DNS 5 | doc,Datastore 6 | fst,Filestore 7 | fun,Functions 8 | gtw,Endpoints 9 | kub,Kubernetes Engine 10 | lba,Load Balancing 11 | mem,Memorystore 12 | msg,Pub/Sub 13 | ost,Storage 14 | rdb,SQL 15 | ser,μService 16 | usr,User 17 | waf,Armor 18 | web,Browser -------------------------------------------------------------------------------- /assets/default.csv: -------------------------------------------------------------------------------- 1 | bst,Block Store 2 | cdn,CDN 3 | cli,CLI App 4 | dns,DNS 5 | doc,NoSQL DB 6 | fst,File Store 7 | fun,Function 8 | gtw,Gateway 9 | kub,Containers Engine 10 | lba,Load Balancer 11 | mem,Cache 12 | msg,Pub/Sub 13 | ost,Object Store 14 | que,Queue 15 | rdb,SQL DB 16 | ser,μService 17 | usr,User 18 | waf,Firewall 19 | web,Browser -------------------------------------------------------------------------------- /assets/azure.csv: -------------------------------------------------------------------------------- 1 | bst,Disk Storage 2 | cdn,CDN 3 | cli,CLI App 4 | dns,DNS 5 | doc,Cosmos DB 6 | fst,File Storage 7 | fun,Functions 8 | gtw,API Management 9 | kub,AKS 10 | lba,Load Balancer 11 | mem,Redis Caches 12 | msg,Notification Hubs 13 | ost,Blob Storage 14 | rdb,SQL Database 15 | ser,μService 16 | usr,User 17 | waf,Firewall 18 | web,Browser -------------------------------------------------------------------------------- /examples/clients.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: cli 6 | outline: Clients 7 | label: 'kind: cli' 8 | - 9 | kind: usr 10 | outline: Clients 11 | label: 'kind: usr' 12 | - 13 | kind: web 14 | outline: Clients 15 | label: 'kind: web' 16 | connections: 17 | - 18 | origin: cli1 19 | targets: 20 | - 21 | id: usr1 22 | color: transparent 23 | - 24 | origin: usr1 25 | targets: 26 | - 27 | id: web1 28 | color: transparent 29 | -------------------------------------------------------------------------------- /examples/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: rdb 6 | outline: Database 7 | label: 'kind: rdb' 8 | - 9 | kind: doc 10 | outline: Database 11 | label: 'kind: doc' 12 | - 13 | kind: mem 14 | outline: Database 15 | label: 'kind: mem' 16 | connections: 17 | - 18 | origin: rdb1 19 | targets: 20 | - 21 | id: doc1 22 | color: transparent 23 | - 24 | origin: doc1 25 | targets: 26 | - 27 | id: mem1 28 | color: transparent 29 | -------------------------------------------------------------------------------- /examples/storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: bst 6 | outline: Storage 7 | label: 'kind: bst' 8 | - 9 | kind: ost 10 | outline: Storage 11 | label: 'kind: ost' 12 | - 13 | kind: fst 14 | outline: Storage 15 | label: 'kind: fst' 16 | connections: 17 | - 18 | origin: bst1 19 | targets: 20 | - 21 | id: ost1 22 | color: transparent 23 | - 24 | origin: ost1 25 | targets: 26 | - 27 | id: fst1 28 | color: transparent 29 | 30 | -------------------------------------------------------------------------------- /examples/networking.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: dns 6 | outline: Networking 7 | label: 'kind: dns' 8 | - 9 | kind: cdn 10 | outline: Networking 11 | label: 'kind: cdn' 12 | - 13 | kind: lba 14 | outline: Networking 15 | label: 'kind: lba' 16 | connections: 17 | - 18 | origin: dns1 19 | targets: 20 | - 21 | id: cdn1 22 | color: transparent 23 | - 24 | origin: cdn1 25 | targets: 26 | - 27 | id: lba1 28 | color: transparent 29 | -------------------------------------------------------------------------------- /examples/message-bus-pattern.yml: -------------------------------------------------------------------------------- 1 | title: Message Bus Pattern 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: ser 6 | label: Producer 7 | - 8 | kind: msg 9 | label: | 10 | Notification 11 | Service 12 | - 13 | kind: ser 14 | label: Subscriber @topic 1 15 | - 16 | kind: ser 17 | label: Subscriber @topic 2 18 | connections: 19 | - 20 | origin: ser1 21 | targets: 22 | - 23 | id: msg1 24 | - 25 | origin: msg1 26 | targets: 27 | - 28 | id: ser2 29 | dashed: true 30 | - 31 | id: ser3 32 | dashed: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Intellij 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/encodings.xml 5 | .idea/**/compiler.xml 6 | .idea/**/misc.xml 7 | .idea/**/modules.xml 8 | .idea/**/vcs.xml 9 | 10 | ## VSCode 11 | .vscode/ 12 | 13 | ## File-based project format: 14 | *.iws 15 | *.iml 16 | .idea/ 17 | 18 | # Binaries for programs and plugins 19 | *.exe 20 | *.exe~ 21 | *.dll 22 | *.so 23 | *.dylib 24 | *.dat 25 | *.DS_Store 26 | go.sum 27 | 28 | # Test binary, built with `go test -c` 29 | *.test 30 | 31 | # Output of the go coverage tool, specifically when used with LiteIDE 32 | *.out 33 | 34 | # Goreleaser builds 35 | dist/** 36 | 37 | icons/** -------------------------------------------------------------------------------- /cmd/.gitignore: -------------------------------------------------------------------------------- 1 | ## Intellij 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/encodings.xml 5 | .idea/**/compiler.xml 6 | .idea/**/misc.xml 7 | .idea/**/modules.xml 8 | .idea/**/vcs.xml 9 | 10 | ## File-based project format: 11 | *.iws 12 | *.iml 13 | .idea/ 14 | 15 | # Binaries for programs and plugins 16 | *.exe 17 | *.exe~ 18 | *.dll 19 | *.so 20 | *.dylib 21 | *.dat 22 | *.DS_Store 23 | go.sum 24 | 25 | # Test binary, built with `go test -c` 26 | *.test 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | # Goreleaser builds 32 | dist/** 33 | 34 | # Goreleaser file 35 | .goreleaser.yml 36 | 37 | -------------------------------------------------------------------------------- /kinds_test.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestValidateProvider(t *testing.T) { 8 | tests := []struct { 9 | provider string 10 | want string 11 | }{ 12 | {"aws", "aws"}, 13 | {"google", "google"}, 14 | {"alibaba", "default"}, 15 | {"digitalocean", "default"}, 16 | {"azure", "azure"}, 17 | } 18 | 19 | validate := validateProvider() 20 | 21 | for _, tt := range tests { 22 | t.Run(tt.provider, func(t *testing.T) { 23 | com := Component{Provider: tt.provider} 24 | validate(&com) 25 | 26 | if com.Provider != tt.want { 27 | t.Errorf("got [%v] want [%v]", com.Provider, tt.want) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/compute.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: gtw 6 | outline: Compute 7 | label: 'kind: gtw' 8 | - 9 | kind: fun 10 | outline: Compute 11 | label: 'kind: fun' 12 | - 13 | kind: kub 14 | outline: Compute 15 | label: 'kind: kub' 16 | - 17 | kind: ser 18 | outline: Compute 19 | label: 'kind: ser' 20 | connections: 21 | - 22 | origin: gtw1 23 | targets: 24 | - 25 | id: fun1 26 | color: transparent 27 | - 28 | origin: fun1 29 | targets: 30 | - 31 | id: kub1 32 | color: transparent 33 | - 34 | origin: kub1 35 | targets: 36 | - 37 | id: ser1 38 | color: transparent 39 | -------------------------------------------------------------------------------- /examples/s3-upload-presigned-url.yml: -------------------------------------------------------------------------------- 1 | backgroundColor: '#ffffff' 2 | components: 3 | - 4 | kind: web 5 | label: SPA 6 | - 7 | kind: gtw 8 | outline: AWS 9 | provider: aws 10 | - 11 | kind: fun 12 | outline: AWS 13 | provider: aws 14 | label: | 15 | Get 16 | Pre-Signed URL 17 | - 18 | kind: ost 19 | outline: AWS 20 | provider: aws 21 | label: "*.jpg, *.png" 22 | connections: 23 | - 24 | origin: web1 25 | targets: 26 | - 27 | id: gtw1 28 | num: 1 29 | labeldistance: 4.5 30 | - 31 | origin: gtw1 32 | targets: 33 | - 34 | id: fun1 35 | num: 2 36 | labeldistance: 4.5 37 | - 38 | origin: fun1 39 | targets: 40 | - 41 | id: ost1 42 | num: 3 43 | labeldistance: 4.5 44 | -------------------------------------------------------------------------------- /pkg/graph/graph_test.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestDefaultAttributes(t *testing.T) { 9 | g := New() 10 | 11 | want := `digraph {fontname="Fira Mono Bold";fontsize="13.00";labelloc="t";newrank="true";nodesep="0.80";rankdir="LR";ranksep="1.10 equally";}` 12 | if got := flatten(g.String()); got != want { 13 | t.Errorf("got [%v] want [%v]", got, want) 14 | } 15 | } 16 | 17 | func TestBottomTopLayout(t *testing.T) { 18 | g := New(BottomTop(true)) 19 | 20 | want := `digraph {fontname="Fira Mono Bold";fontsize="13.00";labelloc="t";newrank="true";nodesep="0.80";rankdir="BT";ranksep="1.10 equally";}` 21 | if got := flatten(g.String()); got != want { 22 | t.Errorf("got [%v] want [%v]", got, want) 23 | } 24 | } 25 | 26 | // remove tabs and newlines and spaces 27 | func flatten(s string) string { 28 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 29 | } 30 | -------------------------------------------------------------------------------- /examples/token-manager.yml: -------------------------------------------------------------------------------- 1 | backgroundColor: '#ffffff' 2 | components: 3 | - 4 | kind: waf 5 | - 6 | kind: cdn 7 | - 8 | kind: gtw 9 | - 10 | kind: lba 11 | - 12 | kind: fun 13 | label: Verify JWT 14 | - 15 | kind: fun 16 | label: Verify OTP 17 | - 18 | kind: kub 19 | - 20 | kind: ser 21 | label: Token Manager 22 | - 23 | kind: mem 24 | - 25 | kind: ost 26 | - 27 | kind: rdb 28 | label: MySQL 29 | connections: 30 | - 31 | origin: waf1 32 | targets: 33 | - 34 | id: cdn1 35 | - 36 | origin: cdn1 37 | targets: 38 | - 39 | id: gtw1 40 | - 41 | origin: gtw1 42 | targets: 43 | - 44 | id: lba1 45 | - 46 | id: fun1 47 | - 48 | id: fun2 49 | - 50 | origin: lba1 51 | targets: 52 | - 53 | id: kub1 54 | - 55 | origin: kub1 56 | targets: 57 | - 58 | id: ser1 59 | - 60 | origin: ser1 61 | targets: 62 | - 63 | id: mem1 64 | - 65 | id: ost1 66 | - 67 | id: rdb1 68 | -------------------------------------------------------------------------------- /examples/connections.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: gtw 6 | - 7 | kind: fun 8 | - 9 | kind: fun 10 | - 11 | kind: rdb 12 | - 13 | kind: ser 14 | - 15 | kind: fun 16 | - 17 | kind: unk 18 | - 19 | kind: unk 20 | connections: 21 | - 22 | origin: gtw1 23 | targets: 24 | - 25 | id: fun1 26 | num: 1 27 | labeldistance: 10 28 | minlen: 2 29 | label: 'dir: forward (default)' 30 | - 31 | origin: fun2 32 | targets: 33 | - 34 | id: rdb1 35 | dir: both 36 | labeldistance: 4 37 | num: 2 38 | label: 'dir: both' 39 | - 40 | origin: fun3 41 | targets: 42 | - 43 | id: ser1 44 | dir: back 45 | labeldistance: 4 46 | num: 3 47 | label: 'dir: back' 48 | - 49 | origin: unk1 50 | targets: 51 | - 52 | id: unk2 53 | dir: none 54 | labeldistance: 4 55 | num: 4 56 | label: 'dir: none' 57 | 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luca Sepe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/edge/edge_test.go: -------------------------------------------------------------------------------- 1 | package edge 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/emicklei/dot" 8 | ) 9 | 10 | func TestDefaultAttributes(t *testing.T) { 11 | di := dot.NewGraph(dot.Directed) 12 | di.Node("A") 13 | di.Node("B") 14 | 15 | New(di, "A", "B") 16 | 17 | want := `digraph {n1[label="A"];n2[label="B"];n1->n2[arrowsize="0.6",fontname="Fira Mono",fontsize="8.00",penwidth="0.6"];}` 18 | if got := flatten(di.String()); got != want { 19 | t.Errorf("got [%v] want [%v]", got, want) 20 | } 21 | } 22 | 23 | func TestHighlightWithLabel(t *testing.T) { 24 | di := dot.NewGraph(dot.Directed) 25 | di.Node("A") 26 | di.Node("B") 27 | 28 | New(di, "A", "B", Highlight(true), Label(1, "Go!")) 29 | 30 | want := `digraph {n1[label="A"];n2[label="B"];n1->n2[arrowsize="0.9",fontname="Fira Mono",fontsize="8.00",label="Go!",penwidth="1.2"];}` 31 | if got := flatten(di.String()); got != want { 32 | t.Errorf("got [%v] want [%v]", got, want) 33 | } 34 | } 35 | 36 | // remove tabs and newlines and spaces 37 | func flatten(s string) string { 38 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 39 | } 40 | -------------------------------------------------------------------------------- /examples/backend-for-frontend.yml: -------------------------------------------------------------------------------- 1 | title: Backend For Frontend (BFF) 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: web 6 | label: Web App 7 | - 8 | kind: web 9 | label: Mobile App 10 | - 11 | kind: gtw 12 | label: Web BFF 13 | - 14 | kind: gtw 15 | label: Mobile BFF 16 | - 17 | kind: ser 18 | label: μService A 19 | - 20 | kind: ser 21 | label: μService B 22 | - 23 | kind: ser 24 | label: μService C 25 | - 26 | kind: ser 27 | label: μService D 28 | connections: 29 | - 30 | origin: web1 31 | targets: 32 | - 33 | id: gtw1 34 | color: '#ee82ee' 35 | - 36 | origin: web2 37 | targets: 38 | - 39 | id: gtw2 40 | - 41 | origin: gtw1 42 | targets: 43 | - 44 | id: ser1 45 | color: '#ee82ee' 46 | - 47 | id: ser2 48 | color: '#ee82ee' 49 | - 50 | id: ser3 51 | color: '#ee82ee' 52 | - 53 | id: ser4 54 | color: '#ee82ee' 55 | - 56 | origin: gtw2 57 | targets: 58 | - 59 | id: ser1 60 | highlight: true 61 | - 62 | id: ser2 63 | highlight: true 64 | - 65 | id: ser3 66 | highlight: true 67 | - 68 | id: ser4 69 | highlight: true 70 | -------------------------------------------------------------------------------- /examples/token-manager-google.yml: -------------------------------------------------------------------------------- 1 | backgroundColor: '#ffffff' 2 | components: 3 | - 4 | kind: waf 5 | provider: google 6 | - 7 | kind: cdn 8 | provider: google 9 | - 10 | kind: gtw 11 | provider: google 12 | - 13 | kind: lba 14 | provider: google 15 | - 16 | kind: fun 17 | label: Verify JWT 18 | provider: google 19 | - 20 | kind: fun 21 | label: Verify OTP 22 | provider: google 23 | - 24 | kind: kub 25 | provider: google 26 | - 27 | kind: ser 28 | label: Token Manager 29 | - 30 | kind: mem 31 | provider: google 32 | - 33 | kind: ost 34 | provider: google 35 | - 36 | kind: rdb 37 | provider: google 38 | label: MySQL 39 | connections: 40 | - 41 | origin: waf1 42 | targets: 43 | - 44 | id: cdn1 45 | - 46 | origin: cdn1 47 | targets: 48 | - 49 | id: gtw1 50 | - 51 | origin: gtw1 52 | targets: 53 | - 54 | id: lba1 55 | - 56 | id: fun1 57 | - 58 | id: fun2 59 | - 60 | origin: lba1 61 | targets: 62 | - 63 | id: kub1 64 | - 65 | origin: kub1 66 | targets: 67 | - 68 | id: ser1 69 | - 70 | origin: ser1 71 | targets: 72 | - 73 | id: mem1 74 | - 75 | id: ost1 76 | - 77 | id: rdb1 78 | -------------------------------------------------------------------------------- /cmd/draft-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #export DRAFT_ICONS_PATH=../icons 4 | SRC_DIR=../examples 5 | DPI=110 6 | EXE=./dist/draft_linux_amd64/draft 7 | 8 | ## declare an array of files 9 | declare -a arr=("$SRC_DIR/clients.yml" 10 | "$SRC_DIR/networking.yml" 11 | "$SRC_DIR/compute.yml" 12 | "$SRC_DIR/database.yml" 13 | "$SRC_DIR/storage.yml" 14 | "$SRC_DIR/security.yml" 15 | "$SRC_DIR/connections.yml" ) 16 | 17 | ## now loop through the above array 18 | for i in "${arr[@]}" 19 | do 20 | # grab the filename without extension 21 | filename=$(basename -- "$i") 22 | # run draft...run! 23 | "$EXE" -impl -verbose "$i" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/${filename%.*}.png" 24 | done 25 | 26 | "$EXE" -impl "$SRC_DIR/s3-upload-presigned-url.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/s3-upload-presigned-url.png" 27 | "$EXE" -impl "$SRC_DIR/backend-for-frontend.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/backend-for-frontend.png" 28 | "$EXE" -impl "$SRC_DIR/cognito-custom-auth-flow.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/cognito-custom-auth-flow.png" 29 | "$EXE" -impl "$SRC_DIR/token-manager.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/token-manager.png" 30 | "$EXE" -impl "$SRC_DIR/token-manager-google.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/token-manager-google.png" 31 | "$EXE" -impl "$SRC_DIR/auth0-custom-db-connection-with-jwt.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/auth0-custom-db-connection-with-jwt.png" 32 | -------------------------------------------------------------------------------- /examples/cognito-custom-auth-flow.yml: -------------------------------------------------------------------------------- 1 | title: Amazon Cognito Custom Authentication Flow with external database 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: web 6 | label: SPA 7 | - 8 | kind: cli 9 | - 10 | kind: ser 11 | label: | 12 | OAuth 2.0 13 | Auth Service 14 | provider: aws 15 | impl: Cognito 16 | - 17 | kind: fun 18 | label: | 19 | Define 20 | AuthChallange 21 | provider: aws 22 | outline: AWS 23 | - 24 | kind: fun 25 | label: | 26 | Create 27 | AuthChallange 28 | provider: aws 29 | outline: AWS 30 | - 31 | kind: fun 32 | label: | 33 | Verify 34 | AuthChallange 35 | provider: aws 36 | outline: AWS 37 | - 38 | kind: rdb 39 | label: | 40 | Users 41 | Repository 42 | outline: On Prem 43 | impl: Oracle DB 44 | connections: 45 | - 46 | origin: web1 47 | targets: 48 | - 49 | id: ser1 50 | - 51 | origin: cli1 52 | targets: 53 | - 54 | id: ser1 55 | - 56 | origin: ser1 57 | targets: 58 | - 59 | id: fun1 60 | num: 1 61 | labeldistance: 4.5 62 | - 63 | id: fun2 64 | num: 2 65 | labeldistance: 4.5 66 | - 67 | id: fun3 68 | num: 4 69 | labeldistance: 4.5 70 | - 71 | origin: fun2 72 | targets: 73 | - 74 | id: rdb1 75 | num: 3 76 | labeldistance: 4.5 77 | -------------------------------------------------------------------------------- /draft_test.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/emicklei/dot" 9 | ) 10 | 11 | func TestIDAutoGen(t *testing.T) { 12 | tests := []struct { 13 | kind string 14 | want string 15 | }{ 16 | {kindCDN, "cdn1"}, 17 | {kindCDN, "cdn2"}, 18 | {kindCDN, "cdn3"}, 19 | {kindService, "ser1"}, 20 | {kindService, "ser2"}, 21 | {kindCDN, "cdn4"}, 22 | {kindService, "ser3"}, 23 | } 24 | 25 | gen := idAutoGen() 26 | 27 | for _, tt := range tests { 28 | com := Component{Kind: tt.kind} 29 | 30 | t.Run(tt.kind, func(t *testing.T) { 31 | 32 | gen(&com) 33 | if got := com.ID; got != tt.want { 34 | t.Errorf("got [%v] want [%v]", got, tt.want) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestSketchComponents(t *testing.T) { 41 | gfx := dot.NewGraph(dot.Directed) 42 | 43 | items := []Component{ 44 | {Kind: kindGateway}, 45 | {Kind: kindFunction}, 46 | } 47 | 48 | if err := sketchComponents(gfx, Config{}, items); err != nil { 49 | t.Error(err) 50 | } 51 | 52 | if n, ok := gfx.FindNodeById("gtw1"); ok { 53 | got := fmt.Sprintf("%v", n.Value("label")) 54 | want := "default/gtw.png" 55 | 56 | if !strings.Contains(got, want) { 57 | t.Errorf("got [%v] want [%v]", got, want) 58 | } 59 | } 60 | 61 | if n, ok := gfx.FindNodeById("fun1"); ok { 62 | got := fmt.Sprintf("%v", n.Value("label")) 63 | want := "default/fun.png" 64 | if !strings.Contains(got, want) { 65 | t.Errorf("got [%v] want [%v]", got, want) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /impl.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | "strings" 7 | 8 | "github.com/rakyll/statik/fs" 9 | ) 10 | 11 | func setImpl(com *Component) { 12 | if s := strings.TrimSpace(com.Impl); len(s) > 0 { 13 | return 14 | } 15 | 16 | impl := getCloudImpl(com.Provider, com.Kind) 17 | if len(impl) > 0 { 18 | com.Impl = impl 19 | } 20 | } 21 | 22 | func getCloudImpl(provider, kind string) string { 23 | switch strings.TrimSpace(strings.ToLower(provider)) { 24 | case "aws": 25 | return awsImpl()(kind) 26 | case "google": 27 | return googleImpl()(kind) 28 | case "azure": 29 | return azureImpl()(kind) 30 | default: 31 | return defaultImpl()(kind) 32 | } 33 | } 34 | 35 | func awsImpl() func(string) string { 36 | dict, _ := readCsvFile("/aws.csv") 37 | 38 | return func(key string) string { 39 | return dict[key] 40 | } 41 | } 42 | 43 | func googleImpl() func(string) string { 44 | dict, _ := readCsvFile("/google.csv") 45 | 46 | return func(key string) string { 47 | return dict[key] 48 | } 49 | } 50 | 51 | func azureImpl() func(string) string { 52 | dict, _ := readCsvFile("/azure.csv") 53 | 54 | return func(key string) string { 55 | return dict[key] 56 | } 57 | } 58 | 59 | func defaultImpl() func(string) string { 60 | dict, _ := readCsvFile("/default.csv") 61 | 62 | return func(key string) string { 63 | return dict[key] 64 | } 65 | } 66 | 67 | func readCsvFile(filePath string) (map[string]string, error) { 68 | dict := map[string]string{} 69 | 70 | sfs, err := fs.New() 71 | if err != nil { 72 | return dict, err 73 | } 74 | 75 | f, err := sfs.Open(filePath) 76 | if err != nil { 77 | return dict, err 78 | } 79 | defer f.Close() 80 | 81 | csvr := csv.NewReader(f) 82 | 83 | for { 84 | row, err := csvr.Read() 85 | if err != nil { 86 | if err == io.EOF { 87 | err = nil 88 | } 89 | return dict, err 90 | } 91 | 92 | dict[row[0]] = row[1] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /impl_test.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | //go:generate statik -f -p statik -src=./assets 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestReadCsvFile(t *testing.T) { 10 | dat, err := readCsvFile("/default.csv") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | if len(dat) == 0 { 16 | t.Error("len(dat) should not be 0") 17 | } 18 | } 19 | 20 | func TestCloudImpl(t *testing.T) { 21 | tests := []struct { 22 | provider string 23 | key string 24 | want string 25 | }{ 26 | {"aws", "bst", "EBS"}, 27 | {"aws", "lba", "ELB"}, 28 | {"aws", "ost", "S3"}, 29 | 30 | {"google", "kub", `Kubernetes\nEngine`}, 31 | {"google", "mem", "Memorystore"}, 32 | {"google", "ost", "Storage"}, 33 | 34 | {"azure", "dns", "DNS"}, 35 | {"azure", "mem", "Redis Caches"}, 36 | {"azure", "waf", "Firewall"}, 37 | 38 | {"default", "dns", "DNS"}, 39 | {"default", "mem", "Cache"}, 40 | {"default", "waf", "Firewall"}, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.want, func(t *testing.T) { 45 | if got := getCloudImpl(tt.provider, tt.key); got != tt.want { 46 | t.Errorf("got [%v] want [%v]", got, tt.want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestSetImpl(t *testing.T) { 53 | tests := []struct { 54 | provider string 55 | kind string 56 | want string 57 | }{ 58 | {"aws", "bst", "EBS"}, 59 | {"aws", "lba", "ELB"}, 60 | {"aws", "ost", "S3"}, 61 | 62 | {"google", "kub", `Kubernetes\nEngine`}, 63 | {"google", "mem", "Memorystore"}, 64 | {"google", "ost", "Storage"}, 65 | 66 | {"azure", "dns", "DNS"}, 67 | {"azure", "mem", "Redis Caches"}, 68 | {"azure", "waf", "Firewall"}, 69 | } 70 | 71 | for _, tt := range tests { 72 | t.Run(tt.want, func(t *testing.T) { 73 | com := Component{Provider: tt.provider, Kind: tt.kind} 74 | setImpl(&com) 75 | 76 | if got := com.Impl; got != tt.want { 77 | t.Errorf("got [%v] want [%v]", got, tt.want) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestShowImplOpt(t *testing.T) { 11 | 12 | cfg := NewConfig(ShowImpl(true)) 13 | if got := cfg.showImpl; got != true { 14 | t.Errorf("got [%v] want [%v]", got, true) 15 | } 16 | 17 | ShowImpl(false)(&cfg) 18 | if got := cfg.showImpl; got != false { 19 | t.Errorf("got [%v] want [%v]", got, false) 20 | } 21 | } 22 | 23 | func TestBottomTopOpt(t *testing.T) { 24 | cfg := NewConfig() 25 | 26 | BottomTop(true)(&cfg) 27 | if got := cfg.bottomTop; got != true { 28 | t.Errorf("got [%v] want [true]", got) 29 | } 30 | } 31 | 32 | func TestVerboseOpt(t *testing.T) { 33 | cfg := NewConfig() 34 | 35 | Verbose(true)(&cfg) 36 | if got := cfg.verbose; got != true { 37 | t.Errorf("got [%v] want [true]", got) 38 | } 39 | } 40 | 41 | func TestLoadFromHTTPUri(t *testing.T) { 42 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | fmt.Fprintln(w, `title: Upload file to S3 using Lambda for pre-signed URL 44 | backgroundColor: '#ffffff' 45 | components: 46 | - 47 | kind: cli 48 | label: "Web App" 49 | impl: SPA 50 | - 51 | kind: gtw 52 | - 53 | kind: fun 54 | label: Get Pre-Signed URL 55 | - 56 | kind: ost 57 | label: "*.jpg\n*.png"`) 58 | })) 59 | defer ts.Close() 60 | 61 | cfg := NewConfig(URI(ts.URL)) 62 | prj, err := Load(cfg) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | want := "Upload file to S3 using Lambda for pre-signed URL" 68 | if got := prj.Title; got != want { 69 | t.Errorf("got [%v] want [%v]", got, want) 70 | } 71 | 72 | if got := len(prj.Components); got != 4 { 73 | t.Errorf("got [%v] want [3]", got) 74 | } 75 | 76 | want = "cli" 77 | if got := prj.Components[0].Kind; got != want { 78 | t.Errorf("got [%v] want [%v]", got, want) 79 | } 80 | 81 | want = "Get Pre-Signed URL" 82 | if got := prj.Components[2].Label; got != want { 83 | t.Errorf("got [%v] want [%v]", got, want) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/cluster/cluster.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/emicklei/dot" 8 | ) 9 | 10 | // Attribute is a function that apply a property to a cluster. 11 | type Attribute func(*dot.Graph) 12 | 13 | // Label is the cluster caption. 14 | func Label(label string) Attribute { 15 | return func(el *dot.Graph) { 16 | el.Attr("label", label) 17 | } 18 | } 19 | 20 | // PenColor set the color used to draw the 21 | // bounding box around a cluster. 22 | func PenColor(color string) Attribute { 23 | return func(el *dot.Graph) { 24 | if strings.TrimSpace(color) != "" { 25 | el.Attr("pencolor", color) 26 | } 27 | } 28 | } 29 | 30 | // FontColor specify the text color. 31 | func FontColor(color string) Attribute { 32 | return func(el *dot.Graph) { 33 | if strings.TrimSpace(color) != "" { 34 | el.Attr("fontcolor", color) 35 | } else { 36 | el.Attr("fontcolor", "#000000ff") 37 | } 38 | } 39 | } 40 | 41 | // FontName specify the font used for text. 42 | func FontName(name string) Attribute { 43 | return func(el *dot.Graph) { 44 | el.Attr("fontname", name) 45 | } 46 | } 47 | 48 | // FontSize specify the font size, in points, used for text. 49 | func FontSize(size float32) Attribute { 50 | return func(el *dot.Graph) { 51 | fs := fmt.Sprintf("%.2f", size) 52 | el.Attr("fontsize", fs) 53 | } 54 | } 55 | 56 | // BottomTop sets the label location according to layout. 57 | func BottomTop(bt bool) Attribute { 58 | return func(el *dot.Graph) { 59 | if bt { 60 | el.Attr("labelloc", "b") 61 | } else { 62 | el.Attr("labelloc", "t") 63 | } 64 | } 65 | } 66 | 67 | // New create a new cluster with the specified attributes. 68 | func New(parent *dot.Graph, id string, attrs ...Attribute) *dot.Graph { 69 | cluster := parent.Subgraph(id, dot.ClusterOption{}) 70 | 71 | // default attributes 72 | FontName("Fira Mono Bold")(cluster) 73 | FontSize(9)(cluster) 74 | FontColor("#000000") 75 | PenColor("transparent")(cluster) 76 | 77 | for _, opt := range attrs { 78 | opt(cluster) 79 | } 80 | return cluster 81 | } 82 | -------------------------------------------------------------------------------- /kinds.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import "strings" 4 | 5 | const ( 6 | kindBlockStore = "bst" 7 | kindCDN = "cdn" 8 | kindClient = "cli" 9 | kindUnknown = "unk" 10 | kindDNS = "dns" 11 | kindNoSQL = "doc" 12 | kindFileStore = "fst" 13 | kindFunction = "fun" 14 | kindGateway = "gtw" 15 | kindContainersManager = "kub" 16 | kindLBA = "lba" 17 | kindCache = "mem" 18 | kindPubSub = "msg" 19 | kindObjectStore = "ost" 20 | kindQueue = "que" 21 | kindRDB = "rdb" 22 | kindService = "ser" 23 | kindUser = "usr" 24 | kindFirewall = "waf" 25 | kindWeb = "web" 26 | ) 27 | 28 | // validateProvider sets a valid cloud provider (one of 'aws', 'gcp', 'azure') 29 | func validateProvider() func(com *Component) { 30 | provs := map[string]bool{"aws": true, "google": true, "azure": true} 31 | 32 | return func(com *Component) { 33 | val := strings.ToLower(strings.TrimSpace(com.Provider)) 34 | if provs[val] { 35 | com.Provider = val 36 | } else { 37 | com.Provider = "default" 38 | } 39 | } 40 | 41 | } 42 | 43 | func validateKind() func(com *Component) { 44 | kinds := map[string]bool{ 45 | kindBlockStore: true, 46 | kindCDN: true, 47 | kindClient: true, 48 | kindDNS: true, 49 | kindNoSQL: true, 50 | kindFileStore: true, 51 | kindFunction: true, 52 | kindGateway: true, 53 | kindContainersManager: true, 54 | kindLBA: true, 55 | kindCache: true, 56 | kindPubSub: true, 57 | kindObjectStore: true, 58 | kindQueue: true, 59 | kindRDB: true, 60 | kindService: true, 61 | kindUser: true, 62 | kindFirewall: true, 63 | kindWeb: true, 64 | } 65 | 66 | return func(com *Component) { 67 | val := strings.ToLower(strings.TrimSpace(com.Kind)) 68 | if kinds[val] { 69 | com.Kind = val 70 | } else { 71 | com.Kind = kindUnknown 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/emicklei/dot" 8 | ) 9 | 10 | // Attribute is a function that apply a property to a node. 11 | type Attribute func(*dot.Node) 12 | 13 | // Label is the node caption. If 'htm' is true the 14 | // caption is treated as HTML code. 15 | func Label(label string, htm bool) Attribute { 16 | return func(el *dot.Node) { 17 | if htm { 18 | el.Attr("label", dot.HTML(label)) 19 | } else { 20 | el.Attr("label", label) 21 | } 22 | } 23 | } 24 | 25 | // Shape sets the shape of a node. 26 | func Shape(shape string) Attribute { 27 | return func(el *dot.Node) { 28 | el.Attr("shape", shape) 29 | } 30 | } 31 | 32 | // Rounded sets the shape with round corners. 33 | func Rounded(rounded bool) Attribute { 34 | return func(el *dot.Node) { 35 | if rounded { 36 | el.Attr("style", "rounded,filled") 37 | } else { 38 | el.Attr("style", "filled") 39 | } 40 | } 41 | } 42 | 43 | // FillColor sets the node fill color. 44 | func FillColor(color string) Attribute { 45 | return func(el *dot.Node) { 46 | if strings.TrimSpace(color) != "" { 47 | el.Attr("fillcolor", color) 48 | } 49 | } 50 | } 51 | 52 | // FontColor specify the text color. 53 | func FontColor(color string) Attribute { 54 | return func(el *dot.Node) { 55 | if strings.TrimSpace(color) != "" { 56 | el.Attr("fontcolor", color) 57 | } 58 | } 59 | } 60 | 61 | // FontName specify the font used for text. 62 | func FontName(name string) Attribute { 63 | return func(el *dot.Node) { 64 | el.Attr("fontname", name) 65 | } 66 | } 67 | 68 | // FontSize specify the font size, in points, used for text. 69 | func FontSize(size float32) Attribute { 70 | return func(el *dot.Node) { 71 | fs := fmt.Sprintf("%.2f", size) 72 | el.Attr("fontsize", fs) 73 | } 74 | } 75 | 76 | // New create a new node with the specified attributes. 77 | func New(cluster *dot.Graph, id string, attrs ...Attribute) *dot.Node { 78 | el := cluster.Node(id) 79 | 80 | // default attributes 81 | Rounded(false)(&el) 82 | FontName("Fira Mono")(&el) 83 | FontSize(9)(&el) 84 | 85 | for _, opt := range attrs { 86 | opt(&el) 87 | } 88 | 89 | return &el 90 | } 91 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Draft Examples 3 | 4 | Collection of [draft](https://github.com/lucasepe/draft/releases/latest) architecture descriptor YAML files as examples. 5 | 6 | ## Sample 1 - Message Bus Pattern 7 | 8 | The `draft` architecture descriptor YAML file is here 👉 [message-bus-pattern.yml](./message-bus-pattern.yml) 9 | 10 | Running `draft` with this command: 11 | 12 | ```bash 13 | draft -impl message-bus-pattern.yml | dot -Tpng > message-bus-pattern.png 14 | ``` 15 | 16 | ![](./message-bus-pattern.png) 17 | 18 | ## Sample 2 - Getting the pre-signed URL to Upload a file to Amazon S3 19 | 20 | The `draft` architecture descriptor YAML file is here 👉 [s3-upload-presigned-url.yml](./s3-upload-presigned-url.yml) 21 | 22 | Running `draft` with this command: 23 | 24 | ```bash 25 | draft -impl s3-upload-presigned-url.yml | dot -Tpng > s3-upload-presigned-url.png 26 | ``` 27 | 28 | ![](./s3-upload-presigned-url.png) 29 | 30 | ## Sample 3 - Amazon Cognito Custom Authentication Flow with external Database 31 | 32 | The `draft` architecture descriptor YAML file is here 👉 [cognito-custom-auth-flow.yml](./cognito-custom-auth-flow.yml) 33 | 34 | Running `draft` with this command: 35 | 36 | ```bash 37 | draft -impl cognito-custom-auth-flow.yml | dot -Tpng > cognito-custom-auth-flow.png 38 | ``` 39 | 40 | ![](./cognito-custom-auth-flow.png) 41 | 42 | ## Example 4 - A Google Cloud Architecture 43 | 44 | The `draft` architecture descriptor YAML file is here 👉 [token-manager-google.yml](./token-manager-google.yml) 45 | 46 | Running `draft` with this command: 47 | 48 | ```bash 49 | draft -impl token-manager-google.yml | dot -Tpng > token-manager-google.png 50 | ``` 51 | 52 | ![](./token-manager-google.png) 53 | 54 | ## Example 5 - Auth0 Custom Database Connection with Client Credentials 55 | 56 | The `draft` architecture descriptor YAML file is here 👉 [auth0-custom-db-connection-with-jwt.yml](./auth0-custom-db-connection-with-jwt.yml) 57 | 58 | Running `draft` with this command: 59 | 60 | ```bash 61 | draft -impl auth0-custom-db-connection-with-jwt.yml | dot -Tpng > auth0-custom-db-connection-with-jwt.png 62 | ``` 63 | 64 | ![](./auth0-custom-db-connection-with-jwt.png) 65 | 66 | 67 | 68 | --- 69 | 70 | # Others examples 71 | 72 | Check out this folder for more [draft](https://github.com/lucasepe/draft/releases/latest) architecture descriptor YAML examples. 73 | -------------------------------------------------------------------------------- /pkg/node/node_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/emicklei/dot" 8 | ) 9 | 10 | func TestDefaultAttributes(t *testing.T) { 11 | di := dot.NewGraph(dot.Directed) 12 | New(di, "NODE_1") 13 | 14 | want := `digraph {n1[fontname="Fira Mono",fontsize="9.00",label="NODE_1",style="filled"];}` 15 | if got := flatten(di.String()); got != want { 16 | t.Errorf("got [%v] want [%v]", got, want) 17 | } 18 | } 19 | 20 | func TestFontColor(t *testing.T) { 21 | di := dot.NewGraph(dot.Directed) 22 | New(di, "NODE_1", FontColor("#fafafaff")) 23 | 24 | want := `digraph {n1[fontcolor="#fafafaff",fontname="Fira Mono",fontsize="9.00",label="NODE_1",style="filled"];}` 25 | if got := flatten(di.String()); got != want { 26 | t.Errorf("got [%v] want [%v]", got, want) 27 | } 28 | } 29 | 30 | func TestFillColor(t *testing.T) { 31 | di := dot.NewGraph(dot.Directed) 32 | New(di, "NODE_1", FillColor("#ff0000ff")) 33 | 34 | want := `digraph {n1[fillcolor="#ff0000ff",fontname="Fira Mono",fontsize="9.00",label="NODE_1",style="filled"];}` 35 | if got := flatten(di.String()); got != want { 36 | t.Errorf("got [%v] want [%v]", got, want) 37 | } 38 | } 39 | 40 | func TestShape(t *testing.T) { 41 | di := dot.NewGraph(dot.Directed) 42 | New(di, "NODE_1", Shape("box")) 43 | 44 | want := `digraph {n1[fontname="Fira Mono",fontsize="9.00",label="NODE_1",shape="box",style="filled"];}` 45 | if got := flatten(di.String()); got != want { 46 | t.Errorf("got [%v] want [%v]", got, want) 47 | } 48 | } 49 | 50 | func TestPlainLabel(t *testing.T) { 51 | di := dot.NewGraph(dot.Directed) 52 | New(di, "NODE_1", Label("Service", false)) 53 | 54 | want := `digraph {n1[fontname="Fira Mono",fontsize="9.00",label="Service",style="filled"];}` 55 | if got := flatten(di.String()); got != want { 56 | t.Errorf("got [%v] want [%v]", got, want) 57 | } 58 | } 59 | 60 | func TestHTMLLabel(t *testing.T) { 61 | di := dot.NewGraph(dot.Directed) 62 | New(di, "NODE_1", Label("Service", true)) 63 | 64 | want := `digraph {n1[fontname="Fira Mono",fontsize="9.00",label=<Service>,style="filled"];}` 65 | if got := flatten(di.String()); got != want { 66 | t.Errorf("got [%v] want [%v]", got, want) 67 | } 68 | } 69 | 70 | // remove tabs and newlines and spaces 71 | func flatten(s string) string { 72 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/draft-all copy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export DRAFT_ICONS_PATH=../icons 4 | SRC_DIR=../examples 5 | DPI=120 6 | EXE=./dist/draft_linux_amd64/draft 7 | 8 | ## declare an array of files 9 | declare -a arr=("$SRC_DIR/clients.yml" 10 | "$SRC_DIR/ser.yml" 11 | "$SRC_DIR/msg.yml" 12 | "$SRC_DIR/gtw.yml" 13 | "$SRC_DIR/que.yml" 14 | "$SRC_DIR/fun.yml" 15 | "$SRC_DIR/rdb.yml" 16 | "$SRC_DIR/doc.yml" 17 | "$SRC_DIR/bst.yml" 18 | "$SRC_DIR/ost.yml" 19 | "$SRC_DIR/fst.yml" 20 | "$SRC_DIR/lba.yml" 21 | "$SRC_DIR/cdn.yml" 22 | "$SRC_DIR/dns.yml" 23 | "$SRC_DIR/waf.yml" 24 | "$SRC_DIR/kub.yml" 25 | "$SRC_DIR/mem.yml" ) 26 | 27 | ## now loop through the above array 28 | for i in "${arr[@]}" 29 | do 30 | # grab the filename without extension 31 | filename=$(basename -- "$i") 32 | # run draft...run! 33 | "$EXE" -verbose "$i" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/${filename%.*}.png" 34 | 35 | "$EXE" -verbose "$i" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/${filename%.*}_aws.png" 36 | "$EXE" -verbose "$i" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/${filename%.*}_google.png" 37 | "$EXE" -verbose "$i" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/${filename%.*}_azure.png" 38 | done 39 | 40 | "$EXE" -verbose "$SRC_DIR/system-view.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/system-view.png" 41 | "$EXE" -verbose "$SRC_DIR/system-view.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/system-view-aws.png" 42 | 43 | "$EXE" -verbose "$SRC_DIR/impl-example.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/impl-example.png" 44 | "$EXE" -verbose "$SRC_DIR/impl-example.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/impl-example-aws.png" 45 | "$EXE" -verbose "$SRC_DIR/impl-example.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/impl-example-google.png" 46 | "$EXE" -verbose "$SRC_DIR/impl-example.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/impl-example-azure.png" 47 | 48 | "$EXE" -verbose "$SRC_DIR/cognito-custom-auth-flow.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/cognito-custom-auth-flow.png" 49 | "$EXE" -verbose "$SRC_DIR/cognito-custom-auth-flow.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/cognito-custom-auth-flow-aws.png" 50 | 51 | "$EXE" -verbose "$SRC_DIR/s3-upload-presigned-url.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/s3-upload-presigned-url.png" 52 | "$EXE" -verbose -impl aws "$SRC_DIR/s3-upload-presigned-url.yml" | dot -Tpng -Gdpi=$DPI > "$SRC_DIR/s3-upload-presigned-url-aws.png" 53 | 54 | "$EXE" -verbose "$SRC_DIR/demo.yml" | dot -Tpng > "$SRC_DIR/demo.png" 55 | "$EXE" -verbose "$SRC_DIR/demo.yml" | dot -Tpng > "$SRC_DIR/demo-aws.png" 56 | "$EXE" -verbose "$SRC_DIR/demo.yml" | dot -Tpng > "$SRC_DIR/demo-google.png" 57 | "$EXE" -verbose "$SRC_DIR/demo.yml" | dot -Tpng > "$SRC_DIR/demo-azure.png" -------------------------------------------------------------------------------- /pkg/cluster/cluster_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/emicklei/dot" 9 | ) 10 | 11 | func TestDefaultAttributes(t *testing.T) { 12 | want := `digraph {subgraph cluster_s1 {fontname="Fira Mono Bold";fontsize="9.00";label="DUMMY";pencolor="transparent";}}` 13 | 14 | di := dot.NewGraph(dot.Directed) 15 | New(di, "DUMMY") 16 | 17 | if got := flatten(di.String()); got != want { 18 | t.Errorf("got [%v] want [%v]", got, want) 19 | } 20 | } 21 | 22 | func TestFontColor(t *testing.T) { 23 | di := dot.NewGraph(dot.Directed) 24 | New(di, "DUMMY", FontColor("#fafafaff")) 25 | 26 | want := `fontcolor="#fafafaff";fontname="Fira Mono Bold";fontsize="9.00";label="DUMMY"` 27 | got := flatten(di.String()) 28 | if !verify(got, want) { 29 | t.Errorf("got [%v] want [%v]", got, want) 30 | } 31 | } 32 | 33 | func TestFontName(t *testing.T) { 34 | di := dot.NewGraph(dot.Directed) 35 | New(di, "DUMMY", FontName("Inconsolata")) 36 | 37 | want := `fontname="Inconsolata";fontsize="9.00";label="DUMMY"` 38 | got := flatten(di.String()) 39 | if !verify(got, want) { 40 | t.Errorf("got [%v] want [%v]", got, want) 41 | } 42 | } 43 | 44 | func TestFontSize(t *testing.T) { 45 | di := dot.NewGraph(dot.Directed) 46 | New(di, "DUMMY", FontSize(14)) 47 | 48 | want := `fontsize="14.00";label="DUMMY"` 49 | got := flatten(di.String()) 50 | if !verify(got, want) { 51 | t.Errorf("got [%v] want [%v]", got, want) 52 | } 53 | } 54 | 55 | func TestPenColor(t *testing.T) { 56 | di := dot.NewGraph(dot.Directed) 57 | New(di, "DUMMY", PenColor("red")) 58 | 59 | want := `fontname="Fira Mono Bold";fontsize="9.00";label="DUMMY";pencolor="red"` 60 | got := flatten(di.String()) 61 | if !verify(got, want) { 62 | t.Errorf("got [%v] want [%v]", got, want) 63 | } 64 | } 65 | 66 | func TestLabel(t *testing.T) { 67 | di := dot.NewGraph(dot.Directed) 68 | New(di, "DUMMY", Label("AaA")) 69 | 70 | want := `fontname="Fira Mono Bold";fontsize="9.00";label="AaA";pencolor="transparent"` 71 | got := flatten(di.String()) 72 | if !verify(got, want) { 73 | t.Errorf("got [%v] want [%v]", got, want) 74 | } 75 | } 76 | 77 | func TestBottomTop(t *testing.T) { 78 | di := dot.NewGraph(dot.Directed) 79 | New(di, "DUMMY", BottomTop(true)) 80 | 81 | want := `fontname="Fira Mono Bold";fontsize="9.00";label="DUMMY";labelloc="b"` 82 | got := flatten(di.String()) 83 | if !verify(got, want) { 84 | t.Errorf("got [%v] want [%v]", got, want) 85 | } 86 | } 87 | 88 | // remove tabs and newlines and spaces 89 | func flatten(s string) string { 90 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 91 | } 92 | 93 | func verify(src, kv string) bool { 94 | r := regexp.MustCompile(`\{(.*)\}`) 95 | if m := r.FindAllStringSubmatch(src, -1); len(m) > 0 { 96 | return strings.Contains(m[0][1], kv) 97 | } 98 | return false 99 | } 100 | -------------------------------------------------------------------------------- /examples/auth0-custom-db-connection-with-jwt.yml: -------------------------------------------------------------------------------- 1 | title: Auth0 custom database login with client credentials 2 | backgroundColor: '#ffffff' 3 | components: 4 | - 5 | kind: web 6 | - 7 | kind: fun 8 | impl: Login Action Script 9 | label: login(email, password, callback) 10 | outline: Auth0 11 | - 12 | kind: ser 13 | outline: Auth0 14 | label: /oauth/token 15 | - 16 | kind: dns 17 | provider: AWS 18 | outline: AWS 19 | - 20 | kind: cdn 21 | provider: AWS 22 | outline: AWS 23 | - 24 | kind: gtw 25 | provider: AWS 26 | outline: AWS 27 | - 28 | kind: fun 29 | label: /login 30 | provider: AWS 31 | outline: AWS 32 | - 33 | kind: fun 34 | label: verify(access_token) 35 | impl: Authorizer 36 | provider: AWS 37 | outline: AWS 38 | - 39 | kind: rdb 40 | label: Users Repository 41 | impl: Oracle DB 42 | outline: On Prem 43 | 44 | connections: 45 | - 46 | origin: web1 47 | targets: 48 | - 49 | id: fun1 50 | num: 1 51 | label: GET /oauth/authorize 52 | labeldistance: 8 53 | minlen: 2 54 | dashed: false 55 | color: '#c0c0c0' 56 | - 57 | origin: fun1 58 | targets: 59 | - 60 | id: ser1 61 | num: 2 62 | label: POST /oauth/token
clientID
clientSecret 63 | labeldistance: 4 64 | minlen: 2 65 | dashed: false 66 | color: '#c0c0c0' 67 | - 68 | id: dns1 69 | num: 3 70 | label: POST /login
email
hash(pass)
access_token 71 | labeldistance: 10 72 | minlen: 2 73 | dashed: false 74 | color: '#c0c0c0' 75 | - 76 | origin: dns1 77 | targets: 78 | - 79 | id: cdn1 80 | num: 4 81 | labeldistance: 2 82 | minlen: 1.7 83 | dashed: false 84 | color: '#c0c0c0' 85 | - 86 | origin: cdn1 87 | targets: 88 | - 89 | id: gtw1 90 | num: 5 91 | labeldistance: 6 92 | dashed: false 93 | color: '#c0c0c0' 94 | - 95 | origin: gtw1 96 | targets: 97 | - 98 | id: fun2 99 | num: 7 100 | labeldistance: 6 101 | dashed: false 102 | color: '#c0c0c0' 103 | - 104 | id: fun3 105 | num: 6 106 | labeldistance: 2 107 | minlen: 1.7 108 | dashed: false 109 | color: '#c0c0c0' 110 | - 111 | origin: fun2 112 | targets: 113 | - 114 | id: rdb1 115 | num: 8 116 | labeldistance: 5 117 | dashed: false 118 | color: '#c0c0c0' 119 | ranks: 120 | - 121 | name: aaa 122 | components: 123 | - dns1 124 | - cdn1 125 | - 126 | name: bbb 127 | components: 128 | - gtw1 129 | - fun3 130 | - 131 | name: ccc 132 | components: 133 | - fun1 134 | - ser1 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.8.0] - 2020-07-04 8 | ### Added 9 | - 🎉 default component icons embedded in the [draft](https://github.com/lucasepe/draft) binary executable 10 | - 🎉 new connection attribute `num` in order to eventyally number them 11 | - 🎉 new `labeldistance` `labelangle` and `minlen` attributes in order to have full control of labels displacement 12 | 13 | ### Changed 14 | - complete restyling of the icons representing the individual components (see [README.md](./README.md)) 15 | - to render different providers implementations in the same graph `provider` is a _component_ attribute now 16 | - you can add eventually some simple HTML tag to the component attribute `label` 17 | - `-impl` flag it's now a simple boolean, if specified it will show each component `impl` attribute - otherwise it will be hide 18 | 19 | ## [0.7.0] - 2020-06-27 20 | ### Added 21 | - 🎉 load architecture YAML files from remote HTTP sites 22 | - `draft 'http://my.my.domain.com/arch.yml' | dot -Tpng > arch.png` 23 | 24 | ### Fixed 25 | - 🐛 [issue #5](https://github.com/lucasepe/draft/issues/5) labels for all components and connections 26 | 27 | ### Changed 28 | - `impl` flag value for Google Cloud is now `google` 29 | 30 | ## [0.6.1] - 2020-06-22 31 | ### Fixed 32 | - 🐛 outline frames not working 33 | 34 | ## [0.6.0] - 2020-06-20 35 | ### Added 36 | - 🎉 new feature use specific icons for each provider (see [README](./README.md) for the _How To_) 37 | - 🎉 icons for AWS, GCP and Azure cloud providers (see: [icons/](./icons/) folder) 38 | - new commandline flag `-verbose` to print some debug infos 39 | 40 | ### Changed 41 | - 🎉 total code refactoring - more function(al) less OOP 42 | - easier to add new components 43 | 44 | ## [0.5.0] - 2020-06-15 45 | ### Added 46 | - new component _Block Storage_ (kind: `bst`) 47 | - new component _Object Storage_ (kind: `ost`) 48 | - new component _File Storage_ (kind: `fst`) 49 | - new component _RDBMS_ (kind: `rdb`) 50 | - new component _No SQL_ (kind: `doc`) 51 | - new component _Caching_ (kind: `mem`) 52 | - new commandline flag `-impl=[aws,gcp,azure]` to auto fill components implementations according to the specified provider 53 | 54 | ### Changed 55 | - autogenerated id prefix now is equal to the _kind_ value (see [./README.md](README.md)) **breaking change** 56 | - modified YAML schema 57 | - connection info (see [./README.md](README.md)) **breaking change** 58 | - modified YAML schema 59 | 60 | ### Removed 61 | - generic html component 62 | 63 | ## [0.4.0] - 2020-06-11 64 | ### Added 65 | - This CHANGELOG file 66 | - Test cases 67 | - New commandline flag `-bottom-top` to set bottom top layout 68 | - New component kind: [`container-service`](./examples/cos.yml) 69 | - New component kind: [`waf`](./examples/waf.yml) 70 | - New example [./examples/system-view.yml](./examples/system-view.yml) 71 | -------------------------------------------------------------------------------- /pkg/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/emicklei/dot" 8 | ) 9 | 10 | // Attribute is a function that apply a property to a Graph. 11 | type Attribute func(*dot.Graph) 12 | 13 | // Label is the Graph title. 14 | func Label(label string) Attribute { 15 | return func(el *dot.Graph) { 16 | if strings.TrimSpace(label) != "" { 17 | el.Attr("label", label) 18 | } 19 | } 20 | } 21 | 22 | // FontName specify the font used for the Graph title. 23 | func FontName(name string) Attribute { 24 | return func(el *dot.Graph) { 25 | el.Attr("fontname", name) 26 | } 27 | } 28 | 29 | // FontSize specify the font size, in points, used for title. 30 | func FontSize(size float32) Attribute { 31 | return func(el *dot.Graph) { 32 | fs := fmt.Sprintf("%.2f", size) 33 | el.Attr("fontsize", fs) 34 | } 35 | } 36 | 37 | // LeftToRight sets direction of Graph layout from left to right. 38 | func LeftToRight() Attribute { 39 | return func(el *dot.Graph) { 40 | el.Attr("rankdir", "LR") 41 | } 42 | } 43 | 44 | // BottomTop sets direction of Graph layout from bottom to top. 45 | func BottomTop(enable bool) Attribute { 46 | return func(el *dot.Graph) { 47 | if enable { 48 | el.Attr("rankdir", "BT") 49 | } 50 | } 51 | } 52 | 53 | // RankSep gives the desired rank separation, in inches. 54 | // This is the minimum vertical distance between the bottom 55 | // of the nodes in one rank and the tops of nodes in the next. 56 | func RankSep(size float32) Attribute { 57 | return func(el *dot.Graph) { 58 | fs := fmt.Sprintf("%.2f equally", size) 59 | el.Attr("ranksep", fs) 60 | } 61 | } 62 | 63 | // NodeSep specifies the minimum space between two 64 | // adjacent nodes in the same rank, in inches. 65 | func NodeSep(size float32) Attribute { 66 | return func(el *dot.Graph) { 67 | fs := fmt.Sprintf("%.2f", size) 68 | el.Attr("nodesep", fs) 69 | } 70 | } 71 | 72 | // Ortho controls how edges are represented, if true lines are orthogonal. 73 | func Ortho(enable bool) Attribute { 74 | return func(el *dot.Graph) { 75 | if enable { 76 | el.Attr("splines", "ortho") 77 | } else { 78 | el.Attr("splines", "line") 79 | } 80 | } 81 | } 82 | 83 | // BackgroundColor sets the Graph background color. 84 | func BackgroundColor(color string) Attribute { 85 | return func(el *dot.Graph) { 86 | if strings.TrimSpace(color) != "" { 87 | el.Attr("bgcolor", color) 88 | } else { 89 | el.Attr("bgcolor", "transparent") 90 | } 91 | } 92 | } 93 | 94 | // New create a new Graph with the specified attributes. 95 | func New(attrs ...Attribute) *dot.Graph { 96 | el := dot.NewGraph(dot.Directed) 97 | el.Attr("newrank", "true") 98 | el.Attr("labelloc", "t") 99 | //el.Attr("sep", "+10,10") 100 | el.Attr("overlap", "scalexy") 101 | el.Attr("nodesep", "0.6") 102 | 103 | FontName("Fira Mono Bold")(el) 104 | FontSize(13)(el) 105 | LeftToRight()(el) 106 | RankSep(1.2)(el) 107 | 108 | for _, opt := range attrs { 109 | opt(el) 110 | } 111 | 112 | return el 113 | } 114 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // Opt is a function to define a configuration property. 16 | type Opt func(cfg *Config) 17 | 18 | // Verbose sets verbose output. 19 | func Verbose(enable bool) func(*Config) { 20 | return func(cfg *Config) { 21 | cfg.verbose = enable 22 | } 23 | } 24 | 25 | // BottomTop sets the graph layout. 26 | func BottomTop(enable bool) func(*Config) { 27 | return func(cfg *Config) { 28 | cfg.bottomTop = enable 29 | } 30 | } 31 | 32 | // URI sets the input YAML definition file. 33 | // Can be also an HTTP URL. 34 | func URI(s string) func(*Config) { 35 | return func(cfg *Config) { 36 | if !strings.HasPrefix(s, "http") { 37 | cfg.uri, _ = filepath.Abs(s) 38 | } else { 39 | cfg.uri = s 40 | } 41 | } 42 | } 43 | 44 | // IconsPath sets the custom icons path. 45 | func IconsPath(s string) func(*Config) { 46 | return func(cfg *Config) { 47 | cfg.iconsPath, _ = filepath.Abs(strings.TrimSpace(s)) 48 | os.Mkdir(filepath.Join(cfg.iconsPath, "default"), os.ModePerm) 49 | } 50 | } 51 | 52 | // ShowImpl show the cloud provider implementation. 53 | func ShowImpl(show bool) func(*Config) { 54 | return func(cfg *Config) { 55 | cfg.showImpl = show 56 | } 57 | } 58 | 59 | // Config defines the 'draft' configuration. 60 | type Config struct { 61 | bottomTop bool 62 | verbose bool 63 | showImpl bool 64 | iconsPath string 65 | uri string 66 | } 67 | 68 | // NewConfig create a configuration 69 | // with the specified attributes. 70 | func NewConfig(opts ...Opt) Config { 71 | res := Config{} 72 | 73 | for _, op := range opts { 74 | op(&res) 75 | } 76 | 77 | return res 78 | } 79 | 80 | // Load a YAML from the config info. 81 | func Load(cfg Config) (Design, error) { 82 | const bytesLimit = 500 * 1024 83 | 84 | if strings.HasPrefix(cfg.uri, "http") { 85 | body, err := getURI(cfg.uri, bytesLimit) 86 | if err != nil { 87 | return Design{}, err 88 | } 89 | 90 | return decodeYAML(body) 91 | } 92 | 93 | body, err := getFILE(cfg.uri, bytesLimit) 94 | if err != nil { 95 | return Design{}, err 96 | } 97 | 98 | return decodeYAML(body) 99 | } 100 | 101 | // getURI fetch data (with limit) from an HTTP URL 102 | func getURI(uri string, limit int64) ([]byte, error) { 103 | response, err := http.Get(uri) 104 | if err != nil { 105 | return nil, err 106 | } 107 | defer response.Body.Close() 108 | 109 | return ioutil.ReadAll(io.LimitReader(response.Body, limit)) 110 | } 111 | 112 | // getFILE fetch data (with limit) from an file 113 | func getFILE(fin string, limit int64) ([]byte, error) { 114 | file, err := os.Open(fin) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | defer file.Close() 120 | 121 | return ioutil.ReadAll(io.LimitReader(file, limit)) 122 | } 123 | 124 | // decodeYAML decode a YAML to return a Design struct. 125 | func decodeYAML(dat []byte) (Design, error) { 126 | res := Design{} 127 | 128 | // Init new YAML decode 129 | d := yaml.NewDecoder(bytes.NewReader(dat)) 130 | 131 | // Start YAML decoding from file 132 | if err := d.Decode(&res); err != nil { 133 | return res, err 134 | } 135 | 136 | return res, nil 137 | } 138 | -------------------------------------------------------------------------------- /icon.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | //go:generate statik -f -p statik -src=./assets 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/emicklei/dot" 13 | "github.com/lucasepe/draft/pkg/node" 14 | 15 | // init the embedded file system 16 | _ "github.com/lucasepe/draft/statik" 17 | "github.com/rakyll/statik/fs" 18 | ) 19 | 20 | // render a Component using the cloud provider icon. 21 | func render(ctx Config, com Component, gfx *dot.Graph) *dot.Node { 22 | 23 | img := iconPath(ctx, com) 24 | 25 | if fc := strings.TrimSpace(com.FontColor); len(fc) == 0 { 26 | com.FontColor = "#000000ff" 27 | } 28 | 29 | if imp := strings.TrimSpace(com.Impl); len(imp) == 0 { 30 | com.Impl = " " 31 | } 32 | 33 | var sb strings.Builder 34 | sb.WriteString(``) 35 | if ctx.showImpl { 36 | fmt.Fprintf(&sb, ``, com.Impl) 37 | } 38 | 39 | sb.WriteString("") 40 | fmt.Fprintf(&sb, ``, img) 41 | sb.WriteString("") 42 | 43 | label := " " 44 | if s := strings.TrimSpace(com.Label); len(s) > 0 { 45 | label = s 46 | } 47 | fmt.Fprintf(&sb, ``, label) 48 | sb.WriteString("
%s
%s
") 49 | 50 | return node.New(gfx, com.ID, 51 | node.Label(sb.String(), true), 52 | node.FillColor("transparent"), 53 | node.Shape("plain"), 54 | ) 55 | } 56 | 57 | // iconPath resolve the component icon path by it's provider attribute. 58 | // If the icon does not exists returns a default image. 59 | func iconPath(ctx Config, com Component) string { 60 | if prov := strings.TrimSpace(com.Provider); len(prov) == 0 { 61 | com.Provider = "default" 62 | } else { 63 | com.Provider = strings.ToLower(com.Provider) 64 | } 65 | 66 | fn := fmt.Sprintf("%s.png", com.Kind) 67 | 68 | dst := filepath.Join(ctx.iconsPath, com.Provider, fn) 69 | if fileExists(dst) { 70 | return dst 71 | } 72 | 73 | src := filepath.Join(ctx.iconsPath, "default", fn) 74 | 75 | if ctx.verbose { 76 | fmt.Fprintf(os.Stderr, " ! file '%s' not found\n", dst) 77 | } 78 | 79 | if !fileExists(src) { 80 | if err := copyFileTo(fmt.Sprintf("/icons/%s", fn), src); err != nil { 81 | if os.IsNotExist(err) { 82 | src = filepath.Join(ctx.iconsPath, "default", "def.png") 83 | copyFileTo("/icons/def.png", src) 84 | } 85 | } 86 | fmt.Fprintf(os.Stderr, " • using default image: '%s'\n", src) 87 | } 88 | 89 | return src 90 | } 91 | 92 | // copyFileTo extract the embedded icon to the user file system. 93 | func copyFileTo(fromFn, toFn string) error { 94 | sfs, err := fs.New() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | from, err := sfs.Open(fromFn) 100 | if err != nil { 101 | return err 102 | } 103 | defer from.Close() 104 | 105 | to, err := os.OpenFile(toFn, os.O_RDWR|os.O_CREATE, 0666) 106 | if err != nil { 107 | return err 108 | } 109 | defer to.Close() 110 | 111 | _, err = io.Copy(to, from) 112 | return err 113 | } 114 | 115 | // fileExists checks if a file exists and is not a directory before we 116 | // try using it to prevent further errors. 117 | func fileExists(filename string) bool { 118 | info, err := os.Stat(filename) 119 | if os.IsNotExist(err) { 120 | return false 121 | } 122 | return !info.IsDir() 123 | } 124 | -------------------------------------------------------------------------------- /icon_test.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | //go:generate statik -f -p statik -src=./assets 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/emicklei/dot" 15 | ) 16 | 17 | func TestCopyFileTo(t *testing.T) { 18 | dir, err := ioutil.TempDir("", "icons") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer os.RemoveAll(dir) // clean up 23 | 24 | if err := os.Mkdir(filepath.Join(dir, "default"), os.ModePerm); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | tests := []struct { 29 | src string 30 | dst string 31 | }{ 32 | {"/icons/waf.png", fmt.Sprintf("%s/default/waf.png", dir)}, 33 | {"/icons/fun.png", fmt.Sprintf("%s/default/fun.png", dir)}, 34 | {"/icons/kub.png", fmt.Sprintf("%s/default/kub.png", dir)}, 35 | } 36 | 37 | for _, tt := range tests { 38 | t.Run(tt.src, func(t *testing.T) { 39 | if err := copyFileTo(tt.src, tt.dst); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if !fileExists(tt.dst) { 44 | t.Errorf("file [%v] does not exists", tt.dst) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestIconPath(t *testing.T) { 51 | dir, err := ioutil.TempDir("", "icons") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | defer os.RemoveAll(dir) // clean up 56 | 57 | tests := []struct { 58 | provider string 59 | kind string 60 | want string 61 | }{ 62 | {"aws", kindFirewall, fmt.Sprintf("%s/default/waf.png", dir)}, 63 | {"aws", kindFunction, fmt.Sprintf("%s/default/fun.png", dir)}, 64 | {"google", kindRDB, fmt.Sprintf("%s/default/rdb.png", dir)}, 65 | {"google", kindCache, fmt.Sprintf("%s/default/mem.png", dir)}, 66 | {"azure", kindDNS, fmt.Sprintf("%s/default/dns.png", dir)}, 67 | {"azure", kindCDN, fmt.Sprintf("%s/default/cdn.png", dir)}, 68 | } 69 | 70 | cfg := NewConfig(IconsPath(dir)) 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.kind, func(t *testing.T) { 74 | got := iconPath(cfg, Component{Kind: tt.kind}) 75 | if got != tt.want { 76 | t.Errorf("got [%v] want [%v]", got, tt.want) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestIconFigure(t *testing.T) { 83 | dir, err := ioutil.TempDir("", "icons") 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | defer os.RemoveAll(dir) // clean up 88 | 89 | tests := []struct { 90 | provider string 91 | kind string 92 | find string 93 | want bool 94 | }{ 95 | {"aws", kindFirewall, fmt.Sprintf(`src="%s/default/waf.png"`, dir), true}, 96 | {"aws", kindFunction, fmt.Sprintf(`src="%s/default/fun.png"`, dir), true}, 97 | {"google", kindRDB, fmt.Sprintf(`src="%s/default/rdb.png"`, dir), true}, 98 | {"google", kindCache, fmt.Sprintf(`src="%s/default/mem.png"`, dir), true}, 99 | {"azure", kindDNS, fmt.Sprintf(`src="%s/default/dns.png"`, dir), true}, 100 | {"azure", kindCDN, fmt.Sprintf(`src="%s/default/cdn.png"`, dir), true}, 101 | } 102 | 103 | cfg := NewConfig(IconsPath(dir)) 104 | 105 | gfx := dot.NewGraph(dot.Directed) 106 | 107 | for _, tt := range tests { 108 | com := Component{Kind: tt.kind} 109 | 110 | t.Run(tt.find, func(t *testing.T) { 111 | n := render(cfg, com, gfx) 112 | x := fmt.Sprintf("%v", n.AttributesMap.Value("label")) 113 | if got := strings.Contains(x, tt.find); got != tt.want { 114 | t.Errorf("got [%v] want [%v] : %s", got, tt.want, x) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | // remove tabs and newlines and spaces 121 | func flatten(s string) string { 122 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 123 | } 124 | 125 | func verify(src, kv string) bool { 126 | r := regexp.MustCompile(`n1\[(.*)\]`) 127 | if m := r.FindAllStringSubmatch(src, -1); len(m) > 0 { 128 | return strings.Contains(m[0][1], kv) 129 | } 130 | return false 131 | } 132 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/lucasepe/draft" 12 | "github.com/mitchellh/go-homedir" 13 | ) 14 | 15 | // go run main.go | dot -Tpng -Gdpi=300 > test.png 16 | 17 | const ( 18 | envIconsPath = "DRAFT_ICONS_PATH" 19 | banner = ` 20 | ______ __ _ 21 | | _ \ / _|| | Crafted with passion by Luca Sepe 22 | | | | | _ __ __ _ | |_ | |_ 23 | | | | || '__| / _' || _|| __| https://github.com/lucasepe/draft 24 | | |/ / | | | (_| || | | |_ 25 | |___/ |_| \__,_||_| \__| v{{VERSION}}` 26 | ) 27 | 28 | var ( 29 | version = "dev" 30 | commit = "none" 31 | date = "unknown" 32 | 33 | flagBottomTop bool 34 | flagOrtho bool 35 | flagVerbose bool 36 | flagImpl bool 37 | ) 38 | 39 | func main() { 40 | if dir := os.Getenv(envIconsPath); len(dir) == 0 { 41 | setDefaultIconsPath() 42 | } 43 | 44 | configureFlags() 45 | 46 | if flag.CommandLine.Arg(0) == "" { 47 | flag.CommandLine.Usage() 48 | os.Exit(2) 49 | } 50 | 51 | uri := flag.Args()[0] 52 | 53 | cfg := draft.NewConfig( 54 | draft.Verbose(flagVerbose), 55 | draft.BottomTop(flagBottomTop), 56 | draft.IconsPath(os.Getenv(envIconsPath)), 57 | draft.ShowImpl(flagImpl), 58 | draft.URI(uri), 59 | ) 60 | 61 | dia, err := draft.Sketch(cfg) 62 | handleErr(err, uri) 63 | 64 | fmt.Println(dia) 65 | } 66 | 67 | // handleErr check for an error and eventually exit 68 | func handleErr(err error, src string) { 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, "error: %s @ %s\n", err.Error(), src) 71 | os.Exit(1) 72 | } 73 | } 74 | 75 | func configureFlags() { 76 | name := appName() 77 | 78 | flag.CommandLine.Usage = func() { 79 | printBanner() 80 | fmt.Printf("Generate High Level Cloud Architecture diagrams using YAML.\n\n") 81 | 82 | fmt.Print("USAGE:\n\n") 83 | fmt.Printf(" %s [options] \n\n", name) 84 | 85 | fmt.Print("EXAMPLE(s):\n\n") 86 | fmt.Printf(" %s input.yml | dot -Tpng -Gdpi=200 > output.png\n", name) 87 | fmt.Printf(" %s http://a.domain.com/input.yml | dot -Tpng -Gdpi=200 > output.png\n\n", name) 88 | 89 | fmt.Print("OPTIONS:\n\n") 90 | flag.CommandLine.SetOutput(os.Stdout) 91 | flag.CommandLine.PrintDefaults() 92 | flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors 93 | fmt.Print(" -help\n\tprints this message\n") 94 | fmt.Println() 95 | 96 | fmt.Print("ENVIRONMENT:\n\n") 97 | fmt.Fprint(os.Stdout, " DRAFT_ICONS_PATH\n") 98 | fmt.Fprintf(os.Stdout, " \tthe base path for custom icons (default %s)\n", os.Getenv(envIconsPath)) 99 | fmt.Println() 100 | } 101 | 102 | flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors 103 | flag.CommandLine.Init(os.Args[0], flag.ExitOnError) 104 | 105 | flag.CommandLine.BoolVar(&flagImpl, "impl", false, "if true show the component provider implementation") 106 | flag.CommandLine.BoolVar(&flagBottomTop, "bottom-top", false, "if true sets layout direction as bottom top") 107 | flag.CommandLine.BoolVar(&flagVerbose, "verbose", false, fmt.Sprintf("show some extra info as %s is running", name)) 108 | 109 | flag.CommandLine.Parse(os.Args[1:]) 110 | } 111 | 112 | func printBanner() { 113 | fmt.Print(strings.Trim(strings.Replace(banner, "{{VERSION}}", version, 1), "\n"), "\n\n") 114 | } 115 | 116 | // setDefaultIconsPath sets the default icons directory 117 | // creating it if does not exists. 118 | func setDefaultIconsPath() error { 119 | home, err := homedir.Dir() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | workdir := filepath.Join(home, fmt.Sprintf(".%s", appName()), "icons") 125 | if _, err := os.Stat(workdir); os.IsNotExist(err) { 126 | err = os.MkdirAll(workdir, os.ModePerm) 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | 132 | os.Mkdir(filepath.Join(workdir, "default"), os.ModePerm) 133 | 134 | os.Setenv(envIconsPath, workdir) 135 | return nil 136 | } 137 | 138 | func appName() string { 139 | return filepath.Base(os.Args[0]) 140 | } 141 | -------------------------------------------------------------------------------- /pkg/edge/edge.go: -------------------------------------------------------------------------------- 1 | package edge 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/emicklei/dot" 9 | ) 10 | 11 | // Attribute is a function that apply a property to an edge. 12 | type Attribute func(*dot.Edge) 13 | 14 | // Label is the edge caption. If 'htm' is true the 15 | // caption is treated as HTML code. 16 | func Label(num int, text string) Attribute { 17 | return func(el *dot.Edge) { 18 | var render = false 19 | var sb strings.Builder 20 | sb.WriteString(``) 21 | if num > 0 { 22 | render = true 23 | sb.WriteString(`") 26 | } 27 | 28 | if lab := strings.TrimSpace(text); len(lab) > 0 { 29 | render = true 30 | sb.WriteString("") 33 | } 34 | sb.WriteString("
`) 24 | sb.WriteString(strconv.Itoa(num)) 25 | sb.WriteString("
") 31 | sb.WriteString(lab) 32 | sb.WriteString("
") 35 | 36 | if render { 37 | el.Attr("taillabel", dot.HTML(sb.String())) 38 | } 39 | } 40 | } 41 | 42 | // LabelDistance adjusts the distance that the 43 | // headlabel(taillabel) is from the head(tail) node. 44 | func LabelDistance(dist float32) Attribute { 45 | return func(el *dot.Edge) { 46 | 47 | el.Attr("labeldistance", fmt.Sprintf("%.2f", dist)) 48 | } 49 | } 50 | 51 | // LabelAngle along with labeldistance, determine where 52 | // the headlabel (taillabel) are placed with respect 53 | // to the head (tail) in polar coordinates. 54 | // The origin in the coordinate system is the point 55 | // where the edge touches the node. 56 | // The ray of 0 degrees goes from the origin back along 57 | // the edge, parallel to the edge at the origin. 58 | // The angle, in degrees, specifies the rotation from 59 | // the 0 degree ray, with positive angles moving counterclockwise 60 | // and negative angles moving clockwise. 61 | func LabelAngle(angle float32) Attribute { 62 | return func(el *dot.Edge) { 63 | el.Attr("labelangle", fmt.Sprintf("%.2f", angle)) 64 | } 65 | } 66 | 67 | // MinLen sets the minimum edge length (rank difference between head and tail). 68 | func MinLen(len float32) Attribute { 69 | return func(el *dot.Edge) { 70 | if len <= 0 { 71 | return 72 | } 73 | el.Attr("minlen", fmt.Sprintf("%.2f", len)) 74 | } 75 | } 76 | 77 | // FontName specify the font used for text. 78 | func FontName(name string) Attribute { 79 | return func(el *dot.Edge) { 80 | el.Attr("fontname", name) 81 | } 82 | } 83 | 84 | // FontSize specify the font size, in points, used for text. 85 | func FontSize(size float32) Attribute { 86 | return func(el *dot.Edge) { 87 | fs := fmt.Sprintf("%.2f", size) 88 | el.Attr("fontsize", fs) 89 | } 90 | } 91 | 92 | // Dir sets the ege direction, values: both, forward, back, none. 93 | func Dir(dir string) Attribute { 94 | return func(el *dot.Edge) { 95 | if strings.TrimSpace(dir) != "" { 96 | el.Attr("dir", dir) 97 | } 98 | } 99 | } 100 | 101 | // Dashed set the edge line dashed. 102 | func Dashed(dashed bool) Attribute { 103 | return func(el *dot.Edge) { 104 | if dashed { 105 | el.Attr("style", "dashed") 106 | } 107 | } 108 | } 109 | 110 | // Color set the color for an edge line. 111 | func Color(color string) Attribute { 112 | return func(el *dot.Edge) { 113 | if strings.TrimSpace(color) != "" { 114 | el.Attr("color", color) 115 | } else { 116 | el.Attr("color", "#708090ff") 117 | } 118 | } 119 | } 120 | 121 | // Highlight makes the line thicker. 122 | func Highlight(enable bool) Attribute { 123 | return func(el *dot.Edge) { 124 | if enable { 125 | el.Attr("penwidth", "1.2") 126 | el.Attr("arrowsize", "0.9") 127 | } else { 128 | el.Attr("penwidth", "0.6") 129 | el.Attr("arrowsize", "0.6") 130 | } 131 | } 132 | } 133 | 134 | // New add to dot.Graph a new connection line between two components. 135 | func New(g *dot.Graph, fromNodeID, toNodeID string, attrs ...Attribute) error { 136 | n1, ok := g.FindNodeById(fromNodeID) 137 | if !ok { 138 | return fmt.Errorf("node with id=%s not found", fromNodeID) 139 | } 140 | 141 | n2, ok := g.FindNodeById(toNodeID) 142 | if !ok { 143 | return fmt.Errorf("node with id=%s not found", toNodeID) 144 | } 145 | 146 | el := g.Edge(n1, n2) 147 | 148 | FontName("Fira Mono")(&el) 149 | FontSize(8)(&el) 150 | Highlight(false)(&el) 151 | 152 | for _, opt := range attrs { 153 | opt(&el) 154 | } 155 | 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /draft.go: -------------------------------------------------------------------------------- 1 | package draft 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/emicklei/dot" 10 | "github.com/lucasepe/draft/pkg/cluster" 11 | "github.com/lucasepe/draft/pkg/edge" 12 | "github.com/lucasepe/draft/pkg/graph" 13 | ) 14 | 15 | // Connection is a link between two components. 16 | type Connection struct { 17 | Origin string `yaml:"origin"` 18 | Targets []struct { 19 | ID string `yaml:"id"` 20 | Label string `yaml:"label,omitempty"` 21 | LabelDistance float32 `yaml:"labeldistance,omitempty"` 22 | LabelAngle float32 `yaml:"labelangle,omitempty"` 23 | MinLen float32 `yaml:"minlen,omitempty"` 24 | Num int `yaml:"num,omitempty"` 25 | Color string `yaml:"color,omitempty"` 26 | Dashed bool `yaml:"dashed,omitempty"` 27 | Dir string `yaml:"dir,omitempty"` 28 | Highlight bool `yaml:"highlight,omitempty"` 29 | } `yaml:"targets"` 30 | } 31 | 32 | // Component is a basic architecture unit. 33 | type Component struct { 34 | ID string `yaml:"id,omitempty"` 35 | Kind string `yaml:"kind"` 36 | Label string `yaml:"label,omitempty"` 37 | Outline string `yaml:"outline,omitempty"` 38 | Impl string `yaml:"impl,omitempty"` 39 | Provider string `yaml:"provider,omitempty"` 40 | FontColor string `yaml:"fontColor,omitempty"` 41 | } 42 | 43 | // Design represents a whole diagram. 44 | type Design struct { 45 | Title string `yaml:"title,omitempty"` 46 | BackgroundColor string `yaml:"backgroundColor,omitempty"` 47 | Components []Component `yaml:"components"` 48 | Connections []Connection `yaml:"connections,omitempty"` 49 | Ranks []Rank `yaml:"ranks,omitempty"` 50 | } 51 | 52 | // Rank define the nodes laying on the same level. 53 | type Rank struct { 54 | Name string `yaml:"name"` 55 | Components []string `yaml:"components"` 56 | } 57 | 58 | // Sketch generates the GraphViz definition for this architecture diagram. 59 | func Sketch(cfg Config) (string, error) { 60 | prj, err := Load(cfg) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | if cfg.verbose { 66 | fmt.Fprintf(os.Stderr, "elaborating draft architecture definition: %s\n", cfg.uri) 67 | } 68 | 69 | gfx := graph.New(graph.BackgroundColor(prj.BackgroundColor), 70 | //graph.Ortho(ark.ortho), 71 | graph.BottomTop(cfg.bottomTop), 72 | graph.Label(prj.Title)) 73 | 74 | if err := sketchComponents(gfx, cfg, prj.Components); err != nil { 75 | return "", err 76 | } 77 | 78 | if err := sketchConnections(gfx, cfg, prj.Connections); err != nil { 79 | return "", err 80 | } 81 | 82 | sketchSameRanks(gfx, cfg, prj.Ranks) 83 | 84 | return gfx.String(), nil 85 | } 86 | 87 | // idAutoGen auto generate a component id. 88 | func idAutoGen() func(comp *Component) { 89 | counters := make(map[string]int16) 90 | 91 | return func(comp *Component) { 92 | if strings.TrimSpace(comp.ID) == "" { 93 | key := comp.Kind 94 | counters[key]++ 95 | comp.ID = fmt.Sprintf("%s%d", key, counters[key]) 96 | } 97 | } 98 | } 99 | 100 | // sketchComponents draws all components. 101 | func sketchComponents(gfx *dot.Graph, cfg Config, items []Component) error { 102 | genID := idAutoGen() 103 | fixKind := validateKind() 104 | fixProvider := validateProvider() 105 | 106 | for _, it := range items { 107 | genID(&it) 108 | 109 | fixKind(&it) 110 | fixProvider(&it) 111 | setImpl(&it) 112 | 113 | if cfg.verbose { 114 | bin, _ := json.Marshal(it) 115 | fmt.Fprintf(os.Stderr, " • component: %s\n", string(bin)) 116 | } 117 | 118 | parent := gfx 119 | if box := strings.TrimSpace(it.Outline); len(box) > 0 { 120 | parent = cluster.New(gfx, it.Outline, 121 | cluster.PenColor("#78959b"), 122 | cluster.FontSize(10), 123 | cluster.FontColor("#63625b")) 124 | parent.Attr("style", "dashed,rounded") 125 | } 126 | 127 | render(cfg, it, parent) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // sketchConnections draws connections 134 | func sketchConnections(gfx *dot.Graph, ctx Config, items []Connection) error { 135 | for _, el := range items { 136 | 137 | for _, x := range el.Targets { 138 | if ctx.verbose { 139 | fmt.Fprintf(os.Stderr, " • connection: <%s> to <%s>\n", el.Origin, x.ID) 140 | } 141 | 142 | err := edge.New(gfx, el.Origin, x.ID, 143 | edge.Label(x.Num, x.Label), 144 | edge.Dir(x.Dir), 145 | edge.Color(x.Color), 146 | edge.Dashed(x.Dashed), 147 | edge.LabelDistance(x.LabelDistance), 148 | edge.LabelAngle(x.LabelAngle), 149 | edge.MinLen(x.MinLen), 150 | edge.Highlight(x.Highlight)) 151 | 152 | if err != nil { 153 | return err 154 | } 155 | } 156 | } 157 | 158 | return nil 159 | } 160 | 161 | // sketchSameRanks groups component belonging to the same rank. 162 | func sketchSameRanks(gfx *dot.Graph, ctx Config, items []Rank) { 163 | for _, grp := range items { 164 | if name := strings.TrimSpace(grp.Name); len(name) > 0 { 165 | for _, el := range grp.Components { 166 | if n, ok := gfx.FindNodeById(el); ok { 167 | gfx.AddToSameRank(name, n) 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draft 2 | 3 | A commandline tool that generate **H**igh **L**evel microservice & serverless **A**rchitecture diagrams using a declarative syntax defined in a YAML file. 4 | 5 | - Works on [linux, macOS, windows](https://github.com/lucasepe/draft/releases/latest) 6 | - Just a [single portable binary file](https://github.com/lucasepe/draft/releases/latest) 7 | - Input data in flat YAML text files 8 | - Usable with shell scripts 9 | 10 | 11 | # Why? 12 | 13 | I prefer to think in terms of capabilities rather than specific vendor services. 14 | 15 | - _"do we need a DNS?"_ instead of _"do we need Route 53?"_ 16 | - _"do we need a CDN?"_ instead of _"do we need Cloudfront?"_ 17 | - _"do we need a database? if yes? what type? Relational? No SQL"_ instead of "do we need Google Cloud Datastore?"_ 18 | - _"do we need some serverless function?"_ instead of _"do we need an Azure Function"_ 19 | 20 | ...and so on. 21 | 22 | # How `draft` works? 23 | 24 | `draft` takes in input a declarative YAML file and generates a [`dot`](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) script for [Graphviz](https://www.graphviz.org/) 25 | 26 | ```bash 27 | draft backend-for-frontend.yml | dot -Tpng -Gdpi=200 > backend-for-frontend.png 28 | ``` 29 | 30 | Piping the `draft` output to [GraphViz](http://www.graphviz.org/doc/info/output.html/) `dot` you can generate different output formats: 31 | 32 | | format | command | 33 | |:-------------|:---------------------------------------------------------------| 34 | | PNG | draft input.yml | dot -Tpng > output.png | 35 | | JPEG | draft input.yml | dot -Tjpg > output.jpg | 36 | | PostScript | draft input.yml | dot -Tps > output.ps | 37 | | SVG | draft input.yml | dot -Tsvg > output.svg | 38 | 39 | To install GraphViz to your favorite OS, please, follow this link [https://graphviz.gitlab.io/download/](https://graphviz.gitlab.io/download/). 40 | 41 | # Installation Steps 42 | 43 | To build the binaries by yourself, assuming that you have `Go` installed, here the steps: 44 | 45 | Clone the repo, 46 | ```psh 47 | git clone https://github.com/lucasepe/draft.git 48 | ``` 49 | 50 | Move to the 'cmd' directory: 51 | ```psh 52 | cd draft/cmd 53 | ``` 54 | 55 | Generate the static assets 56 | ```psh 57 | go generate ../... 58 | ``` 59 | 60 | Build the binary tool 61 | ```psh 62 | go build -o draft 63 | ``` 64 | 65 | # Components 66 | 67 | The basic unit of each _draft_ design is the `component`, has these attributes: 68 | 69 | | Name | Required | Scope | Notes | 70 | |:----------|:--------:|:------------------------------|---------------------------------------------------------| 71 | | id | no | used for the connecttions | autogenerated if omitted (read more for details...) | 72 | | kind | yes | identify the component type | see [all available kinds](#list-of-all-available-kinds) | 73 | | provider | no | get the specific provider icon| see [using custom icons](#using-custom-icons) | 74 | | label | no | text below the component icon | can contain basic HTML tags | 75 | | outline | no | tag to group components | | 76 | | impl | no | text above the icon | can use this to specify the provider implementation | 77 | | fontColor | no | the label text color | hex color code - supports transparency too | 78 | 79 | ### Notes about a component `id` 80 | 81 | - you can define your component `id` explicitly (i.e. _id: MY_SERVICE_A_) 82 | - or you can omit the component `id` attribute and it will be autogenerated 83 | 84 | ### How is auto-generated a component `id`? 85 | 86 | An auto-generated component `id` has a prefix and a sequential number 87 | 88 | - the prefix is related to the component `kind` 89 | - examples _waf1, ..., wafN_ or _ser1, ..., serN_ etc. 90 | 91 | # List of all available kinds 92 | 93 | Draft uses a set of symbols independent from the different providers (AWS, Microsoft Azure, GCP). 94 | 95 | Below is a list of all the components currently implemented. 96 | 97 | ## Clients 98 | 99 | Sample YAML file [examples/clients.yml](./examples/clients.yml). 100 | 101 | ```bash 102 | draft -impl -verbose examples/clients.yml | dot -Gdpi=110 -Tpng > examples/clients.png 103 | ``` 104 | 105 | ![Clients](examples/clients.png) 106 | 107 | ## Networking 108 | 109 | Sample YAML file [examples/networking.yml](./examples/networking.yml). 110 | 111 | ```bash 112 | draft -impl -verbose examples/networking.yml | dot -Gdpi=110 -Tpng > examples/networking.png 113 | ``` 114 | 115 | ![Networking](examples/networking.png) 116 | 117 | ## Compute 118 | 119 | Sample YAML file [examples/compute.yml](./examples/compute.yml). 120 | 121 | ```bash 122 | draft -impl -verbose examples/compute.yml | dot -Gdpi=110 -Tpng > examples/compute.png 123 | ``` 124 | 125 | ![Compute](examples/compute.png) 126 | 127 | ## Database 128 | 129 | Sample YAML file [examples/database.yml](./examples/database.yml). 130 | 131 | ```bash 132 | draft -impl -verbose examples/database.yml | dot -Gdpi=110 -Tpng > examples/database.png 133 | ``` 134 | 135 | ![Database](examples/database.png) 136 | 137 | ## Storage 138 | 139 | Sample YAML file [examples/storage.yml](./examples/storage.yml). 140 | 141 | ```bash 142 | draft -impl -verbose examples/storage.yml | dot -Gdpi=110 -Tpng > examples/storage.png 143 | ``` 144 | 145 | ![Storage](examples/storage.png) 146 | 147 | ## Security 148 | 149 | Sample YAML file [examples/security.yml](./examples/security.yml). 150 | 151 | ```bash 152 | draft -impl -verbose examples/security.yml | dot -Gdpi=110 -Tpng > examples/security.png 153 | ``` 154 | 155 | ![Security](examples/security.png) 156 | 157 | # Using custom icons 158 | 159 | Here how to render components with specific _aws_, _google_ and _azure_ icons. 160 | 161 | 1. Download the PNG icons of your cloud provider [AWS](https://aws.amazon.com/it/architecture/icons/), [GCP](https://cloud.google.com/icons), [Azure](https://www.microsoft.com/en-us/download/details.aspx?id=41937) 162 | 163 | 2. Take only the icons related to the [components supported](#list-of-all-available-kinds) by [draft](https://github.com/lucasepe/draft/releases/latest) 164 | 165 | 3. Make a directory with the provider name (i.e. `/draft/icons/aws`, `/draft/icons/google`, `/draft/icons/azure`) 166 | 167 | 4. Rename each icon as [draft](https://github.com/lucasepe/draft/releases/latest) components `kind` (i.e. `dns.png`, `cdn.png` and so on...) 168 | 169 | 5. Run [draft](https://github.com/lucasepe/draft/releases/latest) specifyng the icons folder using the environment variable `DRAFT_ICONS_PATH` 170 | - example: `DRAFT_ICONS_PATH=/draft/icons draft my-file.yml | dot > ark-aws.png` 171 | 172 | 👉 I have already done all the work for points 1 to 4. So you can avoid it by copying the directory [icons](./icons) 👈 173 | 174 | 175 | # Connections 176 | 177 | The arrows that join the components. 178 | 179 | To connect an _origin_ component with one or more _targets_ component you need to specify at least each `id`. 180 | 181 | A _connection_ has the following properties: 182 | 183 | | Attribute | Type | Required | What is it? | 184 | |:----------|:--------:|:--------:|--------------------------------| 185 | | origin | string | yes | id of the starting component | 186 | | targets | object | yes | one or more destinations | 187 | 188 | Each _target_ has the following properties: 189 | 190 | | Attribute | Type | Required | What is it? | 191 | |:---------------|:--------:|:--------:|---------------------------------------------------| 192 | | id | string | yes | target component id | 193 | | label | string | no | text on the connection | 194 | | labeldistance | float | no | distance of the label from the connection tail | 195 | | labelangle | float | no | determine the label position relative to the tail | 196 | | minlen | float | no | sets the minimum connection length | 197 | | num | int | no | usefult to track an order path on your graph | 198 | | color | string | no | label color (hex color string) | 199 | | dashed | bool | no | if true the connection line will be dashed | 200 | | dir | string | no | arrows direction (forward, back, both, none) | 201 | | highlight | bool | no | if true makes the arrow thicker | 202 | 203 | 204 | Sample YAML file [examples/connections.yml](./examples/connections.yml). 205 | 206 | ```bash 207 | draft examples/connections.yml | dot -Gdpi=110 -Tpng > examples/connections.png 208 | ``` 209 | 210 | ![Connections](examples/connections.png) 211 | 212 | 213 | [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Flucasepe%2Fdraft)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Flucasepe%2Fdraft) 214 | 215 | --- 216 | 217 | ## Changelog 218 | 219 | 👉 [Record of all notable changes made to a project](./CHANGELOG.md) 220 | 221 | --- 222 | 223 | ## Examples 224 | 225 | 👉 [Collection of draft architecture descriptor YAML files](./examples/README.md) 226 | 227 | --- 228 | 229 | (c) 2020 Luca Sepe http://lucasepe.it. MIT License 230 | --------------------------------------------------------------------------------