Repository: mantl/mantl

Issue:
Use `plan` or `graph` to lint all of our supported terraform providers
This is an issue that @siddharthist asked me to post.

For CI testing, it would be _really_ nice to verify terraform configs for all of our providers, rather than just AWS, DO, and GCE. We can use either `plan` (or possibly `graph`) to verify sane configs without actually provisioning them. 



Discussion:

Resolving patch:
diff --git a/docker.py b/docker.py
--- a/docker.py
+++ b/docker.py
@@ -55,8 +55,8 @@ def link_user_defined_terraform_files():
         if 'MANTL_PROVIDER' not in os.environ:
             logging.critical("mantl.readthedocs.org for provider")
             exit(1)
-        tf = 'terraform/{}.sample.tf'.format(os.environ['MANTL_PROVIDER'])
 
+        tf = 'terraform/{}.sample.tf'.format(os.environ['MANTL_PROVIDER'])
         symlink_force(tf, 'mantl.tf')
 
 
@@ -83,11 +83,7 @@ def ci_setup():
     """Run all setup commands, saving files to MANTL_CONFIG_DIR"""
 
     if 'OS_IP' in os.environ:
-        ssh_key_path = '/local/ci'
-        os.chmod(ssh_key_path, 0400)
-
         # This string will be collapsed into one line
-        # I made this change for readability
         ssh_cmd = '''
 ssh -i {keypath} -p {ssh_port}
 -o BatchMode=yes -o StrictHostKeyChecking=no
@@ -102,11 +98,11 @@ def ci_setup():
 echo 'build_number = \\"{build}\\"' > terraform.tfvars"
         '''
         ssh_cmd = ssh_cmd.format(commit=os.environ['CI_HEAD_COMMIT'],
-                keypath='/local/ci',
-                ssh_port=os.environ['OS_PRT'],
-                ssh_ip=os.environ['OS_IP'],
-                tf_file=os.environ['TERRAFORM_FILE'],
-                build=os.environ['TF_VAR_build_number'])
+                                 keypath='/local/ci',
+                                 ssh_port=os.environ['OS_PRT'],
+                                 ssh_ip=os.environ['OS_IP'],
+                                 tf_file=os.environ['TERRAFORM_FILE'],
+                                 build=os.environ['TF_VAR_build_number'])
         ssh_cmd = " ".join(ssh_cmd.splitlines())
 
         exit(call(split(ssh_cmd)))
@@ -124,14 +120,8 @@ def ci_build():
     call("ssh-add")
     link_ci_terraform_file()
 
-    # Take different action for PRs from forks
-    if os.environ['MANTL_CI_FORK_CHECK'] == "1":
-        logging.warning("Currently, we can't unlock deploy keys for forks of Mantl, so we are skipping the build")
-        exit(0)
-
     # Filter out commits that are documentation changes.
     commit_range_cmd = 'git diff --name-only {}'.format(os.environ['TRAVIS_COMMIT_RANGE'])
-
     commit_range_str = str(check_output(split(commit_range_cmd)))
 
     commit_range = []
@@ -147,37 +137,38 @@ def ci_build():
             commit_range.append(commit)
 
     if len(commit_range) < 1:
-        logging.info("All of the changes I found were in documentation files. Skipping build")
+        logging.info("All of the changes were in documentation. Skipping build.")
         exit(0)
 
-    # Filter out commits that are pushes to non-master branches
-    ci_branch = os.environ['TRAVIS_BRANCH']
-    ci_is_pr = os.environ['TRAVIS_PULL_REQUEST']
-    if ci_branch != 'master' and ci_is_pr != "1":
-        logging.info("We don't want to build on pushes to branches that aren't master.")
-        exit(0)
+    build_command = 'python2 ./testing/build-cluster.py'
 
-    if 'OS_IP' in os.environ:
-        ssh_cmd = '''
+    if not os.environ.get('DOCKER_SECRETS', False):
+        logging.warning('Secrets not available, only linting')
+        build_command = 'python2 ./testing/build-cluster.py plan-only'
+
+    elif os.environ.get('PROVIDER', '') == 'clc' or 'clc' in os.environ.get('TERRAFORM_FILE', ''):
+        logging.warning('Full CI builds not supported on CLC, only linting')
+        build_command = 'python2 ./testing/build-cluster.py plan-only'
+
+    elif 'OS_IP' in os.environ:
+        ssh_cmd_template = '''
 ssh -i {keypath} -p {ssh_port}
 -o BatchMode=yes -o StrictHostKeyChecking=no
 travis@{ssh_ip} /bin/sh -c '
 eval $(ssh-agent);
 ssh-add;
 cd ./mantl/{commit};
