diff --git a/README.md b/README.md index d475fd7..9beebdb 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,82 @@ # Blast Radius -[![CircleCI](https://circleci.com/gh/28mm/blast-radius/tree/master.svg?style=svg)](https://circleci.com/gh/28mm/blast-radius/tree/master) [![PyPI version](https://badge.fury.io/py/BlastRadius.svg)](https://badge.fury.io/py/BlastRadius) [terraform]: https://www.terraform.io/ [examples]: https://28mm.github.io/blast-radius-docs/ +[overlayfs]: -_Blast Radius_ is a tool for reasoning about [Terraform][] dependency graphs -with interactive visualizations. +_Blast Radius_ is a tool for reasoning about [Terraform][] dependency graphs with interactive visualizations. Use _Blast Radius_ to: * __Learn__ about *Terraform* or one of its providers through real [examples][] * __Document__ your infrastructure * __Reason__ about relationships between resources and evaluate changes to them -* __Interact__ with the diagram below (and many others) [in the docs][examples] +* __Interact__ with the diagrams below (and many others) [in the docs][examples] -![screenshot](doc/blastradius-interactive.png) +--- + +## Blast Radius +![screenshot](doc/blastradiusext.png) + +--- ## Prerequisites -* [Graphviz](https://www.graphviz.org/) * [Python](https://www.python.org/) 3.7 or newer +* [Terraform][] 0.12.x or newer -> __Note:__ For macOS you can `brew install graphviz` +--- ## Quickstart -The fastest way to get up and running with *Blast Radius* is to install it with -`pip` to your pre-existing environment: +For fastest way to get up and running with blast-radius is as follows: -```sh -pip install blastradius -``` +* Download and install the wheel files from the [release](https://github.com/nishubharti/blast-radius/releases) -Once installed just point *Blast Radius* at any initialized *Terraform* -directory: + ``` + copy the blastradius/server/static/images folder to the terraform directory + ``` + install the wheel file + ``` + easy_install blastradius-0.1.25.1-py3-none-any.whl + ``` + or + ``` + pip3 blastradius-0.1.25.1-py3-none-any.whl + ``` -```sh -blast-radius --serve /path/to/terraform/directory -``` +* Enrich the Blast Radius diagrams with the outcome of Terraform plan actions: + ``` + terraform plan --out tfplan.binary + terraform show -json tfplan.binary > tfplan.json + ``` + for including cost and policy information into blast-radius cost.json and policy.json file need to be stored into the working directory. + +* Once installed just point Blast Radius at any initialized Terraform directory: + ```sh + blast-radius --serve /path/to/terraform/directory + ``` + +* Go to the browser link http://127.0.0.1:5000/ to view the Blast Radius diagram for the terraform file. + + ![BlastRadius](doc/blastradiusext.png) + + The enrichments include - information from the Plan file, State file , cost file and time file . + Click the columns adjacent to the Resource Names to view these enrichment in the side panel view. + + ![BlastRadiusExt](doc/blast-radius-ext.png) -And you will shortly be rewarded with a browser link http://127.0.0.1:5000/. +--- + +## Build your own wheel file + +* Create wheel file of this repo + ```sh + python3 setup.py sdist bdist_wheel + ``` +--- ## Docker @@ -50,34 +85,28 @@ And you will shortly be rewarded with a browser link http://127.0.0.1:5000/. To launch *Blast Radius* for a local directory by manually running: -```sh -docker run --rm -it -p 5000:5000 \ - -v $(pwd):/data:ro \ - --security-opt apparmor:unconfined \ - --cap-add=SYS_ADMIN \ - 28mm/blast-radius -``` +* create a dockerhub account + ```sh + docker build -t /blast-radius:v1 . + docker push /blast-radius:v1 + ``` -A slightly more customized variant of this is also available as an example -[docker-compose.yml](./examples/docker-compose.yml) usecase for Workspaces. + ```sh + docker run --cap-add=SYS_ADMIN -dit -p 5000:5000 -v :/data:ro /blast-radius:v1 + ``` ### Docker configurations *Terraform* module links are saved as _absolute_ paths in relative to the project root (note `.terraform/modules/`). Given these paths will vary -betwen Docker and the host, we mount the volume as read-only, assuring we don't -ever interfere with your real environment. +betwen Docker and the host, we mount the volume as read-only, assuring we don't ever interfere with your real environment. -However, in order for *Blast Radius* to actually work with *Terraform*, it needs -to be initialized. To accomplish this, the container creates an [overlayfs][] -that exists within the container, overlaying your own, so that it can operate +However, in order for *Blast Radius* to actually work with *Terraform*, it needs to be initialized as well as planned compulsory. To accomplish this, the container creates an [overlayfs][] that exists within the container, overlaying your own, so that it can operate independently. To do this, certain runtime privileges are required -- specifically `--cap-add=SYS_ADMIN`. -For more information on how this works and what it means for your host, check -out the [runtime privileges][privileges] documentation. -#### Docker & Subdirectories +### Docker & Subdirectories If you organized your *Terraform* project using stacks and modules, *Blast Radius* must be called from the project root and reference them as @@ -101,14 +130,12 @@ It consists of 3 modules `foo`, `bar` and `dead`, followed by one `beef` stack. To apply *Blast Radius* to the `beef` stack, you would want to run the container with the following: -```sh -$ cd project -$ docker run --rm -it -p 5000:5000 \ - -v $(pwd):/data:ro \ - --security-opt apparmor:unconfined \ - --cap-add=SYS_ADMIN \ - 28mm/blast-radius --serve stacks/beef -``` + ```sh + $ cd project + $ docker run --cap-add=SYS_ADMIN -dit -p 5000:5000 -v :/data:ro /blast-radius:v1 + ``` + +--- ## Embedded Figures @@ -121,11 +148,14 @@ You will need the following: You can read more details in the [documentation](doc/embedded.md) +--- + ## Implementation Details *Blast Radius* uses the [Graphviz][] package to layout graph diagrams, -[PyHCL](https://github.com/virtuald/pyhcl) to parse [Terraform][] configuration, -and [d3.js](https://d3js.org/) to implement interactive features and animations. +[hcl] to parse [Terraform][] configuration, and [d3.js](https://d3js.org/) to implement interactive features and animations. + +--- ## Further Reading diff --git a/bin/blast-radius b/bin/blast-radius index eca3dd9..40072f4 100755 --- a/bin/blast-radius +++ b/bin/blast-radius @@ -17,6 +17,8 @@ from blastradius.handlers.apply import Apply from blastradius.handlers.terraform import Terraform from blastradius.server.server import app +from blastradius.server.server import simple_graph + def main(): parser = parser = argparse.ArgumentParser(description='blast-radius: Interactive Terraform Graph Visualizations') @@ -28,7 +30,8 @@ def main(): output_group.add_argument('--dot', action='store_const', const=True, default=False, help='print the graphviz/dot representation of the Terraform graph') output_group.add_argument('--svg', action='store_const', const=True, default=False, help='print the svg representation of the Terraform graph') output_group.add_argument('--serve', action='store_const', const=True, default=False, help='spins up a webserver with an interactive Terraform graph') - + output_group.add_argument('--svg-ext', action='store_const', const=True, default=False, help='download the simple svg of modified svg representation of the Terraform graph') + parser.add_argument('--graph', type=str, help='`terraform graph` output (defaults to stdin)', default=sys.stdin) # options to limit, re-focus, and re-center presentation of larger graphs. @@ -49,11 +52,11 @@ def main(): app.run(host='0.0.0.0',port=args.port) sys.exit(0) - elif args.json or args.dot or args.svg: + elif args.json or args.dot or args.svg or args.svg_ext: if args.graph is sys.stdin: dot = DotGraph('', file_contents=sys.stdin.read()) else: - dot = DotGraph(args.graph) + dot = DotGraph('',file_contents=simple_graph()) # we might not want to show every node in the depedency graph # specifying --module-depth is an easy way to limit detail @@ -76,18 +79,22 @@ def main(): parser.print_help() sys.exit(1) dot.focus(f_node) - + if args.json: tf = Terraform(args.directory) for node in dot.nodes: node.definition = tf.get_def(node) - - if args.json: - print(dot.json()) + f = open("visualization.json", "a") + f.write(dot.json()) + f.close() elif args.dot: print(dot.dot()) elif args.svg: print(dot.svg()) + elif args.svg_ext: + f = open("visualization.svg", "a") + f.write(dot.svg()) + f.close() else: parser.print_help() diff --git a/blastradius/handlers/apply.py b/blastradius/handlers/apply.py index 1390828..e7e5e97 100644 --- a/blastradius/handlers/apply.py +++ b/blastradius/handlers/apply.py @@ -1,55 +1,35 @@ -# standard libraries -import re +from __future__ import print_function import json - -# 1st party libraries -from blastradius.graph import Graph, Node, Edge -from blastradius.handlers.dot import DotNode -from blastradius.util import Re - -class Apply(Graph): - def __init__(self, filename): - self.filename = filename - self.contents = '' - self.nodes = [] # we can populate this, - self.edges = [] # but not this! - - ansi_escape = re.compile(r'\x1b[^m]*m') - with open(filename, 'r') as f: - self.contents = ansi_escape.sub('', f.read()) - - # example output: - # - # aws_vpc.default: Creation complete after 4s (ID: vpc-024f7a64) - # ... - # aws_key_pair.auth: Creating... - # fingerprint: "" => "" - # key_name: "" => "default-key" - # ... - # aws_instance.web: Still creating... (10s elapsed) - # aws_instance.web: Still creating... (20s elapsed) - # aws_instance.web (remote-exec): Connecting to remote host via SSH... - # aws_instance.web (remote-exec): Host: 1.2.3.4 - # aws_instance.web (remote-exec): User: ubuntu - # ... - - node_begin_re =r'(?P\S+)\:\s+Creating...' - node_compl_re = r'(?P\S+)\:\s+Creation\s+complete\s+after\s+(?P\S+)\s+' - node_still_re = r'(?P\S+)\:\s+Still\s+creating\.\.\.\s+\((?P\S+)\s+' - - for line in self.contents.splitlines(): - - r = Re() - if r.match(node_begin_re, line): - - - - - break - - - - - print(self.contents) - - +import sys +import jinja2 +import json +import subprocess +import os.path +from os import path + +class Apply(): + def __init__(self,filename=None): + self.apply_resource_info = [] + #reading from state file + if filename == None: + if(path.exists("terraform.tfstate")): + with open("terraform.tfstate", 'r') as f: + data = json.load(f) + else: + data = "" + else: + with open(filename, 'r') as f: + data = json.load(f) + + if (data) : + for _, var in enumerate(data["resources"]): + temp_data = dict() + temp_data = var + self.apply_resource_info.append(temp_data) + + else: + self.apply_resource_info.append("not applied") + + def json(self): + my_json_string = json.dumps(self.apply_resource_info,indent=4, sort_keys=True) + return my_json_string \ No newline at end of file diff --git a/blastradius/handlers/controls.py b/blastradius/handlers/controls.py new file mode 100644 index 0000000..2501894 --- /dev/null +++ b/blastradius/handlers/controls.py @@ -0,0 +1,30 @@ +import json +import os.path +from os import path + +class Controls(): + def __init__(self,filename=None): + self.resource_controls_info = [] + if filename == None: + if(path.exists("policy.json")): + with open("policy.json", 'r') as f: + data = json.load(f) + else: + data = "" + else: + with open(filename, 'r') as f: + data = json.load(f) + + if (data) : + for _, var in enumerate(data["resources"]): + temp_data = dict() + temp_data = var + self.resource_controls_info.append(temp_data) + + else: + self.resource_controls_info.append("not available") + + + def json(self): + controls_json = json.dumps(self.resource_controls_info,indent=4, sort_keys=True) + return controls_json \ No newline at end of file diff --git a/blastradius/handlers/cost.py b/blastradius/handlers/cost.py new file mode 100644 index 0000000..8199ca1 --- /dev/null +++ b/blastradius/handlers/cost.py @@ -0,0 +1,36 @@ +from __future__ import print_function + +from os import path +import json + + +class Cost(): + def __init__(self,filename=None): + self.resource_cost_info = [] + if filename == None: + if(path.exists("cost.json")): + with open("cost.json", 'r') as f: + data = json.load(f) + else: + data = "" + else: + with open(filename, 'r') as f: + data = json.load(f) + + if (data) : + temp_data = dict() + for (index, var) in data.items(): + temp_data[index] = var + if (index == "Lineitem"): + if var == None: + temp_data[index] = "not available" + + self.resource_cost_info.append(temp_data) + + else: + self.resource_cost_info.append("not available") + + + def json(self): + my_cost_json = json.dumps(self.resource_cost_info,indent=4, sort_keys=True) + return my_cost_json \ No newline at end of file diff --git a/blastradius/handlers/dot.py b/blastradius/handlers/dot.py index 1126c4b..4a364b6 100644 --- a/blastradius/handlers/dot.py +++ b/blastradius/handlers/dot.py @@ -11,6 +11,11 @@ # 1st party libraries from blastradius.graph import Graph, Node, Edge from blastradius.util import OrderedSet +from blastradius.handlers.plan import Plan +from blastradius.handlers.apply import Apply +from blastradius.handlers.cost import Cost +from blastradius.handlers.time import Time +from blastradius.handlers.controls import Controls class DotGraph(Graph): @@ -20,6 +25,14 @@ def __init__(self, filename, file_contents=None): self.edges = [] self.clusters = OrderedDict() self.clusters['root'] = True # Used like an ordered Set. + self.plan = Plan() + self.apply = Apply() + # self.flag = flag + self.cost = Cost() + self.time = Time() + self.controls = Controls() + self.totalcost = "" + self.totaltime = "" if file_contents: self.contents = file_contents @@ -44,16 +57,94 @@ def __init__(self, filename, file_contents=None): e = DotEdge(d['src'], d['dst'], fmt=fmt) self.edges.append(e) elif 'node' in m.groupdict(): - self.nodes.append(DotNode(d['node'], fmt=fmt)) - break + sp = d['node'].split(" ") + res = sp[1].replace("data.","") + type = sp[1].split(".")[0] + plan_data = "no plan available" + cost_data = "no cost available" + time_data = "no time estimation available" + controls_data = "no controls available" + apply_data = "not yet applied" + + if type == "data": + # prepare apply data + if ("not applied" in self.apply.apply_resource_info): + self.nodes.append(DotNode(d['node'], plan_data, apply_data,cost_data,controls_data,time_data,fmt=fmt )) + break + + else: + for i in self.apply.apply_resource_info: + if i['type']+"."+i['name'] == res: + apply_data = i + self.nodes.append(DotNode(d['node'], plan_data, apply_data,cost_data,controls_data,time_data,fmt=fmt )) + break + else: + if type == "provider" or type == "provisioner": + self.nodes.append(DotNode(d['node'], plan_data, apply_data,cost_data,controls_data,time_data,fmt=fmt )) + else: + # prepare plan data + for i in self.plan.resource_info: + data = i["address"].split("[") + if data[0] == res: + plan_data = i + break + + # prepare apply data + if ("not applied" not in self.apply.apply_resource_info): + for i in self.apply.apply_resource_info: + if i['type']+"."+i['name'] == res: + apply_data = i + break + + #prepare cost data + if ("not available" not in self.cost.resource_cost_info): + for i in range(len(self.cost.resource_cost_info)): + currency = self.cost.resource_cost_info[i]["currency"] + self.totalcost = str(self.cost.resource_cost_info[i]["totalcost"]) + " " +currency + if self.cost.resource_cost_info[i]["Lineitem"] == "not available": + cost_data = "no cost available" + else: + for _, val in enumerate(self.cost.resource_cost_info[i]["Lineitem"]): + data = val["terraformItemId"]+"."+val["id"] + if data == res: + val["lineitemtotal"] = str(val["lineitemtotal"]) + " "+ currency + cost_data = val + break + + #prepare time data + if ("not available" not in self.time.resource_time_info): + for i in range(len(self.time.resource_time_info)): + self.totaltime = str(self.time.resource_time_info[i]["totalTimeEstimation"]) + for _, val in enumerate(self.time.resource_time_info[i]["resources"]): + data = val["name"] + if data == res.split(".")[0]: + val["TimeEstimation"] = str(val["TimeEstimation"]) + time_data = val + break + + #prepare controls data + if ("not available" not in self.controls.resource_controls_info): + for i in self.controls.resource_controls_info: + data = i["type"]+"."+i["name"] + if data == res: + controls_data = i + break + + + self.nodes.append(DotNode(d['node'], plan_data, apply_data,cost_data,controls_data,time_data,fmt=fmt )) # terraform graph output doesn't always make explicit node declarations; # sometimes they're a side-effect of edge definitions. Capture them. for e in self.edges: + apply_data = "not yet applied" + plan_data = "no plan available" + controls_data = "no controls available" + cost_data = "no cost available" + time_data = "no time estimation available" if e.source not in [ n.label for n in self.nodes ]: - self.nodes.append(DotNode(e.source)) + self.nodes.append(DotNode(e.source, plan_data,apply_data,cost_data,controls_data,time_data)) if e.target not in [ n.label for n in self.nodes ]: - self.nodes.append(DotNode(e.target)) + self.nodes.append(DotNode(e.source, plan_data,apply_data,cost_data,controls_data,time_data)) self.stack('var') self.stack('output') @@ -80,12 +171,12 @@ def get_node_by_name(self, label): def dot(self): 'returns a dot/graphviz representation of the graph (a string)' - return self.dot_template.render({ 'nodes': self.nodes, 'edges': self.edges, 'clusters' : self.clusters, 'EdgeType' : EdgeType }) + return self.dot_template.render({ 'nodes': self.nodes, 'edges': self.edges, 'clusters' : self.clusters, 'EdgeType' : EdgeType,'totalcost': self.totalcost,'totaltime': self.totaltime }) def json(self): edges = [ dict(e) for e in self.edges ] nodes = [ dict(n) for n in self.nodes ] - return json.dumps({ 'nodes' : nodes, 'edges' : edges }, indent=4, sort_keys=True) + return json.dumps({ 'nodes' : nodes, 'edges' : edges,'totalcost' : self.totalcost,'totaltime': self.totaltime }, indent=4, sort_keys=True) # # A handful of graph manipulations. These are hampered by the decision @@ -311,12 +402,12 @@ def focus(self, node): # dot_template_str = """ -digraph { + digraph { compound = "true" - graph [fontname = "courier new",fontsize=8]; - node [fontname = "courier new",fontsize=8]; - edge [fontname = "courier new",fontsize=8]; - + graph [fontname="Arial Black",fontsize=12]; + node [fontname="Arial Black",fontsize=11]; + edge [fontname=Helvetica,fontsize=10]; + {# just the root module #} {% for cluster in clusters %} subgraph "{{cluster}}" { @@ -324,22 +415,79 @@ def focus(self, node): {% for node in nodes %} {% if node.cluster == cluster and node.module == 'root' %} {% if node.type %} - "{{node.label}}" [ shape=none, margin=0, id={{node.svg_id}} label=< + {% if node.type == 'var' or node.type == 'provider' or node.type == 'meta' or node.type == 'provider' or node.type == 'output'%} + "{{node.label}}" [ shape=none, margin=0, id={{node.svg_id}} label=<
-
{{node.type}}
{{node.resource_name}}
>]; + + >]; + {% else %} + "{{node.label}}" [ shape=none, margin=0, id={{node.svg_id}} label=< + {% if "ibm_cos" in node.type %} + + {% elif "ibm_kp" in node.type %} + + {% elif "ibm_container" in node.type %} + + {% elif "ibm_is" in node.type %} + + {% else %} + + {% endif %} + + + {% if node.controls == "no controls available" %} + + {% elif node.controls.decision == "pass" %} + + {% elif node.controls.decision == "failed" %} + + {% endif %} + {% if node.cost == "no cost available" %} + + {% else %} + + {% endif %} + {% if node.time == "no time estimation available" %} + + {% else %} + + {% endif %} + {% if node.apply == "not yet applied" %} + + {% elif not node.apply %} + + {% elif node.apply %} + {% if node.apply.instances[0] == null %} + + {% else %} + + {% endif %} + {% endif %} +
{{ "%-20s"|format(node.type) }}
{{ "%-20s"|format(node.type) }}
{{ "%-20s"|format(node.type) }}
{{ "%-20s"|format(node.type) }}
{{ "%-20s"|format(node.type) }}
{{ "%-20s"|format(node.resource_name) }}
{{ "%-30s"|format(">_terraform plan") }}
{{ "%-30s"|format(">_controls verify") }}
{{ "%-30s"|format(">_controls verify") }}
{{ "%-30s"|format(">_controls verify") }}
{{ "%-30s"|format(">_estimate cost") }}{{ "%-10s"|format("N/A") }}
{{ "%-30s"|format(">_estimate cost") }}{{ "%-10s"|format(node.cost.currlineitemtotal) }}
{{ "%-30s"|format(">_estimate time") }}
{{ "%-30s"|format(">_estimate time") }}{{ "%-10s"|format(node.time.TimeEstimation) }}
{{ "%-30s"|format(">_terraform apply") }}
{{ "%-30s"|format(">_terraform apply") }}
{{ "%-30s"|format(">_terraform apply") }}
{{ "%-30s"|format(">_terraform apply") }}
>]; + {% endif %} {% else %} + {% if totalcost != "" or totaltime != "" %} + "{{node.label}}" [ shape=none, margin=0, id={{node.svg_id}} label=< + + {% if totalcost != "" %} + + {% endif %} + {% if totaltime != "" %} + + {% endif %} +
{{node.label}}
{{totalcost}}
{{totaltime}}
>]; + {% else %} "{{node.label}}" [{{node.fmt}}] + {% endif %} {% endif %} {% endif %} {% endfor %} } {% endfor %} - {# non-root modules #} {% for node in nodes %} {% if node.module != 'root' %} - {% if node.collapsed %} "{{node.label}}" [ shape=none, margin=0, id={{node.svg_id}} label=< {% for module in node.modules %}{% endfor %} @@ -354,9 +502,7 @@ def focus(self, node):
(M) {{module}}
>]; {% endif %} {% endif %} - {% endfor %} - {% for edge in edges %} {% if edge.edge_type == EdgeType.NORMAL %}"{{edge.source}}" -> "{{edge.target}}" {% if edge.fmt %} [{{edge.fmt}}] {% endif %}{% endif %} {% if edge.edge_type == EdgeType.LAYOUT_SHOWN %}"{{edge.source}}" -> "{{edge.target}}" {% if edge.fmt %} [{{edge.fmt}}] {% endif %}{% endif %} @@ -364,6 +510,7 @@ def focus(self, node): {% endfor %} } """ + dot_template = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(dot_template_str) @@ -402,7 +549,7 @@ def __str__(self): class DotNode(Node): - def __init__(self, label, fmt=None): + def __init__(self, label,plan_data,apply_data,cost_data,controls_data,time_data, fmt=None): self.label = DotNode._label_fixup(label) self.fmt = fmt if fmt else Format('') # graphviz formatting. @@ -415,6 +562,11 @@ def __init__(self, label, fmt=None): self.module = DotNode._module(self.label) # for module groupings. 'root' or 'module.foo.module.bar' self.cluster = None # for stacked resources (usually var/output). self.collapsed = False + self.plan = plan_data + self.apply = apply_data + self.cost = cost_data + self.controls = controls_data + self.time = time_data self.fmt.add(id=self.svg_id, shape='box') @@ -422,7 +574,7 @@ def __init__(self, label, fmt=None): self.modules = [ m for m in self.module.split('.') if m != 'module' ] def __iter__(self): - for key in {'label', 'simple_name', 'type', 'resource_name', 'group', 'svg_id', 'definition', 'cluster', 'module', 'modules'}: + for key in {'label', 'simple_name', 'type', 'resource_name', 'group', 'svg_id', 'definition', 'cluster', 'module', 'modules','plan', 'apply','cost','controls','time'}: yield (key, getattr(self, key)) # diff --git a/blastradius/handlers/plan.py b/blastradius/handlers/plan.py index 15c0651..15bc6ad 100644 --- a/blastradius/handlers/plan.py +++ b/blastradius/handlers/plan.py @@ -1,54 +1,23 @@ -# standard libraries -import re +from __future__ import print_function +import json +import sys +import jinja2 import json -# 1st party libraries -from blastradius.graph import Graph, Node, Edge -from blastradius.handlers.dot import DotNode - -class Plan(Graph): - def __init__(self, filename): - self.filename = filename - self.contents = '' - self.nodes = [] # we can populate this, - self.edges = [] # but not this! - - ansi_escape = re.compile(r'\x1b[^m]*m') - with open(filename, 'r') as f: - self.contents = ansi_escape.sub('', f.read()) - - node_re = re.compile(r'\s+(?P(\+|\-))\s+(?P\S+)') - attr_re = re.compile(r'\s+(?P\S+)\:\s+(?P.*)') - - action = None - name = None - definition = {} - for line in self.contents.splitlines(): - for p in [ node_re, attr_re ]: - m = p.match(line) - if m: - d = m.groupdict() - if 'action' in d: - if action: - self.nodes.append(PlanNode(action, name, definition)) - action = d['action'] - name = d['name'] - definition = {} - elif 'key' in d: - definition[d['key']] = d['value'] - break - - print(json.dumps([ dict(n) for n in self.nodes], indent=4)) - -class PlanNode(Node): - def __init__(self, action, name, definition): - self.action = action - self.simple_name = name - self.definition = definition - self.type = DotNode._resource_type(self.simple_name) - self.resource_name = DotNode._resource_name(self.simple_name) - self.svg_id = 'node_' + str(Node.svg_id_counter()) - - def __iter__(self): - for key in ['action', 'simple_name', 'definition', 'type', 'resource_name', 'svg_id']: - yield (key, getattr(self, key)) +class Plan(): + def __init__(self,filename=None): + self.resource_info = [] + #create json reading from tfplan.json file + with open("tfplan.json", 'r') as f: + data = json.load(f) + for _, var in enumerate(data["planned_values"]["root_module"]["resources"]): + temp_data = dict() + temp_data = var + for _, variable in enumerate(data["resource_changes"]): + if temp_data["address"] == variable["address"]: + temp_data["change"] = variable["change"] + self.resource_info.append(temp_data) + + def json(self): + plan_json = json.dumps(self.resource_info,indent=4, sort_keys=True) + return plan_json \ No newline at end of file diff --git a/blastradius/handlers/terraform.py b/blastradius/handlers/terraform.py index 00b41d9..98ba252 100644 --- a/blastradius/handlers/terraform.py +++ b/blastradius/handlers/terraform.py @@ -5,7 +5,7 @@ import re # 3rd party libraries -import hcl # hashicorp configuration language (.tf) +import hcl2 as hcl # hashicorp configuration language (.tf) class Terraform: """Finds terraform/hcl files (*.tf) in CWD or a supplied directory, parses @@ -79,20 +79,12 @@ def get_def(self, node, module_depth=0): return '' try: - # non resource types - types = { 'var' : lambda x: self.config['variable'][x.resource_name], - 'provider' : lambda x: self.config['provider'][x.resource_name], - 'output' : lambda x: self.config['output'][x.resource_name], - 'data' : lambda x: self.config['data'][x.resource_name], - 'meta' : lambda x: '', - 'provisioner' : lambda x: '', - '' : lambda x: '' } - if node.type in types: - return types[node.type](node) + for n in self.config['resource']: + if node.type in n: + if node.resource_name in n[node.type]: + return n[node.type][node.resource_name] + + return '' - # resources are a little different _many_ possible types, - # nested within the 'resource' field. - else: - return self.config['resource'][node.type][node.resource_name] except: return '' diff --git a/blastradius/handlers/time.py b/blastradius/handlers/time.py new file mode 100644 index 0000000..bbc4a6c --- /dev/null +++ b/blastradius/handlers/time.py @@ -0,0 +1,33 @@ +from __future__ import print_function + +from os import path +import json + + +class Time(): + def __init__(self,filename=None): + self.resource_time_info = [] + if filename == None: + if(path.exists("time.json")): + with open("time.json", 'r') as f: + data = json.load(f) + else: + data = "" + else: + with open(filename, 'r') as f: + data = json.load(f) + + if (data) : + temp_data = dict() + for (index, var) in data.items(): + temp_data[index] = var + + self.resource_time_info.append(data) + + else: + self.resource_time_info.append("not available") + + + def json(self): + my_time_json = json.dumps(self.resource_time_info,indent=4, sort_keys=True) + return my_time_json \ No newline at end of file diff --git a/blastradius/server/server.py b/blastradius/server/server.py index aae9278..8560601 100644 --- a/blastradius/server/server.py +++ b/blastradius/server/server.py @@ -3,6 +3,7 @@ import subprocess import itertools import json +import re # 3rd-party libraries from flask import Flask @@ -33,7 +34,7 @@ def index(): @app.route('/graph.svg') def graph_svg(): Graph.reset_counters() - dot = DotGraph('', file_contents=run_tf_graph()) + dot = DotGraph('', file_contents=simple_graph()) module_depth = request.args.get('module_depth', default=None, type=int) refocus = request.args.get('refocus', default=None, type=str) @@ -52,7 +53,7 @@ def graph_svg(): @app.route('/graph.json') def graph_json(): Graph.reset_counters() - dot = DotGraph('', file_contents=run_tf_graph()) + dot = DotGraph('', file_contents=simple_graph()) module_depth = request.args.get('module_depth', default=None, type=int) refocus = request.args.get('refocus', default=None, type=str) if module_depth is not None and module_depth >= 0: @@ -90,6 +91,25 @@ def get_terraform_exe(): return which('terraform') +def simple_graph(): + file_contents=run_tf_graph() + new_file_content = '' + for line in file_contents.splitlines(): + if re.search("var",line) or re.search("provider",line) or re.search("meta.count-boundary",line) or re.search("output",line) : + if re.search("meta.count-boundary",line) and not (re.search("output",line) or re.search("var",line) or re.search('\[root\] root',line)): + new_line = line.replace("meta.count-boundary (EachMode fixup)","root") + new_file_content+=new_line +'\n' + if re.search("provider.template",line): + x = line.split('->') + if x[0].find("[root] provider.template (close)") != -1 : + new_line = line.replace("[root] provider.template (close)","[root] root") + new_file_content+=new_line +'\n' + else: + new_file_content+=line +'\n' + + return new_file_content + + diff --git a/blastradius/server/static/css/style.css b/blastradius/server/static/css/style.css index 63d8f56..cd9c2ce 100644 --- a/blastradius/server/static/css/style.css +++ b/blastradius/server/static/css/style.css @@ -3,12 +3,14 @@ h1, h2, h3, h4, p { } p.explain { - font-family: monospace; + font-family: Arial; + font-weight:400; white-space: pre; } h3.explain { - font-family: monospace; + font-family: Arial; + font-weight:400; } path.link { @@ -24,7 +26,7 @@ circle { text { fill: #000; - font: 10px sans-serif; + font: 40px sans-serif; pointer-events: none; } @@ -48,12 +50,13 @@ div.graph svg { } .diagmenu .dropdown-item { - font-family: 'courier new'; + font-family: 'Arial'; /*font-size: 14px;*/ } .dropdown-menu div label { - font-family: 'courier new'; + font-family: 'Arial'; + font-weight: bold; } /* tooltip stuff */ @@ -74,7 +77,7 @@ div.graph svg { .d3-tip p { font-weight: normal; line-height: 1.2; - font-family: 'courier new', 'monaco', fixed-width; + font-family: 'Arial', 'sans-serif', fixed-width; font-size: 11px; } @@ -94,7 +97,8 @@ span.title { span.sbox-listing { display: inline-block; font-size: 12px; - font-family: "Courier New"; + font-family: "Arial"; + font-weight: 900; line-height: 18px; color: black; margin: 1px; @@ -104,6 +108,7 @@ span.sbox-listing { span.dep { line-height: 15px; font-size: 12px; + font-weight: 900; color: white; display: inline-block; margin: 1px; @@ -132,6 +137,78 @@ span.dep { } g.node text { - font-family: 'consolas', 'monaco', 'fixed-width'; - font-size: 8px; + font-family: 'Arial', 'sans-serif', 'fixed-width'; + font-size: 8px; + font-weight: 900; +} + +.navbar-nav>li { + padding-left: 6px; +} + +.navbar-brand { + font-family: 'Arial'; + font-weight: 900; +} + +body { + font-family: "Arial", sans-serif; +} +.sidenav { +height: 100%; +width: 0; +position: fixed; +z-index: 1; +top: 0; +right: 0; +background-color: #D3D3D3; + +transition: 0.5s; +padding-top: 60px; +color: #000; +overflow: auto; +} + +.sidenav a { +padding: 8px 8px 8px 32px; +text-decoration: none; +font-size: 25px; +color: #818181; +display: block; +transition: 0.3s; + } + +.sidenav a:hover { +color: #f1f1f1; +} + +.square { +height: 10px; +width: 10px; + +} + +.sidenav .closebtn { + +position: absolute; +top: 60; +right: 20px; +font-size: 16px; +margin-right: 0px; +color: grey; +} + + +table, td { +/* /* position: absolute; */ +margin-top: 60; +margin-left: 20; +border: 1px solid black; +} + + +@media screen and (max-height: 450px) { +.sidenav {padding-top: 15px;} +.sidenav a {font-size: 18px;} +} \ No newline at end of file diff --git a/blastradius/server/static/images/.DS_Store b/blastradius/server/static/images/.DS_Store new file mode 100644 index 0000000..ad1a1ab Binary files /dev/null and b/blastradius/server/static/images/.DS_Store differ diff --git a/blastradius/server/static/images/COS_Icon.png b/blastradius/server/static/images/COS_Icon.png new file mode 100644 index 0000000..9d79eb4 Binary files /dev/null and b/blastradius/server/static/images/COS_Icon.png differ diff --git a/blastradius/server/static/images/apply.png b/blastradius/server/static/images/apply.png new file mode 100644 index 0000000..0f47e43 Binary files /dev/null and b/blastradius/server/static/images/apply.png differ diff --git a/blastradius/server/static/images/controls.png b/blastradius/server/static/images/controls.png new file mode 100644 index 0000000..4204905 Binary files /dev/null and b/blastradius/server/static/images/controls.png differ diff --git a/blastradius/server/static/images/correct.png b/blastradius/server/static/images/correct.png new file mode 100644 index 0000000..03f51ab Binary files /dev/null and b/blastradius/server/static/images/correct.png differ diff --git a/blastradius/server/static/images/cost.png b/blastradius/server/static/images/cost.png new file mode 100644 index 0000000..b1bb83b Binary files /dev/null and b/blastradius/server/static/images/cost.png differ diff --git a/blastradius/server/static/images/diagram.png b/blastradius/server/static/images/diagram.png new file mode 100644 index 0000000..39736aa Binary files /dev/null and b/blastradius/server/static/images/diagram.png differ diff --git a/blastradius/server/static/images/error.png b/blastradius/server/static/images/error.png new file mode 100644 index 0000000..55d5cd0 Binary files /dev/null and b/blastradius/server/static/images/error.png differ diff --git a/blastradius/server/static/images/hourglass.png b/blastradius/server/static/images/hourglass.png new file mode 100644 index 0000000..55f9af5 Binary files /dev/null and b/blastradius/server/static/images/hourglass.png differ diff --git a/blastradius/server/static/images/instance.png b/blastradius/server/static/images/instance.png new file mode 100644 index 0000000..37108a1 Binary files /dev/null and b/blastradius/server/static/images/instance.png differ diff --git a/blastradius/server/static/images/kp.png b/blastradius/server/static/images/kp.png new file mode 100644 index 0000000..ca40303 Binary files /dev/null and b/blastradius/server/static/images/kp.png differ diff --git a/blastradius/server/static/images/kubernetes.png b/blastradius/server/static/images/kubernetes.png new file mode 100644 index 0000000..e227007 Binary files /dev/null and b/blastradius/server/static/images/kubernetes.png differ diff --git a/blastradius/server/static/images/plan.png b/blastradius/server/static/images/plan.png new file mode 100644 index 0000000..efaeff6 Binary files /dev/null and b/blastradius/server/static/images/plan.png differ diff --git a/blastradius/server/static/images/policy.png b/blastradius/server/static/images/policy.png new file mode 100644 index 0000000..3ed79e1 Binary files /dev/null and b/blastradius/server/static/images/policy.png differ diff --git a/blastradius/server/static/images/resource.png b/blastradius/server/static/images/resource.png new file mode 100644 index 0000000..30ffb3d Binary files /dev/null and b/blastradius/server/static/images/resource.png differ diff --git a/blastradius/server/static/images/time.png b/blastradius/server/static/images/time.png new file mode 100644 index 0000000..780193e Binary files /dev/null and b/blastradius/server/static/images/time.png differ diff --git a/blastradius/server/static/images/vpc.png b/blastradius/server/static/images/vpc.png new file mode 100644 index 0000000..790117f Binary files /dev/null and b/blastradius/server/static/images/vpc.png differ diff --git a/blastradius/server/static/js/blast-radius.js b/blastradius/server/static/js/blast-radius.js index 0f9a466..dda3ce3 100644 --- a/blastradius/server/static/js/blast-radius.js +++ b/blastradius/server/static/js/blast-radius.js @@ -104,6 +104,8 @@ blastradius = function (selector, svg_url, json_url, br_state) { // be able to manipulate x.svg with d3.js, or other DOM fns. d3.xml(svg_url, function (error, xml) { + d3.select(selector).selectAll("svg").remove(); + container.node() .appendChild(document.importNode(xml.documentElement, true)); @@ -159,38 +161,112 @@ blastradius = function (selector, svg_url, json_url, br_state) { svg.attr('height', scale).attr('width', scale); } - var render_tooltip = function(d) { - var title_cbox = document.querySelector(selector + '-tooltip-title'); - var json_cbox = document.querySelector(selector + '-tooltip-json'); - var deps_cbox = document.querySelector(selector + '-tooltip-deps'); - if ((! title_cbox) || (! json_cbox) || (! deps_cbox)) - return title_html(d) + (d.definition.length == 0 ? '' : "

