diff --git a/.dockerignore b/.dockerignore index a4886ac..e88192d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,17 @@ .* +AWS/ doc +Dockerfile +*.md +!README.md +*.svg +*.json +*.png +docker-compose.yml providers/ LICENSE +examples/ +example/ +PowerShell/ +BASH/ +Kubernetes/ \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..1a72685 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '41 11 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e5886c6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '38 19 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml new file mode 100644 index 0000000..806d41d --- /dev/null +++ b/.github/workflows/codesee-arch-diagram.yml @@ -0,0 +1,23 @@ +# This workflow was added by CodeSee. Learn more at https://codesee.io/ +# This is v2.0 of this workflow file +on: + push: + branches: + - main + pull_request_target: + types: [opened, synchronize, reopened] + +name: CodeSee + +permissions: read-all + +jobs: + codesee: + runs-on: ubuntu-latest + continue-on-error: true + name: Analyze the repo with CodeSee + steps: + - uses: Codesee-io/codesee-action@v2 + with: + codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} + codesee-url: https://app.codesee.io diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..fe461b4 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v2 diff --git a/.gitignore b/.gitignore index f1b0d54..13e406e 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,17 @@ venv.bak/ .spyderproject .spyproject +#IntelliJ IDEA project settings +.ideas +.idea + +#VS Code project settings +.vscode +.vscode/ + +# Embold +.embold/ + # Rope project settings .ropeproject @@ -102,3 +113,10 @@ venv.bak/ # mypy .mypy_cache/ + +# Terraform configuration +.terraform/ +*.tfstate + +# Mac +.DS_Store \ No newline at end of file diff --git a/AWS/blast-radius-aws.tf b/AWS/blast-radius-aws.tf new file mode 100644 index 0000000..632b217 --- /dev/null +++ b/AWS/blast-radius-aws.tf @@ -0,0 +1,189 @@ +terraform { +} + +provider "aws" { + # uncomment if global configuration is not set up yet +# access_key = var.AWS_ACCESS_KEY_ID +# secret_key = var.AWS_SECRET_ACCESS_KEY + region = var.AWS_REGION +} + +#variable "AWS_ACCESS_KEY_ID" { +# default = "" +#} +# +#variable "AWS_SECRET_ACCESS_KEY" { +# default = "" +#} +# +variable "AWS_REGION" { + default = "" +} + +variable "AMI_ID" { + default = "09d56f8956ab235b3" #UBUNTU +} + +variable "KEY_NAME" { + default = "" +} + +variable "KEY_PATH" { + default = "" +} + +variable "ACCESS_PORT" { + default = 8888 +} + +resource "aws_vpc" "terraform-vpc" { + cidr_block = "10.10.0.0/16" + + tags = { + Name = "blastradius" + } +} + +resource "aws_subnet" "first-subnet" { + cidr_block = "10.10.1.0/24" + vpc_id = aws_vpc.terraform-vpc.id + availability_zone = "${var.AWS_REGION}a" +} + +resource "aws_route_table" "route-table" { + vpc_id = aws_vpc.terraform-vpc.id + tags = { + Name = "route-table" + } +} + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.terraform-vpc.id + + tags = { + Name = "internet-gateway" + } +} + +resource "aws_route" "blast-radius-route" { + route_table_id = aws_route_table.route-table.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + depends_on = [ + aws_route_table.route-table, + aws_internet_gateway.igw + ] +} + +resource "aws_main_route_table_association" "main-rt" { + vpc_id = aws_vpc.terraform-vpc.id + route_table_id = aws_route_table.route-table.id +} + +locals { + rulesmap = { + "HTTP" = { + port = 80, + cidr_blocks = ["0.0.0.0/0"], + ipv6_cidr_blocks = ["::/0"] + }, + "HTTPS" = { + port = 443, + cidr_blocks = ["0.0.0.0/0"], + ipv6_cidr_blocks = ["::/0"] + } + "SSH" = { + port = 22, + cidr_blocks = ["0.0.0.0/0"], + ipv6_cidr_blocks = ["::/0"] + }, + "BLASTR" = { + port = var.ACCESS_PORT, + cidr_blocks = ["0.0.0.0/0"], + ipv6_cidr_blocks = ["::/0"] + } + } +} + +resource "aws_security_group" "sg" { + vpc_id = aws_subnet.first-subnet.vpc_id + + dynamic "ingress" { + for_each = local.rulesmap + content { + description = ingress.key # HTTP or SSH + from_port = ingress.value.port + to_port = ingress.value.port + protocol = "tcp" + cidr_blocks = ingress.value.cidr_blocks + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + egress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + ipv6_cidr_blocks = ["::/0"] + } + + tags = { + Name = "default" + } +} + +resource "aws_instance" "blast-radius-ec2-instance" { + associate_public_ip_address = true + # ami = "ami-02584c1c9d05efa69" // Ubuntu 20.04LTS - not using data.aws_ami.amazon_linux.id + ami = "ami-${var.AMI_ID}" + instance_type = "t2.micro" + key_name = "${var.KEY_NAME}" + vpc_security_group_ids = [aws_security_group.sg.id] + subnet_id = aws_subnet.first-subnet.id + + connection { + agent = false + host = self.public_ip + private_key = file(var.KEY_PATH) + type = "ssh" + user = "ubuntu" + } + + provisioner "remote-exec" { + inline = [ + "sudo apt-get update -y", + "sudo apt-get install docker.io docker -y", + "sudo chmod 666 /var/run/docker.sock", + "sudo service docker start", + "docker run --rm -it -d -p ${var.ACCESS_PORT}:5000 -v $(pwd):/data:ro --security-opt apparmor:unconfined --cap-add=SYS_ADMIN ianyliu/blast-radius-fork" + ] + } + + tags = { + Terraform = "true" + Environment = "dev" + Name = "blast-radius" + } +} + +output "ec1-public-ip" { + value = aws_instance.blast-radius-ec2-instance.public_ip +} + +output "port" { + value = var.ACCESS_PORT +} diff --git a/BASH/docker_build.sh b/BASH/docker_build.sh new file mode 100755 index 0000000..9716271 --- /dev/null +++ b/BASH/docker_build.sh @@ -0,0 +1,29 @@ +#! /bin/bash +IMAGE_NAME="blast-radius-fork-local" +MULTI_CPU=false +SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") # get the directory of this script +DOCKERFILE_DIR=$(dirname -- "$(readlink -f -- "$SCRIPT_DIR")") # get the parent directory of this script (the directory of the Dockerfile) + +if [ "$1" != "" ]; then + IMAGE_NAME=$1 +fi + +if [ "$2" == true ]; then + MULTI_CPU=true +fi + +if [ ! -e "$DOCKERFILE_DIR/Dockerfile" ]; then + echo "File $DOCKERFILE_DIR/Dockerfile does not exist, so image $IMAGE_NAME could not be built. Exiting" + exit 1 +fi + +if [ "$MULTI_CPU" == false ]; then + echo "Building image $IMAGE_NAME without multi-cpu support. Your image will be saved locally." + docker build -t "$IMAGE_NAME" "$DOCKERFILE_DIR" +else + echo "Building image $IMAGE_NAME with multi-cpu support. Your image will be pushed remotely to Docker Hub and saved locally afterwards. " + docker buildx build \ + --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6 \ + -t "$IMAGE_NAME" --push "$DOCKERFILE_DIR" + docker pull "$IMAGE_NAME" +fi \ No newline at end of file diff --git a/BASH/docker_run.sh b/BASH/docker_run.sh new file mode 100644 index 0000000..e28bc4c --- /dev/null +++ b/BASH/docker_run.sh @@ -0,0 +1,59 @@ +#! /bin/bash + +SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") # get the directory of this script +BUILDFILE=$SCRIPT_DIR/docker_build.sh # get the path to the docker build script + +IMAGE_NAME="blast-radius-fork-local" +ACCESS_PORT=5000 + +# first check if number of arguments to script is greater than 3 or not, if it is exit +if [ $# -gt 3 ]; then + echo "$0 does not accept more than 2 arguments (image name & port). You have provided $# arguments." + exit 1 +fi + +# if number of arguments is equal to 0 +if [ $# -eq 0 ]; then + echo "Using default image name: ${IMAGE_NAME} and default port: ${ACCESS_PORT} because no arguments were passed" +else + if [ "$1" != "" ]; then + IMAGE_NAME=$1 + fi + if [ "$2" != "" ]; then + ACCESS_PORT=$2 + fi +fi + +# check if image exists, if not try to build it +if [[ "$(docker image inspect "$IMAGE_NAME" --format='exists')" != 'exists' ]]; then + echo "$IMAGE_NAME does not exist. Trying to build the image using $BUILDFILE ..." + + if [ ! -e "$BUILDFILE" ]; then + echo "File $BUILDFILE does not exist. Exiting" + exit 1 + fi + + if [ ! -s "$BUILDFILE" ]; then + echo "File $BUILDFILE is empty. Exiting" + exit 1 + fi + + if [ ! -x "$BUILDFILE" ]; then + echo "File $BUILDFILE is not executable. Exiting" + echo "Hint: Try running 'chmod +x $BUILDFILE'" + exit 1 + fi + + echo "Using $BUILDFILE to build image $IMAGE_NAME" + $BUILDFILE "$IMAGE_NAME" + +fi + +if [[ "$(docker image inspect "$IMAGE_NAME" --format='exists')" == 'exists' ]]; then + echo "Attempting to run Docker Image: $IMAGE_NAME on $ACCESS_PORT" + docker run --rm -it -d -p "$ACCESS_PORT":5000 \ + -v "$(PWD)":/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + "$IMAGE_NAME" +fi \ No newline at end of file diff --git a/Docker.md b/Docker.md new file mode 100644 index 0000000..aab7031 --- /dev/null +++ b/Docker.md @@ -0,0 +1,314 @@ +

Docker