-python2 ./testing/build-cluster.py'
+{build_cmd}'
         '''
-        ssh_cmd = ssh_cmd.format(commit=os.environ['CI_HEAD_COMMIT'],
-                keypath='/local/ci',
-                ssh_port=os.environ['OS_PRT'],
-                ssh_ip=os.environ['OS_IP'])
-        ssh_cmd = " ".join(ssh_cmd.splitlines())
+        ssh_cmd = ssh_cmd_template.format(commit=os.environ['CI_HEAD_COMMIT'],
+                                          keypath='/local/ci',
+                                          ssh_port=os.environ['OS_PRT'],
+                                          ssh_ip=os.environ['OS_IP'],
+                                          build_cmd=build_command)
+        build_cmd = " ".join(ssh_cmd.splitlines())
 
-        exit(call(split(ssh_cmd)))
-
-    else:
-        logging.info("Starting cloud provider test")
-        exit(call(split("python2 testing/build-cluster.py")))
+    logging.info("Starting cloud provider test")
+    exit(call(split(build_command)))
 
 
 def ci_destroy():
@@ -196,10 +187,10 @@ def ci_destroy():
 rm -fr {commit}'
         '''
         destroy_cmd = ssh_cmd.format(destroy=destroy_cmd,
-                keypath='/local/ci',
-                ssh_port=os.environ['OS_PRT'],
-                ssh_ip=os.environ['OS_IP'],
-                commit=os.environ['CI_HEAD_COMMIT'])
+                                     keypath='/local/ci',
+                                     ssh_port=os.environ['OS_PRT'],
+                                     ssh_ip=os.environ['OS_IP'],
+                                     commit=os.environ['CI_HEAD_COMMIT'])
         destroy_cmd = " ".join(destroy_cmd.splitlines())
     else:
         logging.info("Destroying cloud provider resources")
@@ -207,7 +198,6 @@ def ci_destroy():
         call("ssh-add")
         link_ci_terraform_file()
 
-
     exit(call(split(destroy_cmd)))
 
 
@@ -219,7 +209,7 @@ def ci_destroy():
     if 'MANTL_CONFIG_DIR' not in os.environ:
         exit(1)
 
-    #TODO: replace this with either click or pypsi
+    # TODO: replace this with either click or pypsi
     if len(argv) > 1:
         if argv[1] == 'ci-setup':
             ci_setup()


Test patch:
diff --git a/testing/build-cluster.py b/testing/build-cluster.py
--- a/testing/build-cluster.py
+++ b/testing/build-cluster.py
@@ -1,16 +1,17 @@
 #!/usr/bin/env python2
 from __future__ import print_function
 import subprocess
-from sys import exit
-from os import devnull, environ, symlink
+from sys import exit, argv
 from time import sleep
+import os
+
 
 def run_cmd(cmd, attempts=1):
     """ Runs a command attempts times, logging its output. Returns True if it
     succeeds once, or False if it never does. """
     try:
         for i in range(attempts):
-            proc = subprocess.Popen(cmd, stdin=open(devnull, "r"))
+            proc = subprocess.Popen(cmd, stdin=open(os.devnull, "r"))
             proc.wait()
             if not proc.returncode == 0:
                 print("Command ", str(cmd), " failed")
@@ -23,6 +24,7 @@ def run_cmd(cmd, attempts=1):
         print("Error while attempting to run command ", cmd)
         print(e)
 