" + JSON.stringify(d.definition, replacer, 2) + "


" + child_html(d)); + var render_plan = function(d) { + var plan_title = "plan info" + var ttip = ''; + ttip += title_html(d); + if (d.plan == "no plan available"){ + ttip += '

' + plan_title + '

'+(d.plan.length == 0 ? '' : "

" + JSON.stringify(d.plan, replacer, 2) + "


"+ '
') ; + } else { + var yamlplan = json2yaml(d.plan) + ttip += '

' + plan_title + '

'+(d.plan.length == 0 ? '' : "

" + yamlplan + "


"+ '
') ; + } + ttip += child_html(d); + return ttip; + } + + var render_cost = function(d) { + var cost_title = "cost info" + var ttip = ''; + ttip += title_html(d); + if (d.cost == "no cost available"){ + ttip += '

' + cost_title + '

'+(d.cost.length == 0 ? '' : "

" + JSON.stringify(d.cost, replacer, 2) + "


"+ '
') ; + } + else{ + var yamlcost = json2yaml(d.cost) + ttip += '

' + cost_title + '

'+(d.cost.length == 0 ? '' : "

" + yamlcost + "


"+ '
') ; + } + ttip += child_html(d); + return ttip; + } + + var render_policy = function(d) { + var controls_title = "controls info" + var ttip = ''; + ttip += title_html(d); + if (d.controls == "no controls available" ) + { + ttip += '