+ +[privileges]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities +[overlayfs]: https://wiki.archlinux.org/index.php/Overlay_filesystem + +## Table of Contents +- [Table of Contents](#table-of-contents) +- [Prerequisites](#prerequisites) +- [Run Docker Containers with Docker Hub Images](#run-docker-containers-with-docker-hub-images) +- [Docker configurations](#docker-configurations) +- [Port configurations](#port-configurations) +- [Docker \& Subdirectories](#docker--subdirectories) +- [Image Building](#image-building) + - [Prerequisites for Buildx](#prerequisites-for-buildx) +- [Shell Scripts](#shell-scripts) +- [Aliases](#aliases) + - [Temporary Aliases](#temporary-aliases) + - [Permanent Aliases](#permanent-aliases) + + +## Prerequisites +* Install Docker + * [Linux](https://docs.docker.com/desktop/install/linux-install/) + * [Mac](https://docs.docker.com/desktop/install/mac-install/) + * [Windows](https://docs.docker.com/desktop/install/windows-install/) + +It is recommended to nstall [Docker Desktop](https://www.docker.com/products/docker-desktop/) as well, a more intuitive GUI for Docker. + +Verify Docker is installed in your Terminal: ```docker info``` + +## Run Docker Containers with Docker Hub Images + +Launch *Blast Radius* for a local directory by manually running: + +> sh, zsh, bash +```sh +docker run --rm -it -p 5000:5000 \ + -v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + ianyliu/blast-radius-fork +``` + +Note: If you have spaces in your directory then you may have to change `-v ${pwd}:/data:ro` to `-v "${pwd}:/data:ro"` instead. + +> Windows PowerShell +```powershell +docker run --rm -it -p 5000:5000 ` + -v ${pwd}:/data:ro ` + --security-opt apparmor:unconfined ` + --cap-add=SYS_ADMIN ` + ianyliu/blast-radius-fork +``` + +If you do not have the Docker image, it will be automatically pulled for you. You can also build the image yourself +see ([Image Building](#image-building)). + +A slightly more customized variant of this is also available as an example +[docker-compose.yml](./Docker/docker-compose.yml) use case for Workspaces. + +## 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. + +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 +independently. To do this, certain runtime privileges are required -- +specifically `--cap-add=SYS_ADMIN`. + +> Note: This is considered a security risk by some, so be sure you understand how this works. + +For more information on how this works and what it means for your host, check +out the [runtime privileges][privileges] documentation. +
+ +## Port configurations + +
+To run the Docker image on a different port, you can modify the Docker command so that PORTNUMBER +maps to the desired port number. + +```sh +docker run --rm -it -p PORTNUMBER:5000 \ + -v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + ianyliu/blast-radius-fork +``` + +
+ +## 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 +subdirectories -- don't forget to prefix `--serve`! + +For example, let's create a Terraform `project` with the following: + +```txt +$ tree -d +`-- project/ + |-- modules/ + | |-- foo + | |-- bar + | `-- dead + `-- stacks/ + `-- beef/ + `-- .terraform +``` + +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 \ + ianyliu/blast-radius-fork --serve stacks/beef +``` +
+ +## Image Building + +If you'd like to build your own Docker image after making changes to Blast Radius, you can build it in 2 ways: +1. Normal Build + +To execute a normal build, navigate (using commands like `cd`) to the root of your modified Blast Radius project in your terminal. +Make sure you have the Dockerfile in the root of your project. +Now run: + +``` +docker build -t imagename . +``` + +Replace imagename with the name you'd like to give your image. + +Once the build is complete you can run it in the Terraform directory you'd like visualize. + +> sh, zsh, bash +```sh +docker run --rm -it -p 5000:5000 \ + -v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + imagename +``` + +> Windows PowerShell +```powershell +docker run --rm -it -p 5000:5000 ` + -v ${pwd}:/data:ro ` + --security-opt apparmor:unconfined ` + --cap-add=SYS_ADMIN ` + imagename +``` + +Go to http://127.0.0.1:5000/ to view the visualization. + +2. Multi-CPU Build + +To build Docker images with multi-architecture support, we will use something known as docker +[buildx](https://docs.docker.com/build/buildx/). + +### Prerequisites for Buildx + +There are 2 ways to set up buildx, the first being insanely easy, while the second being quite complicated. + +1. **Install Docker Desktop>=2.1.0** +Install Docker Desktop [here](https://www.docker.com/products/docker-desktop/). + Then go to `Settings > Docker Engine`. Edit the JSON configuration and change it so that "buildkit" is set to "true". + ![](Docker/Docker-Desktop.png) +2. **Manual Installation** +You will need the following software requirements: +- Docker >= 19.03 +- Experimental mode enabled for Docker CLI + - Set an environment variable: `export DOCKER_CLI_EXPERIMENTAL=enabled` + - Edit config file at `$HOME/.docker/config.json`: `{"experimental": "enabled"}` +- Linux kernel >= 4.8 or fix-binary (F) flag support on the kernel side of binfmt_misc +- binfmt_misc file system mounted +- Host or Docker image based installation of: + - Host installation + - QEMU installation + - binfmt-support package >= 2.1.7 + - Docker image-based installation + - A Docker image containing both QEMU binaries and set up scripts that register QEMU in binfmt_misc + +For more details on manual installation see +[this Medium article](https://medium.com/@artur.klauser/building-multi-architecture-docker-images-with-buildx-27d80f7e2408). + +Check that buildx is installed: `docker buildx` + +Now we need to create a buildx builder + +`docker buildx create --name mybuilder` + +and use the builder + +`docker buildx use mybuilder` + +View your new builder: `docker buildx ls` + +As of 2022, buildx can export the image locally or to a Docker registry. + +However, local image loading is only supported for single-architecture images. +To use multi-architecture images, we will need to push to a Docker registry. + +To use a Docker registry, we need to first login using +`docker login`. +If you don't have a Docker account yet, you can create one +[here](https://hub.docker.com/). + +Navigate to the directory where the `Dockerfile` is located (download the repo via Git/GitHub if you haven't already). +Now we can build the image. + +```sh +docker buildx build \ +--platform \ +linux/arm64,linux/amd64,linux/amd64/v2,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6 \ +-t imagename \ +--push . +``` + +Run the image (replace USERNAME and imagename accordingly) +```sh +docker run --rm -it -p 5000:5000 \ + -v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + USERNAME/imagename +``` + +> Note: Architectures `linux/riscv64,linux/mips64le,linux/mips64` are supported by buildx, +but the image of Python Alpine does not usually support these architectures. +> Note: If in the future local loading of Docker images is supported, replace `--push` with `--load` + + +## Shell Scripts +In the [PowerShell folder](PowerShell) and [BASH folder](BASH) there are Docker build and run shell scripts. +Using shell scripts makes running and building the Docker containers easier and less error prone. + +Here's an example of running the docker Shell script for running a container. + +```sh +cd blast-radius-fork +/bin/bash ./docker_run.sh +``` + +Compare that to + +```sh +docker run --rm -it -p 5000:5000 \ + -v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + blast-radius-fork +``` + +## Aliases + +An alias in Linux is a shortcut to a command. They are usually used to replace long commands. +To see what aliases you have, run `alias`. + +There are 2 types of aliases. Temporary ones and permanent ones. + +#### Temporary Aliases + +To creat temporary aliases, simply follow the formula of ```alias SHORTCUT='COMMAND'```. Here's an example of a clear +command referenced by `c`: `alias c='clear'` + +Let's say we want to use `br-docker` to replace the long Docker run command. + +```sh +alias br-docker='docker run --rm -it -p 5000:5000 \ +-v $(pwd):/data:ro \ + --security-opt apparmor:unconfined \ + --cap-add=SYS_ADMIN \ + ianyliu/blast-radius-fork' +``` +Now we can just run `br-docker` whenever we want to run a Docker container to start Blast Radius! + +Here's another example using [Shell scripts](#shell-scripts). +```sh +alias br-build='/bin/bash /Users/USERNAME/blast-radius-fork/BASH/docker_build.sh' +``` +Now we can build our Docker image by just using `br-build`! + +#### Permanent Aliases + +To create permanent aliases, one needs to add it to their shell configuration file. + +* PowerShell configuration files are usually located in `$PSHOME` +* BASH: `~/.bashrc` +* ZSH: `~/.zshrc` +* FISH: `~/.config/fish/config.fish` + +Now open the shell config file in a text editor. +Example: `sudo vi ~/.bashrc` + +Go to the aliases section, and add your aliases. +Here's a helpful +[article](https://phoenixnap.com/kb/linux-alias-command#:~:text=In%20Linux%2C%20an%20alias%20is,and%20avoiding%20potential%20spelling%20errors.). \ No newline at end of file diff --git a/Docker/Docker-Desktop.png b/Docker/Docker-Desktop.png new file mode 100644 index 0000000..cb317c0 Binary files /dev/null and b/Docker/Docker-Desktop.png differ diff --git a/examples/docker-compose.yml b/Docker/docker-compose.yml similarity index 85% rename from examples/docker-compose.yml rename to Docker/docker-compose.yml index 5dc2f4f..f825dd9 100644 --- a/examples/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -1,8 +1,8 @@ -version: '3.7' +version: '3.8' services: blastradius: - image: 28mm/blast-radius + image: ianyliu/blast-radius-fork cap_add: - SYS_ADMIN security_opt: diff --git a/Dockerfile b/Dockerfile index 2d35634..87b9845 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,12 @@ -ARG TF_VERSION=0.12.12 -ARG PYTHON_VERSION=3.7 +ARG TF_VERSION=1.3.3 +ARG PYTHON_VERSION=3.10 FROM hashicorp/terraform:$TF_VERSION AS terraform FROM python:$PYTHON_VERSION-alpine -RUN pip install -U pip ply \ - && apk add --update --no-cache graphviz ttf-freefont +RUN pip install -U --no-cache-dir pip ply \ + && apk add --update --no-cache graphviz ttf-freefont git \ + && apk upgrade COPY --from=terraform /bin/terraform /bin/terraform COPY ./docker-entrypoint.sh /bin/docker-entrypoint.sh @@ -15,7 +16,9 @@ WORKDIR /src COPY . . RUN pip install -e . +# comment out 2 lines below to optimize build speed WORKDIR /data +RUN echo $(timeout 15 blast-radius --serve --port 5001; test $? -eq 124) > /output.txt ENTRYPOINT ["/bin/docker-entrypoint.sh"] -CMD ["blast-radius", "--serve"] +CMD ["blast-radius", "--serve"] \ No newline at end of file diff --git a/Kubernetes/k8-blast-radius-deployment.yaml b/Kubernetes/k8-blast-radius-deployment.yaml new file mode 100644 index 0000000..d9f8fda --- /dev/null +++ b/Kubernetes/k8-blast-radius-deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8-blast-radius-deployment +spec: + selector: + matchLabels: + app: flask-blast-radius + template: + metadata: + labels: + app: flask-blast-radius + spec: + containers: + - name: flask-k8s + image: docker.io/ianyliu/blast-radius-fork + imagePullPolicy: IfNotPresent + ports: + - containerPort: 5000 + args: ["blast-radius", "--serve"] + securityContext: + capabilities: + add: [ "SYS_ADMIN" ] \ No newline at end of file diff --git a/Kubernetes/k8-blast-radius-service.yaml b/Kubernetes/k8-blast-radius-service.yaml new file mode 100644 index 0000000..c1347a0 --- /dev/null +++ b/Kubernetes/k8-blast-radius-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: k8-blast-radius-service +spec: + selector: + app: flask-blast-radius + ports: + - protocol: "TCP" + port: 5000 + targetPort: 5000 + type: LoadBalancer \ No newline at end of file diff --git a/Makefile b/Makefile index b3a191a..65bf3e3 100644 --- a/Makefile +++ b/Makefile @@ -23,13 +23,13 @@ dist: # build docker image .PHONY: docker docker: - -docker build -t 28mm/blast-radius . + -docker build -t ianyliu/blast-radius-fork . # push pypi and docker images to public repos .PHONY: publish publish: -twine upload dist/* - -docker push 28mm/blast-radius:latest + -docker push ianyliu/blast-radius-fork:latest # rebuild categories.js from upstream docs .PHONY: categories diff --git a/PowerShell/docker_build.ps1 b/PowerShell/docker_build.ps1 new file mode 100644 index 0000000..e998329 --- /dev/null +++ b/PowerShell/docker_build.ps1 @@ -0,0 +1,8 @@ +& docker build -f ..\Dockerfile ` +-t blast-radius-fork ../ + +#Multi-cpu build +#& docker buildx build -f ..\Dockerfile ` +#--platform ` +#linux/arm64,linux/amd64,linux/amd64/v2,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6 ` +#-t blast-radius-fork --push ../ \ No newline at end of file diff --git a/PowerShell/docker_run.ps1 b/PowerShell/docker_run.ps1 new file mode 100644 index 0000000..611e72a --- /dev/null +++ b/PowerShell/docker_run.ps1 @@ -0,0 +1 @@ +& docker run --rm -it -p 5000:5000 -v "$PSScriptRoot/template:/data:ro" --security-opt apparmor:unconfined --cap-add=SYS_ADMIN blast-radius-fork \ No newline at end of file diff --git a/README.md b/README.md index d475fd7..b3a0772 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,200 @@ -# 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) +

Blast Radius Fork