+
 def run_cmds(cmds, fail_sequential=False):
     """ Run a list of ([args], tries) tuples, aborting the run if a single one
     fails and fail_sequential is set to True.
@@ -48,16 +50,17 @@ def run_cmds(cmds, fail_sequential=False):
     ]
     setup = [
         (["terraform", "get"], 1),
-        (["terraform", "plan"], 1),
-        (["terraform", "apply"], 1),
-        (ap + ["playbooks/wait-for-hosts.yml"], 3),
-        (ap + ["-e", "serial=0", "playbooks/upgrade-packages.yml"], 1),
-        (ap + ["sample.yml"], 1),
-        (["python2", "testing/healthchecks.py"], 2)
+        (["terraform", "plan", "--input=false", "--refresh=false"], 1),
     ]
-    debug = []
+    # TODO: replace this with either click or pypsi
+    if len(argv) < 2 or argv[1] != "plan-only":
+        setup.extend([
+            (["terraform", "apply"], 1),
+            (ap + ["playbooks/wait-for-hosts.yml"], 3),
+            (ap + ["-e", "serial=0", "playbooks/upgrade-packages.yml"], 1),
+            (ap + ["sample.yml"], 1),
+            (["python2", "testing/healthchecks.py"], 2)
+        ])
 
     if not run_cmds(setup, fail_sequential=True):
-        run_cmds(debug)
         exit(1)
-    # Travis works on the teardown, so there's no need to do so here.
diff --git a/testing/local.sh b/testing/local.sh
--- a/testing/local.sh
+++ b/testing/local.sh
@@ -19,7 +19,7 @@ fi
 export TF_VAR_build_number="$(whoami)-local"
 export TRAVIS_COMMIT_RANGE="$(git rev-parse master)...$(git rev-parse HEAD)"
 export CI_HEAD_COMMIT=$(git rev-list -n 1 --no-merges --branches="$(git rev-parse --abbrev-ref HEAD)" master...HEAD)
-export DOCKER_ARGS='--rm -i -a stdout -a stderr -e TF_VAR_build_number -e CI_HEAD_COMMIT -e TRAVIS_COMMIT_RANGE -e TRAVIS_REPO_SLUG=CiscoCloud/mantl -e TRAVIS_BRANCH=master -e TRAVIS_PULL_REQUEST=1 -e TERRAFORM_FILE'
+export DOCKER_ARGS='--rm -i -a stdout -a stderr -e TF_VAR_build_number -e CI_HEAD_COMMIT -e TRAVIS_COMMIT_RANGE -e TRAVIS_REPO_SLUG=CiscoCloud/mantl -e TRAVIS_BRANCH=master -e TRAVIS_PULL_REQUEST=1 -e TERRAFORM_FILE -e DOCKER_SECRETS'
 export DOCKER_IMAGE=sehqlr/mantl:testing
 
 case $1 in
diff --git a/testing/aws.tf b/testing/terraform/aws.tf
similarity index 100%
rename from testing/aws.tf
rename to testing/terraform/aws.tf
diff --git a/testing/cisco-os.tf b/testing/terraform/cisco-os.tf
similarity index 100%
rename from testing/cisco-os.tf
rename to testing/terraform/cisco-os.tf
diff --git a/testing/terraform/clc.tf b/testing/terraform/clc.tf
new file mode 100644
--- /dev/null
+++ b/testing/terraform/clc.tf
@@ -0,0 +1,49 @@
+variable build_number {}
+
+variable location { default = "CA1" }
+variable control_count { default = 3 }
+variable worker_count { default = 2 }
+variable edge_count { default = 1 }
+variable ssh_pass { default = "Green123$" }
+variable ssh_key { default = "~/.ssh/id_rsa.pub" }
+
+provider "clc" {}
+
+resource "clc_group" "mantl" {
+  location_id = "${var.location}"
+  name = "ci"
+  parent = "Default Group"
+}
+
+module "control-nodes" {
+  source = "./terraform/clc/node"
+  location = "${var.location}"
+  group_id = "${clc_group.mantl.id}"
+  role = "control"
+  name = "CICTL"
+  count = "${var.control_count}"
+  ssh_pass = "${var.ssh_pass}"
+  ssh_key = "${var.ssh_key}"
+}
+
+module "edge-nodes" {
+  source = "./terraform/clc/node"
+  location = "${var.location}"
+  group_id = "${clc_group.mantl.id}"
+  role = "edge"
+  name = "CIEDG"
+  count = "${var.edge_count}"
+  ssh_pass = "${var.ssh_pass}"
+  ssh_key = "${var.ssh_key}"
+}
+
+module "worker-nodes" {
+  source = "./terraform/clc/node"
+  location = "${var.location}"
+  group_id = "${clc_group.mantl.id}"
+  role = "worker"
+  name = "CIWRK"
+  count = "${var.worker_count}"
+  ssh_pass = "${var.ssh_pass}"
+  ssh_key = "${var.ssh_key}"
+}
diff --git a/testing/do.tf b/testing/terraform/do.tf
similarity index 100%
rename from testing/do.tf
rename to testing/terraform/do.tf
diff --git a/testing/gce.tf b/testing/terraform/gce.tf
similarity index 100%
rename from testing/gce.tf
rename to testing/terraform/gce.tf
diff --git a/testing/terraform/softlayer.tf b/testing/terraform/softlayer.tf
new file mode 100644
--- /dev/null
+++ b/testing/terraform/softlayer.tf
@@ -0,0 +1,22 @@
+variable build_number {}
+
+provider "softlayer" {
+  username = "test_username"
+  api_key = "test_api_key"
+}
+
+module "softlayer-keypair" {
+  source = "./terraform/softlayer/keypair"
+  public_key_filename = "~/.ssh/id_rsa.pub"
+}
+
+module "softlayer-hosts" {
+  source = "./terraform/softlayer/hosts"
+  ssh_key = "${module.softlayer-keypair.keypair_id}"
+
+  region_name = "ams01"
+  domain = "ci.mantl.io"
+  control_count = 3
+  worker_count = 2
+  edge_count = 1
+}
diff --git a/testing/terraform/triton.tf b/testing/terraform/triton.tf
new file mode 100644
--- /dev/null
+++ b/testing/terraform/triton.tf
@@ -0,0 +1,87 @@
+variable build_number {}
+
+provider "triton" {
+  # taken from https://www.terraform.io/docs/providers/triton/index.html
+  account      = "AccountName"
+  key_material = "${file("~/.ssh/id_rsa")}"
+  key_id       = "25:d4:a9:fe:ef:e6:c0:bf:b4:4b:4b:d4:a8:8f:01:0f"
+}
+
+variable key_path { default = "~/.ssh/id_rsa.pub" }
+variable control_count { default = 3 }
+variable control_package { default = "Medium 4GB" } # 4GB and 4 VCPUs
+variable edge_count { default = 1 }
+variable edge_package { default = "Medium 2GB" } # 2GB and 2VCPUs
+variable image { default = "dd31507e-031e-11e6-be8a-8f2707b5b3ee" } # centos-7, updated 2016-04-15
+variable kubeworker_count { default = 2 }
+variable short_name { default = "mantl-ci" }
+variable worker_count { default = 2 }
+variable worker_package { default = "Large 8GB" } # 8GB and 8 VCPUs
+variable public_network { default = "9ec60129-9034-47b4-b111-3026f9b1a10f" } # default public in us-east-1. Find yours with `triton network list`
+variable vlan_id { default = 3 }
+
+module "triton-keypair" {
+  source = "./terraform/triton/keypair"
+
+  short_name = "mantl-ci-${var.build_number}"
+  public_key_material = "${file(var.key_path)}"
+}
+
+module "network" {
+  source = "./terraform/triton/network"
+
+  short_name = "mantl-ci-${var.build_number}"
+  vlan_id    = "${var.vlan_id}"
+}
+
+module "control-nodes" {
+  source = "./terraform/triton/instance"
+
+  count           = "${var.control_count}"
+  image           = "${var.image}"
+  keys            = "${file(var.key_path)}"
+  package         = "${var.control_package}"
+  role            = "control"
+  short_name      = "mantl-ci-${var.build_number}"
+  public_network  = "${var.public_network}"
+  private_network = "${module.network.network_id}"
+}
+
+module "worker-nodes" {
+  source = "./terraform/triton/instance"
+
+  count           = "${var.worker_count}"
+  image           = "${var.image}"
+  keys            = "${file(var.key_path)}"
+  package         = "${var.worker_package}"
+  role            = "worker"
+  short_name      = "mantl-ci-${var.build_number}"
+  public_network  = "${var.public_network}"
+  private_network = "${module.network.network_id}"
+}
+
+module "kubeworker-nodes" {
+  source = "./terraform/triton/instance"
+
+  count           = "${var.kubeworker_count}"
+  image           = "${var.image}"
+  keys            = "${file(var.key_path)}"
+  package         = "${var.worker_package}"
+  role            = "kubeworker"
+  short_name      = "mantl-ci-${var.build_number}"
+  public_network  = "${var.public_network}"
+  private_network = "${module.network.network_id}"
+}
+
+module "edge-nodes" {
+  source = "./terraform/triton/instance"
+
+  count           = "${var.edge_count}"
+  image           = "${var.image}"
+  keys            = "${file(var.key_path)}"
+  package         = "${var.edge_package}"
+  role            = "edge"
+  short_name      = "mantl-ci-${var.build_number}"
+  public_network  = "${var.public_network}"
+  private_network = "${module.network.network_id}"
+}