' + controls_title + '

'+(d.controls.length == 0 ? '' : "

" + JSON.stringify(d.controls, replacer, 2) + "


"+ '
'); + } + else{ + var yamlcontrols = json2yaml(d.controls) + ttip += '

' + controls_title + '

'+(d.controls.length == 0 ? '' : "

" + yamlcontrols + "


"+ '
'); + } + ttip += child_html(d); + return ttip; + } + + var render_time = function(d) { + var time_title = "time info" + var ttip = ''; + ttip += title_html(d); + if (d.time == "no time estimation available" ) + { + ttip += '

' + time_title + '

'+(d.time.length == 0 ? '' : "

" + JSON.stringify(d.time, replacer, 2) + "


"+ '
') ; + + } else{ + var yamltime = json2yaml(d.time) + ttip += '

' + time_title + '

'+(d.time.length == 0 ? '' : "

" + yamltime + "


"+ '
') ; + } + ttip += child_html(d); + return ttip; + } + + var render_apply = function(d) { + var apply_title = "apply info" + var ttip = ''; + ttip += title_html(d); + if (d.apply == "not yet applied" ) + { + ttip += '

' + apply_title + '

'+("

" + JSON.stringify(d.apply, replacer, 2) + "


"+ '
'); + } + else { + if( d.apply == null || d.apply.instances[0] == null) + { + ttip += '