[terraform]: https://www.terraform.io/ [examples]: https://28mm.github.io/blast-radius-docs/ - -_Blast Radius_ is a tool for reasoning about [Terraform][] dependency graphs -with interactive visualizations. +[docs]: https://28mm.github.io/blast-radius-docs/ + +_Blast Radius Fork_ is an interactive visualizer for [Terraform](https://www.terraform.io/) based off of +[_Blast Radius_](https://28mm.github.io/blast-radius/), +which hasn't been actively maintained since 2020. + +It is not guaranteed to be bug free. Please feel free to contribute! + +---------------------------------- + +## Table of Contents +- [Table of Contents](#table-of-contents) +- [Usage](#usage) +- [Prerequisites for Local Use](#prerequisites-for-local-use) +- [Local Quickstart](#local-quickstart) +- [Docker Quickstart](#docker-quickstart) +- [Kubernetes Quickstart](#kubernetes-quickstart) + - [Kubernetes Prerequisites](#kubernetes-prerequisites) + - [Start the App on Kubernetes](#start-the-app-on-kubernetes) + - [Kubernetes Debugging/Helpful Commands](#kubernetes-debugginghelpful-commands) +- [Parameters](#parameters) +- [Embedded Figures](#embedded-figures) +- [How It Works](#how-it-works) +- [Motivation](#motivation) +- [What's Different](#whats-different) +- [Future Implementations \& Possible Functionalities](#future-implementations--possible-functionalities) +- [Further Reading](#further-reading) +- [Other Tools to Check Out](#other-tools-to-check-out) + +---------------------------------- + +## Usage Use _Blast Radius_ to: * __Learn__ about *Terraform* or one of its providers through real [examples][] -* __Document__ your infrastructure +* __Visualize__ your infrastructure * __Reason__ about relationships between resources and evaluate changes to them * __Interact__ with the diagram below (and many others) [in the docs][examples] +* __Compare__ different infrastructure ![screenshot](doc/blastradius-interactive.png) -## Prerequisites +## Prerequisites for Local Use +* [Python](https://www.python.org/) 3.7+ * [Graphviz](https://www.graphviz.org/) -* [Python](https://www.python.org/) 3.7 or newer +* [Terraform](https://www.terraform.io/) (if you do not have generated Terraform DOT graphs yet) > __Note:__ For macOS you can `brew install graphviz` + +> __Note:__ For Docker usage prerequisites, see [Docker.md](Docker.md) -## Quickstart +## Local Quickstart -The fastest way to get up and running with *Blast Radius* is to install it with +The fastest way to run with *Blast Radius* is to install it with `pip` to your pre-existing environment: ```sh -pip install blastradius +python -m pip install git+https://github.com/Ianyliu/blast-radius-fork +``` +or +```sh +python3 -m pip install git+https://github.com/Ianyliu/blast-radius-fork ``` -Once installed just point *Blast Radius* at any initialized *Terraform* -directory: +You can then run Blast Radius from the command line: ```sh +blast-radius --serve +``` + +If you want to create graphs for an initialized *Terraform* directory, you can just start *Blast Radius* within the +initialized *Terraform* +directory: + +``` blast-radius --serve /path/to/terraform/directory ``` And you will shortly be rewarded with a browser link http://127.0.0.1:5000/. -## Docker +[//]: # (You can specify the host and/or port number with the `--host` and --port` flags:) + +[//]: # () +[//]: # (```) + +[//]: # (blast-radius --serve /path/to/terraform/directory --host 127.0.0.1 --port=8080) + +[//]: # (```) + + +Note: If you do not have an initialized Terraform directory but have the DOT script (the output of the `terraform graph` command, note that this is not the same as a JSON file or state graph). You can either copy and paste the DOT script into the text input field or uploaded the DOT script file. + +Other ways to run it include [Docker](#docker-quickstart) and [Kubernetes](#kubernetes-quickstart) + +## Docker Quickstart [privileges]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities [overlayfs]: https://wiki.archlinux.org/index.php/Overlay_filesystem -To launch *Blast Radius* for a local directory by manually running: +Launch a container for a local directory with *Blast Radius* running: +sh, zsh, bash, etc. (Linux recommended): ```sh docker run --rm -it -p 5000:5000 \ -v $(pwd):/data:ro \ --security-opt apparmor:unconfined \ --cap-add=SYS_ADMIN \ - 28mm/blast-radius + ianyliu/blast-radius-fork ``` -A slightly more customized variant of this is also available as an example -[docker-compose.yml](./examples/docker-compose.yml) usecase for Workspaces. - -### 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. - -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 -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 - -If you organized your *Terraform* project using stacks and modules, -*Blast Radius* must be called from the project root and reference them as -subdirectories -- don't forget to prefix `--serve`! - -For example, let's create a Terraform `project` with the following: - -```txt -$ tree -d -`-- project/ - |-- modules/ - | |-- foo - | |-- bar - | `-- dead - `-- stacks/ - `-- beef/ - `-- .terraform +Windows PowerShell: +```powershell +docker run --rm -it -p 5000:5000 ` + -v ${pwd}:/data:ro ` + --security-opt apparmor:unconfined ` + --cap-add=SYS_ADMIN ` + ianyliu/blast-radius-fork ``` -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: +Note: If you have spaces in your directory then you may have to change `-v ${pwd}:/data:ro` to `-v "${pwd}:/data:ro"` instead. -```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 +A slightly more customized variant of this is also available as an example +[docker-compose.yml](./Docker/docker-compose.yml) usecase for Workspaces. + +For more details on Docker usage, see [Docker.md](Docker.md) + +## Kubernetes Quickstart + +Launch *Kubernetes* locally using Minikube, Kubernetes, and Kubectl: + +### Kubernetes Prerequisites +
+ + +* Docker (or another container or virtual machine manager) +* Kubectl: https://kubernetes.io/docs/tasks/tools/ +* Minikube: https://minikube.sigs.k8s.io/docs/start/ +
+ +### Start the App on Kubernetes +
+ + +1. Start Minikube +```minikube start``` +2. Change directories to the file containing the 2 YAML files (*k8-blast-radius-deployment.yaml* and +3. *k8-blast-radius-service.yaml* apply the YAML configuration files to the default namespace (or any other namespace) +``` +kubectl apply -f k8-blast-radius-deployment.yaml +kubectl apply -f k8-blast-radius-service.yaml ``` +Access the app +``` +minikube service k8-blast-radius-service +``` +
+ +### Kubernetes Debugging/Helpful Commands +
+ + +* To check the state of your pods (containers), execute the following: +```kubectl get pods``` +* To see more details about a pod. (Replace `````` and `````` with the corresponding values) +```kubectl describe -n= pod/``` +* To see logs for a pod (replace corresponding values) +```kubectl logs -f -n= ``` +* The most helpful tool is probably Minikube's dashboard, where you can more things +```minikube dashboard``` +
+ +## Parameters +* Directory: Defaults to `$PWD` or current directory. The directory in which to look for Terraform files. +This is required if the user wants to use a Terraform project as input +(instead of uploading a file or pasting DOT script). +* `--host`: Defaults to 0.0.0.0. The IP address to bind to, to access the app (http://HOST:5000) +* `--port`: Defaults to 5000. The port to access the app (http://localhost:PORT) +Any valid localhost port is allowed. +* `--serve`: Starts a webserver locally with Terraform's interactive graph +* `--json`: Prints a JSON representation of a Terraform graph. The JSON has 2 items, `edges` and `nodes`. +* `--dot`: Returns a string consisting of Graphviz DOT script of graph. (no colors) +* `--svg`: Prints SVG representation of graph (with colors). +* `--graph`: +* `--module-depth`: Takes an integer as input and only eliminates display of deeply nested modules. +This will not show every node on the graph unless the user specifies a depth larger than the graph. +* `--focus`: Show only specified resource and its dependencies. Not available in web app. Only works with `--json` and `--svg`. + * Example: ```terraform graph | blast-radius --focus \ + "[root] module.us-west-2.module.secondary_subnet.data.aws_vpc.target" --svg``` +* `--center`: Prunes the graph to a subgraph (same thing as red button in web app). Only works with `--json` and `--svg`. + * Example: ```terraform graph | blast-radius --center \ + "[root] module.us-west-2.module.secondary_subnet.data.aws_vpc.target" --svg``` ## Embedded Figures @@ -119,18 +205,85 @@ You will need the following: 2. `javascript` and `css` found in `.../blastradius/server/static` 3. A uniquely identified DOM element, where the `` should appear. -You can read more details in the [documentation](doc/embedded.md) +You can read more details in the [documentation for embedded figures](doc/embedded.md). + +## How It Works + +*Blast Radius* uses +- [Graphviz](https://graphviz.org/) package to layout graph diagrams +- [python-hcl2](https://github.com/amplify-education/python-hcl2) to parse [Terraform][] configuration +- [d3.js](https://d3js.org/) to implement interactive features +- [Flask](https://flask.palletsprojects.com/) to start a server +- [Vanilla JavaScript](http://vanilla-js.com/) and [jQuery](https://jquery.com/) for front-end functionality +- [HTML](https://html.com/), [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), +- [Bootstrap](https://getbootstrap.com/), and other libraries for front-end design + +Terraform generates graphs in the form of [DOT](https://en.wikipedia.org/wiki/DOT_language) language. *Blast Radius* +uses [Graphviz](https://graphviz.org/) to layout the graph after converting to SVG, +and D3.js to implement interactive features. +Terraform configurations are then parsed by [python-hcl2](https://github.com/amplify-education/python-hcl2) to generate +a [JSON](https://en.wikipedia.org/wiki/JSON_document) representation of the graph, +which provides details of each resource on hover. +All of this is hosted on [Flask](https://flask.palletsprojects.com/) and runs on a [local server](http://localhost:5000/). + +## Motivation +The original creator of this open source project, [Patrick McMurchie](https://github.com/28mm), has been inactive on +this project for some time. +There are many issues waiting to be resolved, and features to be added. This repository presents some +**basic modifications**, **additional features**, and **enhanced accessibility**. + +## What's Different +* **Independence** from Terraform and Terraform files + * App can run on its own, accepting DOT file or keyboard input +* Multi-graph feature + * The app can **display multiple graphs** and can be compared side by side with tabs +* Print + * The graph can be printed, although the print can sometimes cut off the graph at times +* UI changes + * To enable a better design, the multi-colored buttons and other parts of the page have been changed to follow the 60-30-10 design rule +* Compatability with recent versions of Terraform, Python, and Python packages +* Created a new Docker image at [ianyliu/blast-radius-fork](https://hub.docker.com/repository/docker/ianyliu/blast-radius-fork/) for multi-cpu architectures equipped with updated features +* Added Shell scripts that can be used to run & build the Docker image with aliases for convenience +* Updated README.md +* Integrated changes across other forks and pull requests of Blast Radius, including: + * PowerShell scripts for running and building Docker containers + * Running Terratests during Docker build + * Allowing Blast Radius to run even if JSON data couldn't be parsed + +## Future Implementations & Possible Functionalities +* Accept file input as a command-line argument + * (```Ex. blast-radius --serve --graphfile ./graphraw.txt```) +* Allow downloading of other file formats such as .json, .png, .jpg, .zip with entire static assets +* Hovering over tabs display number of resources or perhaps a snapshot of the graph +* Allow upload of multiple files and folders +* Drag and drop file upload +* Dark mode +* Support for Terragrunt +* Support for Tfenv +* Compress Docker image size (currently 180+MB) +* Editable tab names +* Reorder tabs via drag +* Responsive webpage +* Add filter by color option for graphs +* Graph sharing + * Generate unique URL to allow users to view graphs created by others +* Create standalone executable (run without CLI) + * Docker2exe + * PyInstaller +* Mobile interface formatting +* Loading spinner before graphs load (and disable buttons) +* Cache DOT script or SVG in local storage so it can be loaded next time without re-upload +* Animation + * Shows difference between current state and state after apply + * Shows difference between one Terraform graph and another via animation +* Add example Terraform DOT files that allow Blast Radius to be run on Microsoft Azure, Google Cloud, IBM Cloud Services, etc. +* Integration with Neo4j or other graph database, parse *Terraform* files and find relationships between resources -## 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. ## Further Reading -The development of *Blast Radius* is documented in a series of -[blog](https://28mm.github.io) posts: +The original development of *Blast Radius* is documented in a series of +[blog](https://28mm.github.io) posts by the original creator: * [part 1](https://28mm.github.io/notes/d3-terraform-graphs): motivations, d3 force-directed layouts vs. vanilla graphviz. * [part 2](https://28mm.github.io/notes/d3-terraform-graphs-2): d3-enhanced graphviz layouts, meaningful coloration, animations. @@ -149,3 +302,86 @@ These examples are drawn primarily from the `examples/` directory distributed with various *Terraform* providers, and aren't necessarily ideal. Additional examples, particularly demonstrations of best-practices, or of multi-cloud configurations strongly desired. + +There are 258 forks as of October 2024, each containing new updates or features of some sort. Notable ones include: +- https://github.com/gruberdev/blast-radius/ +- https://github.com/IBM-Cloud/blast-radius/ +- https://github.com/nishubharti/blast-radius/ +- https://github.com/obourdon/blast-radius/ +- https://github.com/nibhart1/blast-radius/ + +An alternate working Docker image for Blast Radius is https://hub.docker.com/r/grubertech/blast-radius + + +It would greatly help if you could contribute to bringing all of these forks into one repository so that we can have a tool that can be used by everyone. + +## Other Tools to Check Out + +[Inframap]: https://github.com/cycloidio/inframap +[Terraform Graph Beautifier]: https://github.com/pcasteran/terraform-graph-beautifier +[Terraform Visual]: https://github.com/hieven/terraform-visual +[Rover]: https://github.com/im2nguyen/rover +[Pluralith]: https://www.pluralith.com/ +* [Pluralith] + * "_A tool for Terraform state visualisation and automated generation of infrastructure documentation_" + * Written in: Golang + * Pros + * Change Highlighting + * Apply plan within application + * Cost information + * Provides granular details on click + * Plan-to-plan comparison (tabs) + * Filter (by Created, Destroyed, Updated, Recreated) + * GUI (Graphical User Interface) + * Lots more features... there's so many! + * Cons + * More advanced features cost money (understandably) + * Newer, with less tutorials and tests +* [Inframap] + * "_Read your tfstate or HCL to generate a graph specific for each provider, showing only the resources that are most important/relevant._" + * Input: tfstate or HCL + * Written in: Golang + * Pros: + * Works directly with Terraform state files or .tf files, instead of DOT input + * Docker + * Simplifies graph + * Cons: + * Cannot provide more detail, oversimplification +* [Terraform Graph Beautifier] + * "_Terraform graph beautifier_" + * Input: DOT script output from ```terraform graph``` command in Terraform init directory + * Written in: Golang + * Pros + * Outputs to: HTML page, JSON document, cleaner version of Graphviz DOT script + * Cons + * Requires Terraform init directory and Terraform installation +* [Terraform Visual] + * "_Terraform Visual is an interactive way of visualizing your Terraform plan_" + * Input: Terraform JSON plan files + * Written in: TypeScript, JavaScript, CSS + * Pros + * Docker compatible + * Creates HTML page that you can save later + * Has online version: https://hieven.github.io/terraform-visual/ (so doesn't require local installation) +* [Rover] + * "_Interactive Terraform visualization. State and configuration explorer._" + * Inputs: Terraform files in a directory or provided plan file + * Written in: Golang, VueJS + * Pros + * Very granular view and control of resources + * Shareable via .svg, .html, .json + * Standalone mode generates .zip file containing all static assets + * Docker compatible + * Cons + * Requires Terraform directory to be init, or else it will not work (even in Docker it also needs init) + + +## Star History + + + + + + Star History Chart + + diff --git a/bin/blast-radius b/bin/blast-radius index eca3dd9..702db49 100755 --- a/bin/blast-radius +++ b/bin/blast-radius @@ -21,14 +21,15 @@ def main(): parser = parser = argparse.ArgumentParser(description='blast-radius: Interactive Terraform Graph Visualizations') parser.add_argument('directory', type=str, help='terraform configuration directory', default=os.getcwd(), nargs='?') + parser.add_argument('--host', type=str, help='specify an IP to bind to other than the default 0.0.0.0', default=os.getenv('BLAST_RADIUS_HOST','0.0.0.0')) parser.add_argument('--port', type=int, help='specify a port other than the default 5000', default=os.getenv('BLAST_RADIUS_PORT',5000)) - + output_group = parser.add_mutually_exclusive_group() output_group.add_argument('--json', action='store_const', const=True, default=False, help='print a json representation of the Terraform graph') 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') - + 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. @@ -45,9 +46,14 @@ def main(): args = parser.parse_args() if args.serve: - os.chdir(args.directory) - app.run(host='0.0.0.0',port=args.port) - sys.exit(0) + if args.graph == sys.stdin: + os.chdir(args.directory) + app.run(host=args.host,port=args.port) + sys.exit(0) + else: + os.chdir(args.directory) + app.run(host=args.host,port=args.port) + sys.exit(0) elif args.json or args.dot or args.svg: if args.graph is sys.stdin: diff --git a/blastradius/handlers/dot.py b/blastradius/handlers/dot.py index 1126c4b..2f5fac9 100644 --- a/blastradius/handlers/dot.py +++ b/blastradius/handlers/dot.py @@ -1,3 +1,6 @@ +#Allow print function to work in Python 2 +from __future__ import print_function + # standard libraries import json import re @@ -451,7 +454,7 @@ def _module(label): try: if not re.match(r'(\[root\]\s+)*module\..*', label): return 'root' - m = re.match(r'(\[root\]\s+)*(?P\S+)\.(?P\S+)\.\S+', label) + m = re.match(r'(\[root\]\s+)*(?P\S+)\.(?P\S+)\.?\S+', label) return m.groupdict()['module'] except: raise Exception("None: ", label) diff --git a/blastradius/handlers/terraform.py b/blastradius/handlers/terraform.py index 00b41d9..fd57e4d 100644 --- a/blastradius/handlers/terraform.py +++ b/blastradius/handlers/terraform.py @@ -1,3 +1,6 @@ +#Allow print function to work in Python 2 +from __future__ import print_function + # standard libraries from glob import iglob import io @@ -5,66 +8,91 @@ 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 - them with pyhcl, and exposes the configuration via self.config.""" + them with hcl2, and exposes the configuration via self.config.""" def __init__(self, directory=None, settings=None): self.settings = settings if settings else {} # handle the root module first... - self.directory = directory if directory else os.getcwd() + self.directory = os.path.abspath(directory) if directory else os.getcwd() #print(self.directory) + self.config = {} self.config_str = '' iterator = iglob( self.directory + '/*.tf') for fname in iterator: with open(fname, 'r', encoding='utf-8') as f: - self.config_str += f.read() + ' ' - config_io = io.StringIO(self.config_str) - self.config = hcl.load(config_io) + try: + contents = f.read() + self.config_str += contents + ' ' + raw = io.StringIO(contents) + parsed = hcl.load(raw) + self.config.update(parsed) + except Exception as e: + raise RuntimeError('Exception occurred while parsing ' + fname) from e # then any submodules it may contain, skipping any remote modules for # the time being. self.modules = {} if 'module' in self.config: - for name, mod in self.config['module'].items(): + for name, mod in [(k, v) for x in self.config['module'] for (k, v) in x.items()]: if 'source' not in mod: continue - source = mod['source'] - # '//' used to refer to a subdirectory in a git repo - if re.match(r'.*\/\/.*', source): - continue - # '@' should only appear in ssh urls - elif re.match(r'.*\@.*', source): - continue - # 'github.com' special behavior. - elif re.match(r'github\.com.*', source): - continue - # points to new TFE module registry - elif re.match(r'app\.terraform\.io', source): - continue - # bitbucket public and private repos - elif re.match(r'bitbucket\.org.*', source): - continue - # git::https or git::ssh sources - elif re.match(r'^git::', source): - continue - # git:// sources - elif re.match(r'^git:\/\/', source): - continue - # Generic Mercurial repos - elif re.match(r'^hg::', source): - continue - # Public Terraform Module Registry - elif re.match(r'^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+', source): - continue - # AWS S3 buckets - elif re.match(r's3.*\.amazonaws\.com', source): - continue - # fixme path join. eek. - self.modules[name] = Terraform(directory=self.directory+'/'+source, settings=mod) + source = mod['source'][0] + + # # '//' used to refer to a subdirectory in a git repo + # if re.match(r'.*\/\/.*', source): + # continue + # # '@' should only appear in ssh urls + # elif re.match(r'.*\@.*', source): + # continue + # # 'github.com' special behavior. + # elif re.match(r'github\.com.*', source): + # continue + # # points to new TFE module registry + # elif re.match(r'app\.terraform\.io', source): + # continue + # # bitbucket public and private repos + # elif re.match(r'bitbucket\.org.*', source): + # continue + # # git::https or git::ssh sources + # elif re.match(r'^git::', source): + # continue + # # git:// sources + # elif re.match(r'^git:\/\/', source): + # continue + # # Generic Mercurial repos + # elif re.match(r'^hg::', source): + # continue + # # Public Terraform Module Registry + # elif re.match(r'^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_]+', source): + # continue + # # AWS S3 buckets + # elif re.match(r's3.*\.amazonaws\.com', source): + # continue + + if source == '.': + continue # avoid infinite recursion + + path = os.path.join(self.directory, source) + if os.path.exists(path): + # local module + self.modules[name] = Terraform(directory=path, settings=mod) + else: + # remote module + # Since terraform must be init'd before use, we can + # assume remote modules have been downloaded to .terraform/modules + path = os.path.join(os.getcwd(), '.terraform', 'modules', name) + + # Get the subdir if any + match = re.match(r'.*(\/\/.*)(?!:)', source) + if re.match(r'.*\/(\/.*)(?!:)', source): + path = os.path.join(path, match.groups()[0]) + + self.modules = Terraform(directory=path, settings=mod) def get_def(self, node, module_depth=0): diff --git a/blastradius/server/server.py b/blastradius/server/server.py index aae9278..ffb1e7f 100644 --- a/blastradius/server/server.py +++ b/blastradius/server/server.py @@ -1,13 +1,16 @@ +#Allow print function to work in Python 2 +from __future__ import print_function + # standard libraries +from asyncio import run import os +import glob import subprocess import itertools import json # 3rd-party libraries -from flask import Flask -from flask import render_template -from flask import request +from flask import Flask, render_template, request, flash, redirect, jsonify, send_file import jinja2 # 1st-party libraries @@ -18,43 +21,194 @@ app = Flask(__name__) + @app.route('/') def index(): + is_terraform_installation = True + is_terraform_directory = True + tf_data_dir = os.getenv('TF_DATA_DIR') #Might remove later, not sure what to do with it yet + # we need terraform, graphviz, and an init-ed terraform project. if not which('terraform') and not which('terraform.exe'): - return render_template('error.html') - elif not which('dot') and not which('dot.exe'): - return render_template('error.html') - elif not os.path.exists('.terraform'): - return render_template('error.html') + is_terraform_installation = False + if not os.path.exists('.terraform') and not (tf_data_dir is not None and os.path.exists(tf_data_dir)): + is_terraform_directory = False + if not which('dot') and not which('dot.exe'): + #Return error page. Graphviz is a dependency that has to exist. + return render_template('error.html', tf_dir=is_terraform_directory, gviz_install=False, + tf_install=is_terraform_installation) + + if tf_data_dir is not None and os.path.exists(tf_data_dir): + folder_name = os.path.basename(os.path.dirname(tf_data_dir)) else: - return render_template('index.html', help=get_help()) + folder_name = os.path.basename(os.path.dirname(os.getcwd())) + + if is_terraform_directory is False or is_terraform_installation is False: + # Blast Radius template without default graph + print("Blast Radius could not find a Terraform directory ") if is_terraform_directory is False else print("Blast Radius could not find a Terraform installation. ") + template = 'non_tf_dir.html' + else: + # Blast Radius template with default graph + print("Blast Radius is generating graphs for your Terraform directory. ") + template = 'index.html' + + #Run Blast Radius presenting a default graph + return render_template(template, help=get_help(), folder_name=folder_name) + + +@app.route('/upload', methods=['POST']) +def upload(): + if 'file' not in request.files: + flash('No file submitted') + return redirect("/") + file = request.files['file'] + + filecontent = file.read().decode("utf-8") + + module_depth = request.args.get('module_depth', default=None, type=int) + refocus = request.args.get('refocus', default=None, type=str) + + dot = initalizeDotGraph(content=filecontent, + module_depth=module_depth, refocus=refocus) + + resp = {"SVG": dot.svg(), "JSON": dot.json()} + return jsonify(resp) + + +@app.route('/input', methods=['POST']) +def input(): + if 'input' not in request.form: + flash('No input submitted') + return redirect("/") + dot_input = request.form['input'] + + module_depth = request.args.get('module_depth', default=None, type=int) + refocus = request.args.get('refocus', default=None, type=str) + + dot = initalizeDotGraph(content=dot_input, + module_depth=module_depth, refocus=refocus) + + resp = {"SVG": dot.svg(), "JSON": dot.json()} + return jsonify(resp) + + +# @app.route('/convert/', methods=['POST']) +# def convert(filetype): +# removeExistingFiles() +# +# if 'file' not in request.files: +# flash('No file was submitted for conversion') +# return redirect("/") +# +# filecontent = request.files['file'].read().decode("utf-8") +# file = None +# +# if filetype == "pdf": +# file = './converter.pdf' +# svg2pdf(file_obj=filecontent, write_to=file) +# elif filetype == "ps": +# file = './converter.ps' +# svg2png(file_obj=filecontent, write_to=file) +# elif filetype == "png": +# file = './converter.png' +# svg2ps(file_obj=filecontent, write_to=file) +# else: +# flash('Only PDF, PNG, PS files are supported for download') +# return redirect("/") +# +# return send_file(file) + + +@app.route('/error') +def error(): + return render_template('error.html', tf_dir="Not sure", gviz_install="Not sure", tf_install="Not sure") + @app.route('/graph.svg') def graph_svg(): Graph.reset_counters() - dot = DotGraph('', file_contents=run_tf_graph()) module_depth = request.args.get('module_depth', default=None, type=int) - refocus = request.args.get('refocus', default=None, type=str) + refocus = request.args.get('refocus', default=None, type=str) - if module_depth is not None and module_depth >= 0: - dot.set_module_depth(module_depth) + dot = initalizeDotGraph(content=run_tf_graph(), + module_depth=module_depth, refocus=refocus) - if refocus is not None: - node = dot.get_node_by_name(refocus) - if node: - dot.center(node) + # dot = DotGraph('', file_contents=test_content_tfproj) + + # 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: + # dot.set_module_depth(module_depth) + + # if refocus is not None: + # node = dot.get_node_by_name(refocus) + # if node: + # dot.center(node) return dot.svg() @app.route('/graph.json') def graph_json(): Graph.reset_counters() - dot = DotGraph('', file_contents=run_tf_graph()) + module_depth = request.args.get('module_depth', default=None, type=int) - refocus = request.args.get('refocus', default=None, type=str) + refocus = request.args.get('refocus', default=None, type=str) + + dot = initalizeDotGraph(content=run_tf_graph(), + module_depth=module_depth, refocus=refocus) + + # dot = DotGraph('', file_contents=run_tf_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: + # dot.set_module_depth(module_depth) + + # tf = Terraform(os.getcwd()) + # for node in dot.nodes: + # node.definition = tf.get_def(node) + + # if refocus is not None: + # node = dot.get_node_by_name(refocus) + # if node: + # dot.center(node) + + return dot.json() + + +# @app.route('/fupload/') +# def uploadFile(filename): +# path = os.path.join(os.getcwd(), filename) +# if not os.path.exists(path): +# return "File/filepath " +# with open(path) as f: +# contents = f.read() + +# Graph.reset_counters() + +# module_depth = request.args.get('module_depth', default=None, type=int) +# refocus = request.args.get('refocus', default=None, type=str) + +# dot = initalizeDotGraph(content=contents, +# module_depth=module_depth, refocus=refocus) + +# return dot.svg() + + +def run_tf_graph(): + completed = subprocess.run(['terraform', 'graph'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if completed.returncode != 0: + raise Exception('Execution error', completed.stderr) + return completed.stdout.decode('utf-8') + + +def initalizeDotGraph(content, module_depth, refocus): + dot = DotGraph('', file_contents=content) + + # 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: dot.set_module_depth(module_depth) @@ -67,29 +221,42 @@ def graph_json(): if node: dot.center(node) - return dot.json() + return dot + + +def removeExistingFiles(): + for filename in glob.glob(os.path.join(os.getcwd(), "converter*")): + os.remove(filename) -def run_tf_graph(): - completed = subprocess.run(['terraform', 'graph'], stdout=subprocess.PIPE) - if completed.returncode != 0: - raise Exception('Execution error', completed.stderr) - return completed.stdout.decode('utf-8') def get_help(): - return { 'tf_version' : get_terraform_version(), - 'tf_exe' : get_terraform_exe(), - 'cwd' : os.getcwd() } + return {'tf_version': get_terraform_version(), + 'tf_exe': get_terraform_exe(), + 'cwd': os.getcwd(), + 'python_version': get_python_version()} + def get_terraform_version(): - completed = subprocess.run(['terraform', '--version'], stdout=subprocess.PIPE) + completed = subprocess.run( + ['terraform', '--version'], stdout=subprocess.PIPE) if completed.returncode != 0: raise return completed.stdout.decode('utf-8').splitlines()[0].split(' ')[-1] + def get_terraform_exe(): return which('terraform') +def get_python_version(): + completed = subprocess.run( + ['python3', '--version'], stdout=subprocess.PIPE) - + if completed.returncode != 0: + print("'python3' was not found, trying again with 'python' ... ") + completed2 = subprocess.run( + ['python', '--version'], stdout=subprocess.PIPE) + if completed2.returncode != 0: + raise + return completed.stdout.decode('utf-8').splitlines()[0].split(' ')[-1] diff --git a/blastradius/server/static/css/style.css b/blastradius/server/static/css/style.css index 63d8f56..f306027 100644 --- a/blastradius/server/static/css/style.css +++ b/blastradius/server/static/css/style.css @@ -135,3 +135,80 @@ g.node text { font-family: 'consolas', 'monaco', 'fixed-width'; font-size: 8px; } + +.spinner { + width: 80px; + height: 80px; + border: 4px solid #f3f3f3; + border-top: 6px solid #f25a41; + border-radius: 100%; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + animation: spin 1s infinite linear; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +#overlay { + height: 100%; + width: 100%; + background: rgb(0, 0, 0); + position: fixed; + left: 0; + top: 0; +} + +.drag-area{ + /* display: flex; */ + align-items: center; + justify-content: center; + /* flex-direction: column; */ +} + +.drag-area button{ + padding: 7px 18px; + font-size: 100%; + font-weight: 500; + border: none; + outline: none; + background: #fff; + color: #5256ad; + border-radius: 3px; + cursor: pointer; +} + +/* Style tab links */ +.tablink{ + border: none; + outline: none; + cursor: pointer; + font-size: 100%; + width: fit-content; + display: inline-block; +} + +.tablink:hover { + background-color: rgb(252, 186, 3); + color: #fff; +} + +.close-tab { + margin-left: -2%; + padding: revert !important; +} +.close-tab:hover { + background-color: #f25a41 !important; + color: white !important; +} \ No newline at end of file diff --git a/blastradius/server/static/js/blast-radius.js b/blastradius/server/static/js/blast-radius.js index 0f9a466..9229fd3 100644 --- a/blastradius/server/static/js/blast-radius.js +++ b/blastradius/server/static/js/blast-radius.js @@ -5,11 +5,11 @@ // enumerate the various kinds of edges that Blast Radius understands. // only NORMAL and LAYOUT_SHOWN will show up in the , but all four // will likely appear in the json representation. -var edge_types = { - NORMAL : 1, // what we talk about when we're talking about edges. - HIDDEN : 2, // these are normal edges, but aren't drawn. - LAYOUT_SHOWN : 3, // these edges are drawn, but aren't "real" edges - LAYOUT_HIDDEN : 4, // these edges are not drawn, aren't "real" edges, but inform layout. +const edge_types = { + NORMAL: 1, // what we talk about when we're talking about edges. + HIDDEN: 2, // these are normal edges, but aren't drawn. + LAYOUT_SHOWN: 3, // these edges are drawn, but aren't "real" edges + LAYOUT_HIDDEN: 4, // these edges are not drawn, aren't "real" edges, but inform layout. } // Sometimes we have escaped newlines (\n) in json strings. we want
instead. @@ -21,74 +21,297 @@ var replacer = function (key, value) { return value; } -build_uri = function(url, params) { +build_uri = function (url, params) { url += '?' for (var key in params) url += key + '=' + params[key] + '&'; - return url.slice(0,-1); + return url.slice(0, -1); } -var to_list = function(obj) { +let uploadRequest = (url, formData, selector) => { + + fetch(url, { + method: "POST", + body: formData + }) + .then(response => (response.json())) + .then(async function (resjson) { + let br_state = { + selector: {} + } + + xml = $.parseXML(resjson.SVG); + json = JSON.parse(resjson.JSON); + await blastradius(selector, '/graph.svg', '/graph.json', br_state, xml, json) + }); +} + +let uploadFile = function (file, tabNumber) { + let fileType = file.type; + let selector = "#graph-" + tabNumber; + let validExtensions = ["text/plain"]; + if (validExtensions.includes(fileType)) { + let formData = new FormData(); + formData.set('file', file); + uploadRequest('/upload', formData, selector) + } else { + alert("This is not a valid File!"); + } +} + +let inputGraph = async () => { + let graphinput = prompt("Please paste Graphviz DOT script "); + + if (graphinput != null) { + let lastTabContent = $("div.tabcontent").last()[0] + if (lastTabContent != null) { + let prevNumber = parseInt(lastTabContent.id.split("-")[1]) + let curNumber = parseInt(prevNumber) + 1 + let selector = "#graph-" + curNumber; + await insertTabContent(prevNumber) + await createTab(`input-graph${curNumber}`, curNumber); + + let formData = new FormData(); + formData.set('input', graphinput); + + await uploadRequest('/input', formData, selector); + + $('#tablink-' + curNumber).click(); + } else { + console.log("Last tab's content could not be retrieved.") + } + + } else { + alert("Invalid graph input or empty input!") + } +} +/** + * @param {string} filename - The filename + * @param {int} tabNumber - The number of the tab (can be found in ID of tab) + */ +let createTab = (filename, tabNumber) => { + let newTab = ``; + + $('ul.navbar-nav.tab-ul').append(newTab); + var x = Math.round(0xffffff * Math.random()).toString(16); + var y = (6 - x.length); + var z = "000000".substring(0, y); + var randomColor = "#" + z + x; + $('#tablink-' + tabNumber).css("color", "white"); + $('#close-tab-' + tabNumber).css("color", "white") + + document.getElementById("tablink-" + tabNumber).onclick = function () { + displayTabContent(tabNumber, randomColor) + } + document.getElementById("close-tab-" + tabNumber).onclick = function () { + closeTab(tabNumber); + } + + let firstTabNum = parseInt($("div.tabcontent").first()[0].id.split("-")[1]) + let firstXButton = $(`#close-tab-${firstTabNum}`); + + //If the first tab's X has been removed + if (firstXButton.prop("disabled") || firstXButton.prop("hidden")) { + firstXButton.prop("disabled", false); + firstXButton.prop("hidden", false); + } + +} + +let displayTabContent = (tabNumber, color) => { + let tabContents = document.getElementsByClassName("tabcontent"); + let tabLinks = document.getElementsByClassName("tablink"); + let closeTabs = document.getElementsByClassName("close-tab"); + let currentTab = $('#tablink-' + tabNumber) + let currentTabContent = $('#tab-' + tabNumber) + let currentCloseTab = $('#close-tab-' + tabNumber) + + for (let i = 0; i < tabContents.length; i++) { + tabContents[i].style.display = "none"; + } + + for (let i = 0; i < tabLinks.length; i++) { + tabLinks[i].style.backgroundColor = ""; + tabLinks[i].style.color = "#5256ad"; + } + + for (let i = 0; i < closeTabs.length; i++) { + closeTabs[i].style.backgroundColor = ""; + closeTabs[i].style.color = "#5256ad"; + } + + currentTabContent.css("display", "block"); + currentCloseTab.css("background-color", color); + currentCloseTab.css("color", "white"); + currentTab.css("background-color", color); + currentTab.css("color", "white"); + currentTab.addClass("open-tab"); +} +/** + * @param {int} tabNumber - The tab number + */ +let closeTab = (tabNumber) => { + + $(`#tab-${tabNumber}`).remove(); + $(`#nav-item-${tabNumber}`).remove(); + $(`.graph-${tabNumber}-d3-tip`).remove(); + + let newLastTabNum = parseInt($("div.tabcontent").last()[0].id.split("-")[1]) + $(`#tablink-${newLastTabNum}`).click(); //open last tab + + let xButton = $(`#close-tab-${newLastTabNum}`) + //If there's only one tab left after closing the current tab (first element == last element) + if ($("div.tabcontent").last()[0] === $("div.tabcontent").first()[0]) { + xButton.prop("disabled", true); + xButton.prop("hidden", true); + } + +} + +let insertTabContent = (prevNumber) => { + let curNum = prevNumber + 1 + let graphSelector = 'graph-' + curNum + let helpButton = $(`#tab-${prevNumber}`).children("nav").children("ul").children("li").last().children("div.dropdown").parent()[0].innerHTML + let topNavBar = '' + + ' ` + + `
` + + let newTabContentDiv = '
' + let newTabGraph = '
' + $('.drag-area').append(newTabContentDiv); + $(`#tab-${curNum}`).append(topNavBar, newTabGraph); + + +} + +var to_list = function (obj) { var lst = []; for (var k in obj) lst.push(obj[k]); return lst; } -var Queue = function() { +var Queue = function () { this._oldestIndex = 1; this._newestIndex = 1; this._storage = {}; } - -Queue.prototype.size = function() { + +Queue.prototype.size = function () { return this._newestIndex - this._oldestIndex; }; - -Queue.prototype.enqueue = function(data) { + +Queue.prototype.enqueue = function (data) { this._storage[this._newestIndex] = data; this._newestIndex++; }; - -Queue.prototype.dequeue = function() { + +Queue.prototype.dequeue = function () { var oldestIndex = this._oldestIndex, newestIndex = this._newestIndex, deletedData; - + if (oldestIndex !== newestIndex) { deletedData = this._storage[oldestIndex]; delete this._storage[oldestIndex]; this._oldestIndex++; - + return deletedData; } }; -// Takes a unique selector, e.g. "#demo-1", and +// Takes a unique selector, e.g. "#demo-1", and // appends svg xml from svg_url, and takes graph // info from json_url to highlight/annotate it. -blastradius = function (selector, svg_url, json_url, br_state) { +blastradius = function (selector, svg_url, json_url, br_state = {}, uploadXML = null, uploadJSON = null) { + + if ((uploadJSON == null && uploadXML != null) || (uploadJSON != null && uploadXML == null)) { + return + } // TODO: remove scale. scale = null // mainly for d3-tips - class_selector = '.' + selector.slice(1,selector.length); - - - // we should have an object to keep track of state with, but if we - // don't, just fudge one. - if (! br_state) { - var br_state = {}; - } + class_selector = '.' + selector.slice(1, selector.length); // if we haven't already got an entry in br_state to manage our // state with, go ahead and create one. - if (! br_state[selector]) { + if (!br_state[selector]) { br_state[selector] = {}; } - var state = br_state[selector]; + var state = br_state[selector]; var container = d3.select(selector); // color assignments (resource_type : rgb) are stateful. If we use a new palette @@ -97,16 +320,24 @@ blastradius = function (selector, svg_url, json_url, br_state) { var color = (state['color'] ? state['color'] : d3.scaleOrdinal(d3['schemeCategory20'])); state['color'] = color; - //console.log(state); - // 1st pull down the svg, and append it to the DOM as a child // of our selector. If added as , we wouldn't - // be able to manipulate x.svg with d3.js, or other DOM fns. + // be able to manipulate x.svg with d3.js, or other DOM fns. d3.xml(svg_url, function (error, xml) { + if (uploadXML != null) { + xml = uploadXML; + } + container.node() .appendChild(document.importNode(xml.documentElement, true)); + // The xml.documentElement node by default imports a element under that has an id of "graph0", + //which may causes problems like inability to upload a file with the same filename, + // so the code below replaces the id to a unique value + let repetitiveId = $(`${selector}`).children("svg").children("g#graph0.graph")[0].id + $(`#${repetitiveId}`).prop("id", `graph${selector.substring(selector.indexOf('-') + 1)}`); + // remove s in svg; graphviz leaves these here and they // trigger useless tooltips. d3.select(selector).selectAll('title').remove(); @@ -119,27 +350,38 @@ blastradius = function (selector, svg_url, json_url, br_state) { d3.select(selector + ' svg').attr('width', '100%').attr('height', '100%'); // Obtain the graph description. Doing this within the - // d3.xml success callback, to guaruntee the svg/xml + // d3.xml success callback, to gurantee the svg/xml // has loaded. d3.json(json_url, function (error, data) { + + if (uploadJSON !== null) { + data = uploadJSON + } + + if (error) { + console.error("No Terraform files were found, so JSON details will not be available. The graph is still usable but without all features enabled such as filtering content"); + // alert("No Terraform files were found, so JSON details will not be available. The graph is still usable but without all features enabled such as filtering content"); + } + + // if (!error) { var edges = data.edges; var svg_nodes = []; var nodes = {}; data.nodes.forEach(function (node) { if (!(node.type in resource_groups)) - console.log(node.type) - if (node.label == '[root] root') { // FIXME: w/ tf 0.11.2, resource_name not set by server. - node.resource_name = 'root'; - } + if (node.label === '[root] root') { // FIXME: w/ tf 0.11.2, resource_name not set by server. + node.resource_name = 'root'; + } node.group = (node.type in resource_groups) ? resource_groups[node.type] : -1; nodes[node['label']] = node; svg_nodes.push(node); }); + // convenient to access edges by their source. var edges_by_source = {} - for (var i in edges) { - if(edges[i].source in edges_by_source) + for (let i in edges) { + if (edges[i].source in edges_by_source) edges_by_source[edges[i].source].push(edges[i]); else edges_by_source[edges[i].source] = [edges[i]]; @@ -147,8 +389,8 @@ blastradius = function (selector, svg_url, json_url, br_state) { // convenient access to edges by their target. var edges_by_target = {} - for (var i in edges) { - if(edges[i].target in edges_by_target) + for (let i in edges) { + if (edges[i].target in edges_by_target) edges_by_target[edges[i].target].push(edges[i]); else edges_by_target[edges[i].target] = [edges[i]]; @@ -159,19 +401,19 @@ 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'); + 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)); + 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 ttip = ''; + 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) + "


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

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


"); if (deps_cbox.checked) ttip += child_html(d); return ttip; @@ -185,14 +427,13 @@ blastradius = function (selector, svg_url, json_url, br_state) { svg.call(tip); // returns
element representinga node's title and module namespace. - var title_html = function(d) { + var title_html = function (d) { var node = d; - var title = [ '
'] - if (node.modules.length <= 1 && node.modules[0] == 'root') { + var title = ['
'] + if (node.modules.length <= 1 && node.modules[0] === 'root') { title[title.length] = '' + node.type + ''; title[title.length] = '' + node.resource_name + ''; - } - else { + } else { for (var i in node.modules) { title[title.length] = '' + node.modules[i] + ''; } @@ -204,16 +445,15 @@ blastradius = function (selector, svg_url, json_url, br_state) { } // returns
element representing node's title and module namespace. - // intended for use in an interactive searchbox. - var searchbox_listing = function(d) { + // intended for use in an interactive searchbox. + var searchbox_listing = function (d) { var node = d; - var title = [ '
'] - if (node.modules.length <= 1 && node.modules[0] == 'root') { + var title = ['
'] + if (node.modules.length <= 1 && node.modules[0] === 'root') { if (node.type) title[title.length] = '' + node.type + ''; title[title.length] = '' + node.resource_name + ''; - } - else { + } else { for (var i in node.modules) { title[title.length] = '' + node.modules[i] + ''; } @@ -224,20 +464,18 @@ blastradius = function (selector, svg_url, json_url, br_state) { return title.join(''); } - // returns elements representing a node's direct children - var child_html = function(d) { + // returns elements representing a node's direct children + var child_html = function (d) { var children = []; - var edges = edges_by_source[d.label]; - //console.log(edges); + var edges = edges_by_source[d.label]; for (i in edges) { edge = edges[i]; if (edge.edge_type == edge_types.NORMAL || edge.edge_type == edge_types.HIDDEN) { var node = nodes[edge.target]; - if (node.modules.length <= 1 && node.modules[0] == 'root') { + if (node.modules.length <= 1 && node.modules[0] === 'root') { children[children.length] = '' + node.type + ''; children[children.length] = '' + node.resource_name + '
'; - } - else { + } else { for (var i in node.modules) { children[children.length] = '' + node.modules[i] + ''; } @@ -251,13 +489,13 @@ blastradius = function (selector, svg_url, json_url, br_state) { } var get_downstream_nodes = function (node) { - var children = {}; + var children = {}; children[node.label] = node; var visit_queue = new Queue(); visit_queue.enqueue(node); - while (visit_queue.size() > 0 ) { + while (visit_queue.size() > 0) { var cur_node = visit_queue.dequeue(); - var edges = edges_by_source[cur_node.label]; + var edges = edges_by_source[cur_node.label]; for (var i in edges) { if (edges[i].target in children) continue; @@ -276,7 +514,7 @@ blastradius = function (selector, svg_url, json_url, br_state) { visit_queue.enqueue(node); while (visit_queue.size() > 0) { var cur_node = visit_queue.dequeue(); - var edges = edges_by_target[cur_node.label]; + var edges = edges_by_target[cur_node.label]; for (var i in edges) { if (edges[i].source in parents) continue; @@ -288,15 +526,15 @@ blastradius = function (selector, svg_url, json_url, br_state) { return to_list(parents); } - var get_downstream_edges = function(node) { - var ret_edges = new Set(); - var children = new Set(); + var get_downstream_edges = function (node) { + var ret_edges = new Set(); + var children = new Set(); var visit_queue = new Queue(); visit_queue.enqueue(node); while (visit_queue.size() > 0) { var cur_node = visit_queue.dequeue(); - var edges = edges_by_source[cur_node.label]; + var edges = edges_by_source[cur_node.label]; for (var i in edges) { e = edges[i]; if (e in ret_edges || e.edge_type == edge_types.HIDDEN || e.edge_type == edge_types.LAYOUT_HIDDEN) @@ -310,15 +548,15 @@ blastradius = function (selector, svg_url, json_url, br_state) { return Array.from(ret_edges); } - var get_upstream_edges = function(node) { - var ret_edges = new Set(); - var parents = new Set(); + var get_upstream_edges = function (node) { + var ret_edges = new Set(); + var parents = new Set(); var visit_queue = new Queue(); visit_queue.enqueue(node); while (visit_queue.size() > 0) { var cur_node = visit_queue.dequeue(); - var edges = edges_by_target[cur_node.label]; + var edges = edges_by_target[cur_node.label]; for (var i in edges) { e = edges[i]; if (e in ret_edges || e.edge_type == edge_types.HIDDEN || e.edge_type == edge_types.LAYOUT_HIDDEN) @@ -345,19 +583,17 @@ blastradius = function (selector, svg_url, json_url, br_state) { // FIXME: these x,y,z-s pad out parameters I haven't looked up, // 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) { + 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) { + } else if (sticky_node == d && click_count === 2) { unhighlight(d); tip.hide(d); sticky_node = null; click_count = 0; - } - else { + } else { if (sticky_node) { unhighlight(sticky_node); tip.hide(sticky_node); @@ -373,30 +609,28 @@ blastradius = function (selector, svg_url, json_url, br_state) { } } - var node_mouseleave = function(d) { + var node_mouseleave = function (d) { tip.hide(d); } - var node_mouseenter = function(d) { + var node_mouseenter = function (d) { tip.show(d) .direction(tipdir(d)) .offset(tipoff(d)); } - var node_mouseover = function(d) { - if (! sticky_node) + var node_mouseover = function (d) { + if (!sticky_node) highlight(d, true, false); } - var node_mouseout = function(d) { + var node_mouseout = function (d) { if (sticky_node == d) { return; - } - else if (! sticky_node) { + } else if (!sticky_node) { unhighlight(d); - } - else { - if (click_count == 2) + } else { + if (click_count === 2) highlight(sticky_node, true, true); else highlight(sticky_node, true, false); @@ -404,11 +638,11 @@ blastradius = function (selector, svg_url, json_url, br_state) { } - var tipdir = function(d) { + var tipdir = function () { return 'n'; } - var tipoff = function(d) { + var tipoff = function () { return [-10, 0]; } @@ -418,29 +652,33 @@ blastradius = function (selector, svg_url, json_url, br_state) { var highlight_edges = []; if (downstream) { - highlight_nodes = highlight_nodes.concat(get_downstream_nodes(d)); - highlight_edges = highlight_edges.concat(get_downstream_edges(d)); + highlight_nodes = highlight_nodes.concat(get_downstream_nodes(d)); + highlight_edges = highlight_edges.concat(get_downstream_edges(d)); } if (upstream) { - highlight_nodes = highlight_nodes.concat(get_upstream_nodes(d)); - highlight_edges = highlight_edges.concat(get_upstream_edges(d)); + highlight_nodes = highlight_nodes.concat(get_upstream_nodes(d)); + highlight_edges = highlight_edges.concat(get_upstream_edges(d)); } svg.selectAll('g.node') - .data(highlight_nodes, function (d) { return (d && d.svg_id) || d3.select(this).attr("id"); }) + .data(highlight_nodes, function (d) { + return (d && d.svg_id) || d3.select(this).attr("id"); + }) .attr('opacity', 1.0) .exit() .attr('opacity', 0.2); svg.selectAll('g.edge') - .data(highlight_edges, function(d) { return d && d.svg_id || d3.select(this).attr("id"); }) + .data(highlight_edges, function (d) { + return d && d.svg_id || d3.select(this).attr("id"); + }) .attr('opacity', 1.0) .exit() .attr('opacity', 0.0); } - var unhighlight = function (d) { + var unhighlight = function () { svg.selectAll('g.node') .attr('opacity', 1.0); svg.selectAll('g.edge') @@ -449,100 +687,135 @@ 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) + if (svg_nodes) { + 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); - else - return '#000'; - })); + }) + .select('polygon:nth-last-of-type(2)') + .style('fill', (function (d) { + if (d) + return color(d.group); + else + return '#000'; + })); + } else { + console.log("SVG nodes could not be colorized, and mouse functionality could not be added either.") + } + // colorize modules svg.selectAll('polygon') - .each(function(d, i) { - if (d != undefined) - return undefined; - sibling = this.nextElementSibling; - if (sibling) { - if(sibling.innerHTML.match(/\(M\)/)) { - this.setAttribute('fill', color(sibling.innerHTML)); + .each(function (d) { + if (d != undefined) + return undefined; + sibling = this.nextElementSibling; + if (sibling) { + if (sibling.innerHTML.match(/\(M\)/)) { + this.setAttribute('fill', color(sibling.innerHTML)); + } } - } - }); + }); // hack to make mouse events and coloration work on the root node again. - var root = nodes['[root] root']; - svg.selectAll('g.node#' + root.svg_id) - .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) { - if (d) + if (nodes) { + var root = nodes['[root] root']; + + if (root == undefined) { + if (confirm("Invalid graph detected! Would you like to reload the page?") === true) { + window.location.reload() + } + } + + svg.selectAll('g.node#' + root.svg_id) + .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); - else - return '#000'; - })); + }) + .style('fill', (function (d) { + if (d) + return color(d.group); + else + return '#000'; + })); + } else { + console.warn("Mouse events and coloration may not work due to nodes being undefined.") + } // stub, in case we want to do something with edges on init. svg.selectAll('g.edge') - .data(edges, function(d) { return d && d.svg_id || d3.select(this).attr("id"); }); + .data(edges, function (d) { + return d && d.svg_id || d3.select(this).attr("id"); + }); // blast-radius --serve mode stuff. check for a zoom-in button as a proxy // for whether other facilities will be available. if (d3.select(selector + '-zoom-in')) { - var zin_btn = document.querySelector(selector + '-zoom-in'); - var zout_btn = document.querySelector(selector + '-zoom-out'); - var refocus_btn = document.querySelector(selector + '-refocus'); - var download_btn = document.querySelector(selector + '-download') - var svg_el = document.querySelector(selector + ' svg'); - var panzoom = svgPanZoom(svg_el).disableDblClickZoom(); - - console.log('bang'); - console.log(state); - if (state['no_scroll_zoom'] == true) { - console.log('bang'); + let zin_btn = document.querySelector(selector + '-zoom-in'); + let zout_btn = document.querySelector(selector + '-zoom-out'); + let refocus_btn = document.querySelector(selector + '-refocus'); + let download_btn = document.querySelector(selector + '-download'); + let upload_btn = document.querySelector(selector + "-upload"); + let upload_input = document.querySelector(selector + "-input-upload"); + let svg_el = document.querySelector(selector + ' svg'); + let print_btn = document.querySelector(selector + '-print'); + let panzoom = svgPanZoom(svg_el).disableDblClickZoom(); + let dropArea = document.querySelector(".drag-area"); + let prevNumber = parseInt($("div.tabcontent").last()[0].id.split("-")[1]); + + + if (prevNumber === 1) { + document.getElementById("tablink-1").onclick = function () { + displayTabContent(1, "#555") + } + document.getElementById("close-tab-1").onclick = function () { + closeTab(1, "#555") + } + } + + if (state['no_scroll_zoom'] === true) { panzoom.disableMouseWheelZoom(); } - var handle_zin = function(ev){ + var handle_zin = function (ev) { ev.preventDefault(); panzoom.zoomIn(); } zin_btn.addEventListener('click', handle_zin); - var handle_zout = function(ev){ + var handle_zout = function (ev) { ev.preventDefault(); panzoom.zoomOut(); } zout_btn.addEventListener('click', handle_zout); - var handle_refocus = function() { + var handle_refocus = function () { if (sticky_node) { $(selector + ' svg').remove(); clear_listeners(); - if (! state['params']) + if (!state['params']) { state.params = {} + } state.params.refocus = encodeURIComponent(sticky_node.label); - svg_url = svg_url.split('?')[0]; + svg_url = svg_url.split('?')[0]; json_url = json_url.split('?')[0]; - blastradius(selector, build_uri(svg_url, state.params), build_uri(json_url, state.params), br_state); + blastradius(selector, build_uri(svg_url, state.params), build_uri(json_url, state.params), br_state, uploadXML, uploadJSON); } } @@ -551,13 +824,13 @@ blastradius = function (selector, svg_url, json_url, br_state) { refocus_btn.addEventListener('click', handle_refocus); } - var handle_download = function() { + var handle_download = function () { // svg extraction and download as data url borrowed from // http://bl.ocks.org/curran/7cf9967028259ea032e8 - var svg_el = document.querySelector(selector + ' svg') - var svg_as_xml = (new XMLSerializer).serializeToString(svg_el); - var svg_data_url = "data:image/svg+xml," + encodeURIComponent(svg_as_xml); - var dl = document.createElement("a"); + var svg_el = document.querySelector(selector + ' svg') + var svg_as_xml = (new XMLSerializer).serializeToString(svg_el); + var svg_data_url = "data:image/svg+xml," + encodeURIComponent(svg_as_xml); + var dl = document.createElement("a"); document.body.appendChild(dl); dl.setAttribute("href", svg_data_url); dl.setAttribute("download", "blast-radius.svg"); @@ -565,23 +838,81 @@ blastradius = function (selector, svg_url, json_url, br_state) { } download_btn.addEventListener('click', handle_download); - var clear_listeners = function() { + upload_btn.onclick = () => { + upload_input.click() + } + + dropArea.addEventListener("dragover", (event) => { + console.log("dragover") + event.preventDefault(); //preventing from default behaviour + dropArea.classList.add("active"); + }, { + once: true + }); + + let handle_dragleave = () => { + console.log("dragleave") + dropArea.classList.remove("active"); + } + dropArea.addEventListener("dragleave", handle_dragleave, { + once: true + }); + + dropArea.addEventListener("drop", function (event) { + console.log("drop") + event.preventDefault(); //preventing from default behaviour + event.stopImmediatePropagation(); + //getting user select file and [0] this means if user select multiple files then we'll select only the first one + let file = event.dataTransfer.files[0]; + + handle_upload(file, file.name); + }, { + once: true, + passive: false + }); + + var handle_upload = async (file, filename) => { + let prevNumber = parseInt($("div.tabcontent").last()[0].id.split("-")[1]) + insertTabContent(prevNumber) + curNumber = parseInt(prevNumber) + 1 + await createTab(filename, curNumber) + await uploadFile(file, curNumber); + $('#tablink-' + curNumber).click(); + } + + var handle_upload_input = async function () { + let file = this.files[0]; + dropArea.classList.add("active"); + handle_upload(file, file.name); + } + upload_input.addEventListener('change', handle_upload_input, { + once: true + }); + + var handle_print = function () { + window.print(); + } + print_btn.addEventListener('click', handle_print) + + var clear_listeners = function () { zin_btn.removeEventListener('click', handle_zin); zout_btn.removeEventListener('click', handle_zout); refocus_btn.removeEventListener('click', handle_refocus); download_btn.removeEventListener('click', handle_download); + upload_input.removeEventListener('click', handle_upload); + print_btn.removeEventListener('click', handle_print) panzoom = null; // tip.hide(); } - var render_searchbox_node = function(d) { + var render_searchbox_node = function (d) { return searchbox_listing(d); } - - var select_node = function(d) { - if (d === undefined || d.length == 0) { + + var select_node = function (d) { + if (d === undefined || d.length === 0) { return true; } // FIXME: these falses pad out parameters I haven't looked up, @@ -594,23 +925,23 @@ blastradius = function (selector, svg_url, json_url, br_state) { node_mousedown(nodes[d], false, false, false, true); } - if ( $(selector + '-search.selectized').length > 0 ) { + if ($(selector + '-search.selectized').length > 0) { $(selector + '-search').selectize()[0].selectize.clear(); $(selector + '-search').selectize()[0].selectize.clearOptions(); - for (var i in svg_nodes) { - //console.log(svg_nodes[i]); + + for (let i in svg_nodes) { $(selector + '-search').selectize()[0].selectize.addOption(svg_nodes[i]); } - if( state.params.refocus && state.params.refocus.length > 0 ) { - var n = state.params.refocus; - } + // if (state.params.refocus && state.params.refocus.length > 0) { + // var n = state.params.refocus; + // } // 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(); - } - else { + + } else { $(selector + '-search').selectize({ valueField: 'label', searchField: ['label'], @@ -621,7 +952,7 @@ blastradius = function (selector, svg_url, json_url, br_state) { onChange: select_node, render: { option: render_searchbox_node, - item : render_searchbox_node + item: render_searchbox_node }, options: svg_nodes }); @@ -629,11 +960,12 @@ blastradius = function (selector, svg_url, json_url, br_state) { // without this, selecting an item with will submit the form // and force a page refresh. not the desired behavior. - $(selector + '-search-form').submit(function(){return false;}); + $(selector + '-search-form').submit(function () { + return false; + }); } // end if(interactive) - }); // end json success callback - }); // end svg scuccess callback - -} // end blastradius() + }); // end json success callback + }); // end svg success callback +} // end blastradius() \ No newline at end of file diff --git a/blastradius/server/templates/error.html b/blastradius/server/templates/error.html index ea97c1f..95b911f 100644 --- a/blastradius/server/templates/error.html +++ b/blastradius/server/templates/error.html @@ -1,22 +1,22 @@ - - Terraform Graph Tools - - - + + Terraform Graph Tools + + + - -

Error.

-

Something has gone wrong. Please check the following:

-
    -
  • Is Graphviz installed?
  • -
  • Is Terraform installed?
  • -
  • Is this an init-ed Terraform project?
  • -
- + +

Error.

+

Something has gone wrong. Please check the following:

+
    +
  • Graphviz installed: {{gviz_install}}
  • +
  • Terraform installed: {{tf_install}}
  • +
  • init-ed Terraform project: {{tf_dir}}
  • +
+ \ No newline at end of file diff --git a/blastradius/server/templates/index.html b/blastradius/server/templates/index.html index f39e933..185c384 100644 --- a/blastradius/server/templates/index.html +++ b/blastradius/server/templates/index.html @@ -1,80 +1,134 @@ - - Blast Radius (Terraform Graph Tools) - - - - - - - - - - - - - - - - - - - - - + + + + +
+
+
+ +
+ + +
+ --> +
- + + + @@ -135,19 +203,29 @@
- - -
- - - - + + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/blastradius/server/templates/non_tf_dir.html b/blastradius/server/templates/non_tf_dir.html new file mode 100644 index 0000000..b99f541 --- /dev/null +++ b/blastradius/server/templates/non_tf_dir.html @@ -0,0 +1,193 @@ + + + + Blast Radius (Terraform Graph Tools) + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + +
+ + + + +
+ + +
+ +
+ +
+ + + + + + \ No newline at end of file diff --git a/blastradius/util.py b/blastradius/util.py index 27984c5..882e58e 100644 --- a/blastradius/util.py +++ b/blastradius/util.py @@ -1,6 +1,10 @@ import re import os -import collections + +try: # works in Python >= 3.3 + import collections.abc as collections +except ImportError: # Python <= 3.2 including Python 2 + import collections as collections class Re: '''A bit of a hack to simplify conditionals with diff --git a/doc/blastradius-interactive.png b/doc/blastradius-interactive.png index 27e851e..9f63969 100644 Binary files a/doc/blastradius-interactive.png and b/doc/blastradius-interactive.png differ diff --git a/doc/embedded.md b/doc/embedded.md index c66c462..f21466d 100644 --- a/doc/embedded.md +++ b/doc/embedded.md @@ -2,7 +2,7 @@ You may wish to embed figures produced with *Blast Radius* in other documents. You will need the following: - 1. an `svg` file and `json` document representing the graph and its layout. These are produced with *Blast Radius*, as follows + 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 @@ -28,4 +28,4 @@ blastradius('#graph', '/graph.svg', '/graph.json'); ```` -That's it. Ideas to simplify this process strongly desired. \ No newline at end of file +That's it. Ideas to simplify this process are strongly desired. \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ba6ca9d..4e59377 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -9,8 +9,8 @@ if [ "${1}" != "blast-radius" ]; then fi # Assert CLI args are overwritten, otherwise set them to preferred defaults -export TF_CLI_ARGS_get=${TF_CLI_ARGS_get:'-update'} -export TF_CLI_ARGS_init=${TF_CLI_ARGS_init:'-input=false'} +export TF_CLI_ARGS_get=${TF_CLI_ARGS_get:-'-update'} +export TF_CLI_ARGS_init=${TF_CLI_ARGS_init:-'-input=false'} # Inside the container # Need to create the upper and work dirs inside a tmpfs. @@ -30,10 +30,16 @@ cd /data-rw [ -d '.terraform' ] && terraform get # Reinitialize for some reason -terraform init +if [ -n "$CHDIR" ] && [ -d "$CHDIR" ]; then + terraform -chdir="$CHDIR" init + echo "Initializing Terraform in directory: $CHDIR" +else + terraform init +fi # it's possible that we're in a sub-directory. leave. cd /data-rw +cat /output.txt # Let's go! exec "$@" diff --git a/examples/Example1.txt b/examples/Example1.txt new file mode 100644 index 0000000..45902f5 --- /dev/null +++ b/examples/Example1.txt @@ -0,0 +1,48 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] aws_instance.ec2_instance_one (expand)" [label = "aws_instance.ec2_instance_one", shape = "box"] + "[root] aws_internet_gateway.igw (expand)" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_main_route_table_association.main-rt (expand)" [label = "aws_main_route_table_association.main-rt", shape = "box"] + "[root] aws_route.mgmt-default (expand)" [label = "aws_route.mgmt-default", shape = "box"] + "[root] aws_route_table.mgmt-rt (expand)" [label = "aws_route_table.mgmt-rt", shape = "box"] + "[root] aws_s3_bucket.bucket1 (expand)" [label = "aws_s3_bucket.bucket1", shape = "box"] + "[root] aws_s3_bucket_server_side_encryption_configuration.aesbucket1 (expand)" [label = "aws_s3_bucket_server_side_encryption_configuration.aesbucket1", shape = "box"] + "[root] aws_security_group.sg (expand)" [label = "aws_security_group.sg", shape = "box"] + "[root] aws_subnet.first (expand)" [label = "aws_subnet.first", shape = "box"] + "[root] aws_vpc.terraformvpc1 (expand)" [label = "aws_vpc.terraformvpc1", shape = "box"] + "[root] output.ec1-public-ip" [label = "output.ec1-public-ip", shape = "note"] + "[root] output.vpc-id" [label = "output.vpc-id", shape = "note"] + "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"] + "[root] var.AWS_ACCESS_KEY_ID" [label = "var.AWS_ACCESS_KEY_ID", shape = "note"] + "[root] var.AWS_REGION" [label = "var.AWS_REGION", shape = "note"] + "[root] var.AWS_SECRET_ACCESS_KEY" [label = "var.AWS_SECRET_ACCESS_KEY", shape = "note"] + "[root] var.FRANKFURTKEY" [label = "var.FRANKFURTKEY", shape = "note"] + "[root] aws_instance.ec2_instance_one (expand)" -> "[root] aws_security_group.sg (expand)" + "[root] aws_internet_gateway.igw (expand)" -> "[root] aws_vpc.terraformvpc1 (expand)" + "[root] aws_main_route_table_association.main-rt (expand)" -> "[root] aws_route_table.mgmt-rt (expand)" + "[root] aws_route.mgmt-default (expand)" -> "[root] aws_internet_gateway.igw (expand)" + "[root] aws_route.mgmt-default (expand)" -> "[root] aws_route_table.mgmt-rt (expand)" + "[root] aws_route_table.mgmt-rt (expand)" -> "[root] aws_vpc.terraformvpc1 (expand)" + "[root] aws_s3_bucket.bucket1 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_s3_bucket_server_side_encryption_configuration.aesbucket1 (expand)" -> "[root] aws_s3_bucket.bucket1 (expand)" + "[root] aws_security_group.sg (expand)" -> "[root] aws_subnet.first (expand)" + "[root] aws_security_group.sg (expand)" -> "[root] local.rulesmap (expand)" + "[root] aws_subnet.first (expand)" -> "[root] aws_vpc.terraformvpc1 (expand)" + "[root] aws_subnet.first (expand)" -> "[root] var.AWS_REGION" + "[root] aws_vpc.terraformvpc1 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] output.ec1-public-ip" -> "[root] aws_instance.ec2_instance_one (expand)" + "[root] output.vpc-id" -> "[root] aws_vpc.terraformvpc1 (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.ec2_instance_one (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_main_route_table_association.main-rt (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.mgmt-default (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_s3_bucket_server_side_encryption_configuration.aesbucket1 (expand)" + "[root] root" -> "[root] output.ec1-public-ip" + "[root] root" -> "[root] output.vpc-id" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" + "[root] root" -> "[root] var.AWS_ACCESS_KEY_ID" + "[root] root" -> "[root] var.AWS_SECRET_ACCESS_KEY" + "[root] root" -> "[root] var.KEY" + } +} \ No newline at end of file diff --git a/examples/Example2.txt b/examples/Example2.txt new file mode 100644 index 0000000..78f067c --- /dev/null +++ b/examples/Example2.txt @@ -0,0 +1,43 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] aws_instance.BlastRadiusTest (expand)" [label = "aws_instance.BlastRadiusTest", shape = "box"] + "[root] aws_internet_gateway.igw (expand)" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_main_route_table_association.rtAssoc (expand)" [label = "aws_main_route_table_association.rtAssoc", shape = "box"] + "[root] aws_route.route1 (expand)" [label = "aws_route.route1", shape = "box"] + "[root] aws_route_table.routeTable (expand)" [label = "aws_route_table.routeTable", shape = "box"] + "[root] aws_security_group.BRS (expand)" [label = "aws_security_group.BRS", shape = "box"] + "[root] aws_subnet.first-subnet (expand)" [label = "aws_subnet.first-subnet", shape = "box"] + "[root] aws_vpc.main (expand)" [label = "aws_vpc.main", shape = "box"] + "[root] output.ec2-public-dns" [label = "output.ec2-public-dns", shape = "note"] + "[root] output.ec2-public-ip" [label = "output.ec2-public-ip", shape = "note"] + "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"] + "[root] var.amiId" [label = "var.amiId", shape = "note"] + "[root] var.keyName" [label = "var.keyName", shape = "note"] + "[root] var.keyPath" [label = "var.keyPath", shape = "note"] + "[root] var.tfvars" [label = "var.tfvars", shape = "note"] + "[root] aws_instance.BlastRadiusTest (expand)" -> "[root] aws_security_group.BRS (expand)" + "[root] aws_instance.BlastRadiusTest (expand)" -> "[root] aws_subnet.first-subnet (expand)" + "[root] aws_instance.BlastRadiusTest (expand)" -> "[root] var.amiId" + "[root] aws_instance.BlastRadiusTest (expand)" -> "[root] var.keyName" + "[root] aws_instance.BlastRadiusTest (expand)" -> "[root] var.keyPath" + "[root] aws_internet_gateway.igw (expand)" -> "[root] aws_vpc.main (expand)" + "[root] aws_main_route_table_association.rtAssoc (expand)" -> "[root] aws_route_table.routeTable (expand)" + "[root] aws_route.route1 (expand)" -> "[root] aws_internet_gateway.igw (expand)" + "[root] aws_route.route1 (expand)" -> "[root] aws_route_table.routeTable (expand)" + "[root] aws_route_table.routeTable (expand)" -> "[root] aws_vpc.main (expand)" + "[root] aws_security_group.BRS (expand)" -> "[root] aws_vpc.main (expand)" + "[root] aws_subnet.first-subnet (expand)" -> "[root] aws_vpc.main (expand)" + "[root] aws_vpc.main (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] output.ec2-public-dns" -> "[root] aws_instance.BlastRadiusTest (expand)" + "[root] output.ec2-public-ip" -> "[root] aws_instance.BlastRadiusTest (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.BlastRadiusTest (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_main_route_table_association.rtAssoc (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.route1 (expand)" + "[root] root" -> "[root] output.ec2-public-dns" + "[root] root" -> "[root] output.ec2-public-ip" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" + "[root] root" -> "[root] var.tfvars" + } +} \ No newline at end of file diff --git a/examples/Example3.txt b/examples/Example3.txt new file mode 100644 index 0000000..8a4f130 --- /dev/null +++ b/examples/Example3.txt @@ -0,0 +1,410 @@ +digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] aws_ec2_transit_gateway.central-tgw" [label = "aws_ec2_transit_gateway.central-tgw", shape = "box"] + "[root] aws_ec2_transit_gateway.central-tgw (expand)" [label = "aws_ec2_transit_gateway.central-tgw", shape = "box"] + "[root] aws_ec2_transit_gateway.west-tgw" [label = "aws_ec2_transit_gateway.west-tgw", shape = "box"] + "[root] aws_ec2_transit_gateway.west-tgw (expand)" [label = "aws_ec2_transit_gateway.west-tgw", shape = "box"] + "[root] aws_ec2_transit_gateway_peering_attachment.central-west" [label = "aws_ec2_transit_gateway_peering_attachment.central-west", shape = "box"] + "[root] aws_ec2_transit_gateway_peering_attachment.central-west (expand)" [label = "aws_ec2_transit_gateway_peering_attachment.central-west", shape = "box"] + "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west" [label = "aws_ec2_transit_gateway_peering_attachment_accepter.west", shape = "box"] + "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west (expand)" [label = "aws_ec2_transit_gateway_peering_attachment_accepter.west", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_20" [label = "aws_ec2_transit_gateway_route.central-to-10_20", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_20 (expand)" [label = "aws_ec2_transit_gateway_route.central-to-10_20", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_30" [label = "aws_ec2_transit_gateway_route.central-to-10_30", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_30 (expand)" [label = "aws_ec2_transit_gateway_route.central-to-10_30", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_99" [label = "aws_ec2_transit_gateway_route.central-to-10_99", shape = "box"] + "[root] aws_ec2_transit_gateway_route.central-to-10_99 (expand)" [label = "aws_ec2_transit_gateway_route.central-to-10_99", shape = "box"] + "[root] aws_ec2_transit_gateway_route.west-to-10_00" [label = "aws_ec2_transit_gateway_route.west-to-10_00", shape = "box"] + "[root] aws_ec2_transit_gateway_route.west-to-10_00 (expand)" [label = "aws_ec2_transit_gateway_route.west-to-10_00", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table.central-rt" [label = "aws_ec2_transit_gateway_route_table.central-rt", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table.central-rt (expand)" [label = "aws_ec2_transit_gateway_route_table.central-rt", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table.west-rt" [label = "aws_ec2_transit_gateway_route_table.west-rt", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table.west-rt (expand)" [label = "aws_ec2_transit_gateway_route_table.west-rt", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc" [label = "aws_ec2_transit_gateway_route_table_association.central-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc (expand)" [label = "aws_ec2_transit_gateway_route_table_association.central-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc" [label = "aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc (expand)" [label = "aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc" [label = "aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc (expand)" [label = "aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc" [label = "aws_ec2_transit_gateway_route_table_association.west-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc (expand)" [label = "aws_ec2_transit_gateway_route_table_association.west-rtassoc", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag" [label = "aws_ec2_transit_gateway_route_table_propagation.central-propag", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag (expand)" [label = "aws_ec2_transit_gateway_route_table_propagation.central-propag", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag" [label = "aws_ec2_transit_gateway_route_table_propagation.west-propag", shape = "box"] + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag (expand)" [label = "aws_ec2_transit_gateway_route_table_propagation.west-propag", shape = "box"] + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" [label = "aws_ec2_transit_gateway_vpc_attachment.central-hub", shape = "box"] + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub (expand)" [label = "aws_ec2_transit_gateway_vpc_attachment.central-hub", shape = "box"] + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" [label = "aws_ec2_transit_gateway_vpc_attachment.west-remote", shape = "box"] + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote (expand)" [label = "aws_ec2_transit_gateway_vpc_attachment.west-remote", shape = "box"] + "[root] aws_instance.ec2 (expand)" [label = "aws_instance.ec2", shape = "box"] + "[root] aws_instance.ec2[0]" [label = "aws_instance.ec2", shape = "box"] + "[root] aws_instance.ec2[1]" [label = "aws_instance.ec2", shape = "box"] + "[root] aws_instance.ec2[2]" [label = "aws_instance.ec2", shape = "box"] + "[root] aws_instance.ec2remote" [label = "aws_instance.ec2remote", shape = "box"] + "[root] aws_instance.ec2remote (expand)" [label = "aws_instance.ec2remote", shape = "box"] + "[root] aws_internet_gateway.igw (expand)" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_internet_gateway.igw[\"0\"]" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_internet_gateway.igw[\"1\"]" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_internet_gateway.igw[\"2\"]" [label = "aws_internet_gateway.igw", shape = "box"] + "[root] aws_internet_gateway.remoteigw" [label = "aws_internet_gateway.remoteigw", shape = "box"] + "[root] aws_internet_gateway.remoteigw (expand)" [label = "aws_internet_gateway.remoteigw", shape = "box"] + "[root] aws_main_route_table_association.remotertassoc" [label = "aws_main_route_table_association.remotertassoc", shape = "box"] + "[root] aws_main_route_table_association.remotertassoc (expand)" [label = "aws_main_route_table_association.remotertassoc", shape = "box"] + "[root] aws_main_route_table_association.rtassoc (expand)" [label = "aws_main_route_table_association.rtassoc", shape = "box"] + "[root] aws_main_route_table_association.rtassoc[0]" [label = "aws_main_route_table_association.rtassoc", shape = "box"] + "[root] aws_main_route_table_association.rtassoc[1]" [label = "aws_main_route_table_association.rtassoc", shape = "box"] + "[root] aws_main_route_table_association.rtassoc[2]" [label = "aws_main_route_table_association.rtassoc", shape = "box"] + "[root] aws_route.defaultRoutes (expand)" [label = "aws_route.defaultRoutes", shape = "box"] + "[root] aws_route.defaultRoutes[0]" [label = "aws_route.defaultRoutes", shape = "box"] + "[root] aws_route.defaultRoutes[1]" [label = "aws_route.defaultRoutes", shape = "box"] + "[root] aws_route.defaultRoutes[2]" [label = "aws_route.defaultRoutes", shape = "box"] + "[root] aws_route.hubToRemote" [label = "aws_route.hubToRemote", shape = "box"] + "[root] aws_route.hubToRemote (expand)" [label = "aws_route.hubToRemote", shape = "box"] + "[root] aws_route.remoteDefaultRoute" [label = "aws_route.remoteDefaultRoute", shape = "box"] + "[root] aws_route.remoteDefaultRoute (expand)" [label = "aws_route.remoteDefaultRoute", shape = "box"] + "[root] aws_route.remoteRoutes" [label = "aws_route.remoteRoutes", shape = "box"] + "[root] aws_route.remoteRoutes (expand)" [label = "aws_route.remoteRoutes", shape = "box"] + "[root] aws_route.vpcRoutes (expand)" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"0\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"1\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"2\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"3\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"4\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route.vpcRoutes[\"5\"]" [label = "aws_route.vpcRoutes", shape = "box"] + "[root] aws_route_table.remotert" [label = "aws_route_table.remotert", shape = "box"] + "[root] aws_route_table.remotert (expand)" [label = "aws_route_table.remotert", shape = "box"] + "[root] aws_route_table.rt (expand)" [label = "aws_route_table.rt", shape = "box"] + "[root] aws_route_table.rt[\"0\"]" [label = "aws_route_table.rt", shape = "box"] + "[root] aws_route_table.rt[\"1\"]" [label = "aws_route_table.rt", shape = "box"] + "[root] aws_route_table.rt[\"2\"]" [label = "aws_route_table.rt", shape = "box"] + "[root] aws_security_group.remotesg" [label = "aws_security_group.remotesg", shape = "box"] + "[root] aws_security_group.remotesg (expand)" [label = "aws_security_group.remotesg", shape = "box"] + "[root] aws_security_group.sg (expand)" [label = "aws_security_group.sg", shape = "box"] + "[root] aws_security_group.sg[\"0\"]" [label = "aws_security_group.sg", shape = "box"] + "[root] aws_security_group.sg[\"1\"]" [label = "aws_security_group.sg", shape = "box"] + "[root] aws_security_group.sg[\"2\"]" [label = "aws_security_group.sg", shape = "box"] + "[root] aws_subnet.remotesubnet" [label = "aws_subnet.remotesubnet", shape = "box"] + "[root] aws_subnet.remotesubnet (expand)" [label = "aws_subnet.remotesubnet", shape = "box"] + "[root] aws_subnet.subnet (expand)" [label = "aws_subnet.subnet", shape = "box"] + "[root] aws_subnet.subnet[\"0\"]" [label = "aws_subnet.subnet", shape = "box"] + "[root] aws_subnet.subnet[\"1\"]" [label = "aws_subnet.subnet", shape = "box"] + "[root] aws_subnet.subnet[\"2\"]" [label = "aws_subnet.subnet", shape = "box"] + "[root] aws_vpc.myvpc (expand)" [label = "aws_vpc.myvpc", shape = "box"] + "[root] aws_vpc.myvpc[\"0\"]" [label = "aws_vpc.myvpc", shape = "box"] + "[root] aws_vpc.myvpc[\"1\"]" [label = "aws_vpc.myvpc", shape = "box"] + "[root] aws_vpc.myvpc[\"2\"]" [label = "aws_vpc.myvpc", shape = "box"] + "[root] aws_vpc.remotevpc" [label = "aws_vpc.remotevpc", shape = "box"] + "[root] aws_vpc.remotevpc (expand)" [label = "aws_vpc.remotevpc", shape = "box"] + "[root] aws_vpc_peering_connection.eastToHub" [label = "aws_vpc_peering_connection.eastToHub", shape = "box"] + "[root] aws_vpc_peering_connection.eastToHub (expand)" [label = "aws_vpc_peering_connection.eastToHub", shape = "box"] + "[root] aws_vpc_peering_connection.eastToWest" [label = "aws_vpc_peering_connection.eastToWest", shape = "box"] + "[root] aws_vpc_peering_connection.eastToWest (expand)" [label = "aws_vpc_peering_connection.eastToWest", shape = "box"] + "[root] aws_vpc_peering_connection.westToHub" [label = "aws_vpc_peering_connection.westToHub", shape = "box"] + "[root] aws_vpc_peering_connection.westToHub (expand)" [label = "aws_vpc_peering_connection.westToHub", shape = "box"] + "[root] data.aws_ami.amazon_linux (expand)" [label = "data.aws_ami.amazon_linux", shape = "box"] + "[root] data.aws_ami.amazon_linux_remote (expand)" [label = "data.aws_ami.amazon_linux_remote", shape = "box"] + "[root] data.aws_region.peer (expand)" [label = "data.aws_region.peer", shape = "box"] + "[root] output.instance_ip_addresses" [label = "output.instance_ip_addresses", shape = "note"] + "[root] output.instance_pubip_addresses" [label = "output.instance_pubip_addresses", shape = "note"] + "[root] output.remoteInstance_ip_address" [label = "output.remoteInstance_ip_address", shape = "note"] + "[root] output.remoteInstance_pubip_address" [label = "output.remoteInstance_pubip_address", shape = "note"] + "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"] + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" [label = "provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1", shape = "diamond"] + "[root] aws_ec2_transit_gateway.central-tgw (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway.central-tgw" -> "[root] aws_ec2_transit_gateway.central-tgw (expand)" + "[root] aws_ec2_transit_gateway.west-tgw (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway.west-tgw" -> "[root] aws_ec2_transit_gateway.west-tgw (expand)" + "[root] aws_ec2_transit_gateway_peering_attachment.central-west (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_peering_attachment.central-west" -> "[root] aws_ec2_transit_gateway.central-tgw" + "[root] aws_ec2_transit_gateway_peering_attachment.central-west" -> "[root] aws_ec2_transit_gateway.west-tgw" + "[root] aws_ec2_transit_gateway_peering_attachment.central-west" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west (expand)" + "[root] aws_ec2_transit_gateway_peering_attachment.central-west" -> "[root] data.aws_region.peer (expand)" + "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west" + "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west" -> "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west (expand)" + "[root] aws_ec2_transit_gateway_route.central-to-10_20 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route.central-to-10_20" -> "[root] aws_ec2_transit_gateway_route.central-to-10_20 (expand)" + "[root] aws_ec2_transit_gateway_route.central-to-10_20" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route.central-to-10_20" -> "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" + "[root] aws_ec2_transit_gateway_route.central-to-10_30 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route.central-to-10_30" -> "[root] aws_ec2_transit_gateway_route.central-to-10_30 (expand)" + "[root] aws_ec2_transit_gateway_route.central-to-10_30" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route.central-to-10_30" -> "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" + "[root] aws_ec2_transit_gateway_route.central-to-10_99 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route.central-to-10_99" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west" + "[root] aws_ec2_transit_gateway_route.central-to-10_99" -> "[root] aws_ec2_transit_gateway_route.central-to-10_99 (expand)" + "[root] aws_ec2_transit_gateway_route.central-to-10_99" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route.west-to-10_00 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_route.west-to-10_00" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west" + "[root] aws_ec2_transit_gateway_route.west-to-10_00" -> "[root] aws_ec2_transit_gateway_route.west-to-10_00 (expand)" + "[root] aws_ec2_transit_gateway_route.west-to-10_00" -> "[root] aws_ec2_transit_gateway_route_table.west-rt" + "[root] aws_ec2_transit_gateway_route_table.central-rt (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route_table.central-rt" -> "[root] aws_ec2_transit_gateway.central-tgw" + "[root] aws_ec2_transit_gateway_route_table.central-rt" -> "[root] aws_ec2_transit_gateway_route_table.central-rt (expand)" + "[root] aws_ec2_transit_gateway_route_table.west-rt (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_route_table.west-rt" -> "[root] aws_ec2_transit_gateway.west-tgw" + "[root] aws_ec2_transit_gateway_route_table.west-rt" -> "[root] aws_ec2_transit_gateway_route_table.west-rt (expand)" + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc (expand)" + "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc" -> "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west" + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc (expand)" + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc" -> "[root] aws_ec2_transit_gateway_peering_attachment.central-west" + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table.west-rt" + "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc (expand)" + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table.west-rt" + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc" -> "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc (expand)" + "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc" -> "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag" -> "[root] aws_ec2_transit_gateway_route_table.central-rt" + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag" -> "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag (expand)" + "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag" -> "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag" -> "[root] aws_ec2_transit_gateway_route_table.west-rt" + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag" -> "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag (expand)" + "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag" -> "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" -> "[root] aws_ec2_transit_gateway.central-tgw" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" -> "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub (expand)" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" -> "[root] aws_subnet.subnet[\"0\"]" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" -> "[root] aws_subnet.subnet[\"1\"]" + "[root] aws_ec2_transit_gateway_vpc_attachment.central-hub" -> "[root] aws_subnet.subnet[\"2\"]" + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" -> "[root] aws_ec2_transit_gateway.west-tgw" + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" -> "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote (expand)" + "[root] aws_ec2_transit_gateway_vpc_attachment.west-remote" -> "[root] aws_subnet.remotesubnet" + "[root] aws_instance.ec2 (expand)" -> "[root] local.vpcs (expand)" + "[root] aws_instance.ec2 (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_instance.ec2 (expand)" + "[root] aws_instance.ec2[0]" -> "[root] aws_security_group.sg[\"0\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_security_group.sg[\"1\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_security_group.sg[\"2\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_subnet.subnet[\"0\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_subnet.subnet[\"1\"]" + "[root] aws_instance.ec2[0]" -> "[root] aws_subnet.subnet[\"2\"]" + "[root] aws_instance.ec2[0]" -> "[root] data.aws_ami.amazon_linux (expand)" + "[root] aws_instance.ec2[1]" -> "[root] aws_instance.ec2 (expand)" + "[root] aws_instance.ec2[1]" -> "[root] aws_security_group.sg[\"0\"]" + "[root] aws_instance.ec2[1]" -> "[root] aws_security_group.sg[\"1\"]" + "[root] aws_instance.ec2[1]" -> "[root] aws_security_group.sg[\"2\"]" + "[root] aws_instance.ec2[1]" -> "[root] aws_subnet.subnet[\"0\"]" + "[root] aws_instance.ec2[1]" -> "[root] aws_subnet.subnet[\"1\"]" + "[root] aws_instance.ec2[1]" -> "[root] aws_subnet.subnet[\"2\"]" + "[root] aws_instance.ec2[1]" -> "[root] data.aws_ami.amazon_linux (expand)" + "[root] aws_instance.ec2[2]" -> "[root] aws_instance.ec2 (expand)" + "[root] aws_instance.ec2[2]" -> "[root] aws_security_group.sg[\"0\"]" + "[root] aws_instance.ec2[2]" -> "[root] aws_security_group.sg[\"1\"]" + "[root] aws_instance.ec2[2]" -> "[root] aws_security_group.sg[\"2\"]" + "[root] aws_instance.ec2[2]" -> "[root] aws_subnet.subnet[\"0\"]" + "[root] aws_instance.ec2[2]" -> "[root] aws_subnet.subnet[\"1\"]" + "[root] aws_instance.ec2[2]" -> "[root] aws_subnet.subnet[\"2\"]" + "[root] aws_instance.ec2[2]" -> "[root] data.aws_ami.amazon_linux (expand)" + "[root] aws_instance.ec2remote (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_instance.ec2remote" -> "[root] aws_instance.ec2remote (expand)" + "[root] aws_instance.ec2remote" -> "[root] aws_security_group.remotesg" + "[root] aws_instance.ec2remote" -> "[root] aws_subnet.remotesubnet" + "[root] aws_instance.ec2remote" -> "[root] data.aws_ami.amazon_linux_remote (expand)" + "[root] aws_internet_gateway.igw (expand)" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_internet_gateway.igw (expand)" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_internet_gateway.igw (expand)" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_internet_gateway.igw[\"0\"]" -> "[root] aws_internet_gateway.igw (expand)" + "[root] aws_internet_gateway.igw[\"1\"]" -> "[root] aws_internet_gateway.igw (expand)" + "[root] aws_internet_gateway.igw[\"2\"]" -> "[root] aws_internet_gateway.igw (expand)" + "[root] aws_internet_gateway.remoteigw (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_internet_gateway.remoteigw" -> "[root] aws_internet_gateway.remoteigw (expand)" + "[root] aws_internet_gateway.remoteigw" -> "[root] aws_vpc.remotevpc" + "[root] aws_main_route_table_association.remotertassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_main_route_table_association.remotertassoc" -> "[root] aws_main_route_table_association.remotertassoc (expand)" + "[root] aws_main_route_table_association.remotertassoc" -> "[root] aws_route_table.remotert" + "[root] aws_main_route_table_association.rtassoc (expand)" -> "[root] local.subnets (expand)" + "[root] aws_main_route_table_association.rtassoc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_main_route_table_association.rtassoc[0]" -> "[root] aws_main_route_table_association.rtassoc (expand)" + "[root] aws_main_route_table_association.rtassoc[0]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_main_route_table_association.rtassoc[0]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_main_route_table_association.rtassoc[0]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_main_route_table_association.rtassoc[1]" -> "[root] aws_main_route_table_association.rtassoc (expand)" + "[root] aws_main_route_table_association.rtassoc[1]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_main_route_table_association.rtassoc[1]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_main_route_table_association.rtassoc[1]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_main_route_table_association.rtassoc[2]" -> "[root] aws_main_route_table_association.rtassoc (expand)" + "[root] aws_main_route_table_association.rtassoc[2]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_main_route_table_association.rtassoc[2]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_main_route_table_association.rtassoc[2]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.defaultRoutes (expand)" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.defaultRoutes (expand)" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.defaultRoutes (expand)" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.defaultRoutes[0]" -> "[root] aws_internet_gateway.igw[\"0\"]" + "[root] aws_route.defaultRoutes[0]" -> "[root] aws_internet_gateway.igw[\"1\"]" + "[root] aws_route.defaultRoutes[0]" -> "[root] aws_internet_gateway.igw[\"2\"]" + "[root] aws_route.defaultRoutes[0]" -> "[root] aws_route.defaultRoutes (expand)" + "[root] aws_route.defaultRoutes[1]" -> "[root] aws_internet_gateway.igw[\"0\"]" + "[root] aws_route.defaultRoutes[1]" -> "[root] aws_internet_gateway.igw[\"1\"]" + "[root] aws_route.defaultRoutes[1]" -> "[root] aws_internet_gateway.igw[\"2\"]" + "[root] aws_route.defaultRoutes[1]" -> "[root] aws_route.defaultRoutes (expand)" + "[root] aws_route.defaultRoutes[2]" -> "[root] aws_internet_gateway.igw[\"0\"]" + "[root] aws_route.defaultRoutes[2]" -> "[root] aws_internet_gateway.igw[\"1\"]" + "[root] aws_route.defaultRoutes[2]" -> "[root] aws_internet_gateway.igw[\"2\"]" + "[root] aws_route.defaultRoutes[2]" -> "[root] aws_route.defaultRoutes (expand)" + "[root] aws_route.hubToRemote (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_route.hubToRemote" -> "[root] aws_ec2_transit_gateway.central-tgw" + "[root] aws_route.hubToRemote" -> "[root] aws_route.hubToRemote (expand)" + "[root] aws_route.hubToRemote" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.hubToRemote" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.hubToRemote" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.remoteDefaultRoute (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_route.remoteDefaultRoute" -> "[root] aws_internet_gateway.remoteigw" + "[root] aws_route.remoteDefaultRoute" -> "[root] aws_route.remoteDefaultRoute (expand)" + "[root] aws_route.remoteDefaultRoute" -> "[root] aws_route_table.remotert" + "[root] aws_route.remoteRoutes (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_route.remoteRoutes" -> "[root] aws_ec2_transit_gateway.west-tgw" + "[root] aws_route.remoteRoutes" -> "[root] aws_route.remoteRoutes (expand)" + "[root] aws_route.remoteRoutes" -> "[root] aws_route_table.remotert" + "[root] aws_route.vpcRoutes (expand)" -> "[root] local.routes (expand)" + "[root] aws_route.vpcRoutes[\"0\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"0\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"0\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"0\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.vpcRoutes[\"1\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"1\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"1\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"1\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.vpcRoutes[\"2\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"2\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"2\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"2\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.vpcRoutes[\"3\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"3\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"3\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"3\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.vpcRoutes[\"4\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"4\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"4\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"4\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route.vpcRoutes[\"5\"]" -> "[root] aws_route.vpcRoutes (expand)" + "[root] aws_route.vpcRoutes[\"5\"]" -> "[root] aws_route_table.rt[\"0\"]" + "[root] aws_route.vpcRoutes[\"5\"]" -> "[root] aws_route_table.rt[\"1\"]" + "[root] aws_route.vpcRoutes[\"5\"]" -> "[root] aws_route_table.rt[\"2\"]" + "[root] aws_route_table.remotert (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_route_table.remotert" -> "[root] aws_route_table.remotert (expand)" + "[root] aws_route_table.remotert" -> "[root] aws_vpc.remotevpc" + "[root] aws_route_table.rt (expand)" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_route_table.rt (expand)" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_route_table.rt (expand)" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_route_table.rt[\"0\"]" -> "[root] aws_route_table.rt (expand)" + "[root] aws_route_table.rt[\"1\"]" -> "[root] aws_route_table.rt (expand)" + "[root] aws_route_table.rt[\"2\"]" -> "[root] aws_route_table.rt (expand)" + "[root] aws_security_group.remotesg (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_security_group.remotesg" -> "[root] aws_security_group.remotesg (expand)" + "[root] aws_security_group.remotesg" -> "[root] aws_vpc.remotevpc" + "[root] aws_security_group.sg (expand)" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_security_group.sg (expand)" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_security_group.sg (expand)" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_security_group.sg[\"0\"]" -> "[root] aws_security_group.sg (expand)" + "[root] aws_security_group.sg[\"0\"]" -> "[root] local.rulesmap (expand)" + "[root] aws_security_group.sg[\"1\"]" -> "[root] aws_security_group.sg (expand)" + "[root] aws_security_group.sg[\"1\"]" -> "[root] local.rulesmap (expand)" + "[root] aws_security_group.sg[\"2\"]" -> "[root] aws_security_group.sg (expand)" + "[root] aws_security_group.sg[\"2\"]" -> "[root] local.rulesmap (expand)" + "[root] aws_subnet.remotesubnet (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_subnet.remotesubnet" -> "[root] aws_subnet.remotesubnet (expand)" + "[root] aws_subnet.remotesubnet" -> "[root] aws_vpc.remotevpc" + "[root] aws_subnet.remotesubnet" -> "[root] local.remoteAz (expand)" + "[root] aws_subnet.remotesubnet" -> "[root] local.remoteSubnet (expand)" + "[root] aws_subnet.subnet (expand)" -> "[root] local.subnets (expand)" + "[root] aws_subnet.subnet (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_subnet.subnet[\"0\"]" -> "[root] aws_subnet.subnet (expand)" + "[root] aws_subnet.subnet[\"0\"]" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_subnet.subnet[\"0\"]" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_subnet.subnet[\"0\"]" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_subnet.subnet[\"1\"]" -> "[root] aws_subnet.subnet (expand)" + "[root] aws_subnet.subnet[\"1\"]" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_subnet.subnet[\"1\"]" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_subnet.subnet[\"1\"]" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_subnet.subnet[\"2\"]" -> "[root] aws_subnet.subnet (expand)" + "[root] aws_subnet.subnet[\"2\"]" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_subnet.subnet[\"2\"]" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_subnet.subnet[\"2\"]" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_vpc.myvpc (expand)" -> "[root] local.vpcs (expand)" + "[root] aws_vpc.myvpc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_vpc.myvpc[\"0\"]" -> "[root] aws_vpc.myvpc (expand)" + "[root] aws_vpc.myvpc[\"1\"]" -> "[root] aws_vpc.myvpc (expand)" + "[root] aws_vpc.myvpc[\"2\"]" -> "[root] aws_vpc.myvpc (expand)" + "[root] aws_vpc.remotevpc (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] aws_vpc.remotevpc" -> "[root] aws_vpc.remotevpc (expand)" + "[root] aws_vpc.remotevpc" -> "[root] local.remoteCidrBlock (expand)" + "[root] aws_vpc_peering_connection.eastToHub (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_vpc_peering_connection.eastToHub" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_vpc_peering_connection.eastToHub" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_vpc_peering_connection.eastToHub" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_vpc_peering_connection.eastToHub" -> "[root] aws_vpc_peering_connection.eastToHub (expand)" + "[root] aws_vpc_peering_connection.eastToWest (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_vpc_peering_connection.eastToWest" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_vpc_peering_connection.eastToWest" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_vpc_peering_connection.eastToWest" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_vpc_peering_connection.eastToWest" -> "[root] aws_vpc_peering_connection.eastToWest (expand)" + "[root] aws_vpc_peering_connection.westToHub (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] aws_vpc_peering_connection.westToHub" -> "[root] aws_vpc.myvpc[\"0\"]" + "[root] aws_vpc_peering_connection.westToHub" -> "[root] aws_vpc.myvpc[\"1\"]" + "[root] aws_vpc_peering_connection.westToHub" -> "[root] aws_vpc.myvpc[\"2\"]" + "[root] aws_vpc_peering_connection.westToHub" -> "[root] aws_vpc_peering_connection.westToHub (expand)" + "[root] data.aws_ami.amazon_linux (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" + "[root] data.aws_ami.amazon_linux_remote (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] data.aws_region.peer (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1" + "[root] local.routes (expand)" -> "[root] aws_vpc_peering_connection.eastToHub" + "[root] local.routes (expand)" -> "[root] aws_vpc_peering_connection.eastToWest" + "[root] local.routes (expand)" -> "[root] aws_vpc_peering_connection.westToHub" + "[root] output.instance_ip_addresses" -> "[root] aws_instance.ec2[0]" + "[root] output.instance_ip_addresses" -> "[root] aws_instance.ec2[1]" + "[root] output.instance_ip_addresses" -> "[root] aws_instance.ec2[2]" + "[root] output.instance_pubip_addresses" -> "[root] aws_instance.ec2[0]" + "[root] output.instance_pubip_addresses" -> "[root] aws_instance.ec2[1]" + "[root] output.instance_pubip_addresses" -> "[root] aws_instance.ec2[2]" + "[root] output.remoteInstance_ip_address" -> "[root] aws_instance.ec2remote" + "[root] output.remoteInstance_pubip_address" -> "[root] aws_instance.ec2remote" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route.central-to-10_20" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route.central-to-10_30" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route.central-to-10_99" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route_table_association.central-rtassoc" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route_table_association.peering-central-rtassoc" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_ec2_transit_gateway_route_table_propagation.central-propag" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.ec2[0]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.ec2[1]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.ec2[2]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_main_route_table_association.rtassoc[0]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_main_route_table_association.rtassoc[1]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_main_route_table_association.rtassoc[2]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.defaultRoutes[0]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.defaultRoutes[1]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.defaultRoutes[2]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.hubToRemote" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"0\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"1\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"2\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"3\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"4\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_route.vpcRoutes[\"5\"]" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_ec2_transit_gateway_peering_attachment_accepter.west" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_ec2_transit_gateway_route.west-to-10_00" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_ec2_transit_gateway_route_table_association.peering-west-rtassoc" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_ec2_transit_gateway_route_table_association.west-rtassoc" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_ec2_transit_gateway_route_table_propagation.west-propag" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_instance.ec2remote" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_main_route_table_association.remotertassoc" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_route.remoteDefaultRoute" + "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" -> "[root] aws_route.remoteRoutes" + "[root] root" -> "[root] output.instance_ip_addresses" + "[root] root" -> "[root] output.instance_pubip_addresses" + "[root] root" -> "[root] output.remoteInstance_ip_address" + "[root] root" -> "[root] output.remoteInstance_pubip_address" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"].eu-west-1 (close)" + } +} + diff --git a/requirements.txt b/requirements.txt index f322210..3d1f83f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ -setuptools==41.0.1 -requests==2.22.0 -Jinja2==2.10.1 -Flask==1.0.3 -beautifulsoup4==4.7.1 +setuptools==70.0.0 +requests==2.32.0 +Jinja2==3.1.4 +Flask==2.3.2 +beautifulsoup4==4.11.1 ply>=3.11 -pyhcl==0.3.12 +python-hcl2==3.0.5 +markupsafe>=2.0.1 +pyhcl<=0.4.4 +itsdangerous==2.1.2 diff --git a/setup.py b/setup.py index 04e53e9..b419726 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,8 @@ long_description_content_type='text/markdown', author='Patrick McMurchie', author_email='patrick.mcmurchie@gmail.com', - packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + packages=find_packages(exclude=['ez_setup', 'examples', 'example', 'tests']), scripts=['bin/blast-radius'], install_requires=reqs, + include_package_data=True, )