' + apply_title + '

'+("

" + "resource apply failed" + "


"+ '
'); + } + else + { + var yamlapply = json2yaml(d.apply) + ttip += '

' + apply_title + '

'+(d.apply.length == 0 ? '' : "

" + yamlapply + "


"+ '
'); + } + } + ttip += child_html(d); + return ttip; + } + var render_tfstate = function(d) { + var title = "config info" + var yamlstate = json2yaml(d.definition) var ttip = ''; - if (title_cbox.checked) - ttip += title_html(d); - if (json_cbox.checked) - ttip += (d.definition.length == 0 ? '' : "

" + JSON.stringify(d.definition, replacer, 2) + "


"); - if (deps_cbox.checked) - ttip += child_html(d); + ttip += title_html(d); + ttip += '

' + title + '

' +(d.definition.length == 0 ? '' : "

" + yamlstate + "


"+ '
'); + ttip += child_html(d); return ttip; } - // setup tooltips - var tip = d3.tip() - .attr('class', class_selector.slice(1, class_selector.length) + '-d3-tip d3-tip') - .offset([-10, 0]) - .html(render_tooltip); - svg.call(tip); - // returns
element representinga node's title and module namespace. + // returns
element representinga node's title and module namespace var title_html = function(d) { var node = d; var title = [ '
'] + var head = "resource name" + title[title.length] = '' + head + '

'; if (node.modules.length <= 1 && node.modules[0] == 'root') { title[title.length] = '' + node.type + ''; title[title.length] = '' + node.resource_name + ''; + } else { for (var i in node.modules) { @@ -203,6 +279,7 @@ blastradius = function (selector, svg_url, json_url, br_state) { return title.join(''); } + // returns
element representing node's title and module namespace. // intended for use in an interactive searchbox. var searchbox_listing = function(d) { @@ -347,70 +424,220 @@ blastradius = function (selector, svg_url, json_url, br_state) { // FIXME: but don't seem to be necessary for display var node_mousedown = function(d, x, y, z, no_tip_p) { if (sticky_node == d && click_count == 1) { - tip.hide(d); highlight(d, true, true); click_count += 1; } else if (sticky_node == d && click_count == 2) { unhighlight(d); - tip.hide(d); sticky_node = null; click_count = 0; } else { if (sticky_node) { unhighlight(sticky_node); - tip.hide(sticky_node); } sticky_node = d; click_count = 1; highlight(d, true, false); - if (no_tip_p === undefined) { - tip.show(d) - .direction(tipdir(d)) - .offset(tipoff(d)); - } } } - var node_mouseleave = function(d) { - tip.hide(d); + var plan_click = function(d) { + openNav() + var renderInfo = render_plan(d); + $('div.test').html(renderInfo); + + } + + var tfstate_click = function(d) { + openNav() + var renderInfo = render_tfstate(d); + $('div.test').html(renderInfo); + + } + + var apply_click = function(d) { + openNav() + var renderInfo = render_apply(d); + $('div.test').html(renderInfo); + + } + + var cost_click = function(d) { + openNav() + var renderInfo = render_cost(d); + $('div.test').html(renderInfo); + } + + var time_click = function(d) { + openNav() + var renderInfo = render_time(d); + $('div.test').html(renderInfo); + } + + var policy_click = function(d) { + openNav() + + var renderInfo = render_policy(d); + $('div.test').html(renderInfo); + + } + + function openNav() { + document.getElementById("mySidenav").style.width = "350px"; } - var node_mouseenter = function(d) { - tip.show(d) - .direction(tipdir(d)) - .offset(tipoff(d)); - } - var node_mouseover = function(d) { - if (! sticky_node) - highlight(d, true, false); - } + node = svg.selectAll('g.node') + .data(svg_nodes, function (d) { + return (d && d.svg_id) || d3.select(this).attr("id"); + }) + + node.select('polygon:nth-of-type(2)') + .on('click', node_mousedown) + .style('fill', (function (d) { + if (d){ + if (d.label == "[root] root") { + return "#fff"; + } + else { + return "#AEC7E8"; + } + } + else + return '#000'; + })); + + node.select('polygon:nth-of-type(3)') + .on('click',(function (d) { + if (d.label == "[root] root") { + return ""; + } + else { + return tfstate_click(d); + } + })) + .style('fill', (function (d) { + if (d) + return '#fff'; + else + return '#000'; + })); + + + node.select('polygon:nth-of-type(8)') + .on('click',plan_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } + else + return '#000'; + })); - var node_mouseout = function(d) { - if (sticky_node == d) { - return; - } - else if (! sticky_node) { - unhighlight(d); - } - else { - if (click_count == 2) - highlight(sticky_node, true, true); + + + node.select('polygon:nth-of-type(9)') + .on('click',plan_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } else - highlight(sticky_node, true, false); - } + return '#000'; + })); - } + node.select('polygon:nth-of-type(11)') + .on('click',policy_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } + else + return '#000'; + })); - var tipdir = function(d) { - return 'n'; - } + node.select('polygon:nth-of-type(12)') + .on('click',policy_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } + else + return '#000'; + })); - var tipoff = function(d) { - return [-10, 0]; - } + node.select('polygon:nth-of-type(14)') + .on('click',cost_click) + .style('fill', (function (d) { + if (d) { + return '#fff'; + } + else + return '#000'; + })); + + node.select('polygon:nth-of-type(15)') + .on('click',cost_click) + .style('fill', (function (d) { + if (d) { + return '#fff'; + } + else + return '#000'; + })); + + + node.select('polygon:nth-of-type(17)') + .on('click',time_click) + .style('fill', (function (d) { + if (d) { + return '#fff'; + } + else + return '#000'; + })); + + node.select('polygon:nth-of-type(18)') + .on('click',time_click) + .style('fill', (function (d) { + if (d) { + return '#fff'; + } + else + return '#000'; + })); + + + + + node.select('polygon:nth-of-type(20)') + .on('click',apply_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } + else + return '#000'; + })); + + node.select('polygon:nth-of-type(21)') + .on('click',apply_click) + .style('fill', (function (d) { + if (d){ + return '#fff'; + + } + else + return '#000'; + })); + + var highlight = function (d, downstream, upstream) { @@ -448,24 +675,6 @@ blastradius = function (selector, svg_url, json_url, br_state) { } - // colorize nodes, and add mouse candy. - svg.selectAll('g.node') - .data(svg_nodes, function (d) { - return (d && d.svg_id) || d3.select(this).attr("id"); - }) - .on('mouseenter', node_mouseenter) - .on('mouseleave', node_mouseleave) - .on('mouseover', node_mouseover) - .on('mouseout', node_mouseout) - .on('mousedown', node_mousedown) - .attr('fill', function (d) { return color(d.group); }) - .select('polygon:nth-last-of-type(2)') - .style('fill', (function (d) { - if (d) - return color(d.group); - else - return '#000'; - })); // colorize modules svg.selectAll('polygon') @@ -486,9 +695,6 @@ blastradius = function (selector, svg_url, json_url, br_state) { .data(svg_nodes, function (d) { return (d && d.svg_id) || d3.select(this).attr("id"); }) - .on('mouseover', node_mouseover) - .on('mouseout', node_mouseout) - .on('mousedown', node_mousedown) .select('polygon') .attr('fill', function (d) { return color(d.group); }) .style('fill', (function (d) { @@ -571,9 +777,6 @@ blastradius = function (selector, svg_url, json_url, br_state) { refocus_btn.removeEventListener('click', handle_refocus); download_btn.removeEventListener('click', handle_download); panzoom = null; - - // - tip.hide(); } var render_searchbox_node = function(d) { @@ -608,7 +811,7 @@ blastradius = function (selector, svg_url, json_url, br_state) { // because of scoping, we need to change the onChange callback to the new version // of select_node(), and delete the old callback associations. $(selector + '-search').selectize()[0].selectize.settings.onChange = select_node; - $(selector + '-search').selectize()[0].selectize.swapOnChange(); + // $(selector + '-search').selectize()[0].selectize.swapOnChange(); } else { $(selector + '-search').selectize({ diff --git a/blastradius/server/static/js/json2yaml.js b/blastradius/server/static/js/json2yaml.js new file mode 100644 index 0000000..6c5e320 --- /dev/null +++ b/blastradius/server/static/js/json2yaml.js @@ -0,0 +1,101 @@ +(function (self) { + /* + * TODO, lots of concatenation (slow in js) + */ + var spacing = " "; + + function getType(obj) { + var type = typeof obj; + if (obj instanceof Array) { + return 'array'; + } else if (type == 'string') { + return 'string'; + } else if (type == 'boolean') { + return 'boolean'; + } else if (type == 'number') { + return 'number'; + } else if (type == 'undefined' || obj === null) { + return 'null'; + } else { + return 'hash'; + } + } + + function convert(obj, ret) { + var type = getType(obj); + + switch(type) { + case 'array': + convertArray(obj, ret); + break; + case 'hash': + convertHash(obj, ret); + break; + case 'string': + convertString(obj, ret); + break; + case 'null': + ret.push('null'); + break; + case 'number': + ret.push(obj.toString()); + break; + case 'boolean': + ret.push(obj ? 'true' : 'false'); + break; + } + } + + function convertArray(obj, ret) { + for (var i=0; i + + @@ -130,6 +132,31 @@
+ + @@ -139,6 +166,10 @@ pan/zoom interactions and to avoid clipping -->
+
+ +
+
@@ -146,7 +177,19 @@ // br_state['#graph'] maintains state for this instance of blastradius var br_state = { '#graph' : {}} - blastradius('#graph', '/graph.svg', '/graph.json', br_state); + var id1 = document.getElementById("graph-radio-extended"); + + (function () { + // your page initialization code here + // the DOM will be available here + if (id1.checked) { + blastradius('#graph', '/graph.svg', '/graph.json', br_state); + } + })(); + + function closeNav() { + document.getElementById("mySidenav").style.width = "0"; + } diff --git a/doc/blast-radius-demo.svg b/doc/blast-radius-demo.svg index 866e001..b57bb7c 100644 --- a/doc/blast-radius-demo.svg +++ b/doc/blast-radius-demo.svg @@ -1,306 +1,752 @@ - - + + - - - + - -aws_elb - -web + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +template_cloudinit_config + + + + +cloud-init-apptier + + + + + +provider + + +template + + + + + + + + + + + +ibm_is_floating_ip + + ++ + + +floatingip1 + + + + + +ibm_is_instance + + ++ + + +instance1 + + + + + + + + + + + +ibm_is_floating_ip + + ++ + + +floatingip2 + + + + + +ibm_is_instance + + ++ + + +instance2 + + + + + + - - + + - -aws_instance - -web + + - - + + - - + +ibm_is_ssh_key + + ++ + + +ssh1 + + + + + + + + + + + +ibm_is_subnet + + ++ + + +subnet1 + + + + + + + + + + + +var + + +image + + + + + + + + + + + +var + + +profile + + + + + + + + + + + + - - + + - -aws_security_group - -elb + + + + + + + +ibm_is_subnet + + ++ + + +subnet2 - - + + - - + + - - + + - -aws_key_pair - -auth + + + + + + + + - - + + - - + +ibm_is_lb + + ++ + + +lb1 - - + + - -aws_security_group - -default + + - - + + - - + + - - + + - -aws_subnet - -default + +ibm_is_lb_listener + + ++ + + +lb1-listener - - + + - - + +ibm_is_lb_pool + + ++ + + +lb1-pool - - + + - -var - -aws_amis + + - - + + - - + + - - + + - -aws_internet_gateway - -default + +ibm_is_lb_pool_member + + ++ + + +lb1-pool-member1 - - + + - -aws_vpc - -default + + - - + + - - + + - - + + - -provider - -aws + +ibm_is_lb_pool_member + + ++ + + +lb1-pool-member2 + + + + + + + + + + + + - - + + - - + +ibm_is_security_group_rule + + ++ + + +sg1_tcp_rule_22 + + + + + + + + + + + + + + + + + +ibm_is_security_group_rule + + ++ + + +sg1_tcp_rule_80 - - + + - -var - -key_name + + - - + + - - + + - - + + - -var - -public_key_path + +provider + + +ibm - - + + - - + + - - + + - -aws_route - -internet_access + +var + + +ssh_public_key - - + + - - + + - - + + - - + +ibm_is_vpc_address_prefix + + ++ + + +vpc-ap1 - - + + - - + + - - + + - - + +ibm_is_vpc_address_prefix + + ++ + + +vpc-ap2 - - + + - - + + - - + + - -var - -aws_region + +ibm_is_vpc + + ++ + + +vpc1 - - + + - - + + - - + + - -meta - -count-boundary + +var + + +vpc_name - - + + - - + + - - + + - -output - -address + + - - + + - - + +var + + +zone1 - - + + - - + + - - + + - -provider - -aws + + - - + + - - + +var + + +zone2 - - + + - - + + - - + + - -provisioner - -remote-exec + +var + + +ibmcloud_region - - + + - - + + + + + + + +meta + + +count-boundary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +output + + +FloatingIP-1 + + + + + + + + + + + +output + + +FloatingIP-2 + + + + + + + + + + + +output + + +LB-Hostname + + + + + + + + + + + +provider + + +ibm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +provider + + +template + + + + + + - + + + +[root] root + + + + + + + + + + + + + + + + + + + + + + + +var + + +zone1_cidr + + + + + + + + + + +var + + +zone2_cidr + + + - -[root] root + - - + + - - + + - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/doc/blast-radius-ext.png b/doc/blast-radius-ext.png new file mode 100644 index 0000000..5ad91eb Binary files /dev/null and b/doc/blast-radius-ext.png differ diff --git a/doc/blastradiusext.png b/doc/blastradiusext.png new file mode 100644 index 0000000..9d48330 Binary files /dev/null and b/doc/blastradiusext.png differ diff --git a/doc/blastradiusextdemo.png b/doc/blastradiusextdemo.png new file mode 100644 index 0000000..9f13be8 Binary files /dev/null and b/doc/blastradiusextdemo.png differ diff --git a/doc/blastradiusextinfo.png b/doc/blastradiusextinfo.png new file mode 100644 index 0000000..162e65a Binary files /dev/null and b/doc/blastradiusextinfo.png differ diff --git a/doc/embedded.md b/doc/embedded.md index c66c462..04744ae 100644 --- a/doc/embedded.md +++ b/doc/embedded.md @@ -5,26 +5,31 @@ You may wish to embed figures produced with *Blast Radius* in other documents. Y 1. an `svg` file and `json` document representing the graph and its layout. These are produced with *Blast Radius*, as follows ````bash -[...]$ terraform graph | blast-radius --svg > graph.svg -[...]$ terraform graph | blast-radius --json > graph.json +[...]$ blast-radius --graph graph --newsvg [path of terraform directory] +[...]$ blast-radius --graph graph --json [path of terraform directory] + + ```` +here graph is the file containing the output of terraform graph 2. `javascript` and `css`. You can find these in the `.../blastradius/server/static` directory. Copy these files to an appropriate location, and ammend the following includes to reflect those locations. + for blastradius extended: + ````html - + ```` 3. A uniquely identified DOM element, where the `` should appear, and a call to `blastradius(...)` somewhere after (usually at the end of the `` document. - ````html +````html
```` diff --git a/requirements.txt b/requirements.txt index f322210..fd538b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,7 @@ Jinja2==2.10.1 Flask==1.0.3 beautifulsoup4==4.7.1 ply>=3.11 -pyhcl==0.3.12 +pyhcl==0.4.4 +python-hcl2==2.0.0 +graphviz==0.17 + diff --git a/setup.py b/setup.py index 04e53e9..ea97791 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='blastradius', - version='0.1.25', + version='0.1.25.1', description='Interactive Terraform graph visualizations', long_description=open('README.md').read(), long_description_content_type='text/markdown', @@ -22,4 +22,8 @@ packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), scripts=['bin/blast-radius'], install_requires=reqs, -) + data_files=[('blastradius/server/templates', ['blastradius/server/templates/error.html', 'blastradius/server/templates/index.html']), + ('blastradius/server/static/images',['blastradius/server/static/images/controls.png','blastradius/server/static/images/apply.png','blastradius/server/static/images/correct.png','blastradius/server/static/images/instance.png','blastradius/server/static/images/plan.png','blastradius/server/static/images/vpc.png','blastradius/server/static/images/instance.png','blastradius/server/static/images/cost.png','blastradius/server/static/images/time.png','blastradius/server/static/images/policy.png','blastradius/server/static/images/error.png','blastradius/server/static/images/hourglass.png','blastradius/server/static/images/kp.png','blastradius/server/static/images/COS_Icon.png','blastradius/server/static/images/kubernetes.png','blastradius/server/static/images/resource.png']), + ('blastradius/server/static/css', ['blastradius/server/static/css/bootstrap.min.css', 'blastradius/server/static/css/selectize.css','blastradius/server/static/css/style.css']), + ('blastradius/server/static/js', ['blastradius/server/static/js/blast-radius.js', 'blastradius/server/static/js/bootstrap.min.js','blastradius/server/static/js/categories.js','blastradius/server/static/js/d3-tip.js','blastradius/server/static/js/d3.v4.js','blastradius/server/static/js/d3.v4.min.js','blastradius/server/static/js/fontawesome-all.min.js','blastradius/server/static/js/jquery.slim.min.js','blastradius/server/static/js/selectize.js','blastradius/server/static/js/svg-pan-zoom.js','blastradius/server/static/js/json2yaml.js']), ], +) \ No newline at end of file