From 2f518e4867991fa1c9ac299c8d0786280b83a1a0 Mon Sep 17 00:00:00 2001
From: Maarten de Waard <maarten@greenhost.nl>
Date: Fri, 1 May 2020 16:09:14 +0200
Subject: [PATCH] truncate branch names from within CLI, use dynamic
 environments

---
 .gitlab-ci.yml                   | 46 +++++++++++++++++---------------
 .gitlab/ci_scripts/create_vps.sh |  3 ++-
 openappstack/__main__.py         | 43 +++++++++++++++++++++++++++--
 openappstack/cluster.py          | 25 +++++++++++++++++
 4 files changed, 92 insertions(+), 25 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fb0307744..1c4735cfb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,9 +9,9 @@
   - |
     echo "Env vars:"
     echo
+    echo "CLUSTER_NAME:              $CLUSTER_NAME"
+    echo "IP_ADDRESS:                $IP_ADDRESS"
     echo "HOSTNAME:                  $HOSTNAME"
-    echo "SUBDOMAIN:                 $SUBDOMAIN"
-    echo "DOMAIN:                    $DOMAIN"
     echo "FQDN:                      $FQDN"
     echo "CLUSTER_DIR:               $CLUSTER_DIR"
     echo "ANSIBLE_HOST_KEY_CHECKING: $ANSIBLE_HOST_KEY_CHECKING"
@@ -74,6 +74,9 @@ create-vps:
     - clusters
     expire_in: 1 month
     when: always
+    reports:
+      dotenv:
+        $CLUSTER_DIR/.env
   only:
     changes:
       - .gitlab-ci.yml
@@ -89,6 +92,12 @@ create-vps:
     paths:
       - clusters/$HOSTNAME/**
     key: ${CI_COMMIT_REF_SLUG}
+  environment:
+    name: staging/$CI_COMMIT_REF_SLUG
+    url: https://$FQDN
+    on_stop: terminate-droplet
+    auto_stop_in: 1 week
+
 
 setup-openappstack:
   stage: setup-cluster
@@ -275,28 +284,21 @@ behave-grafana:
       - openappstack/**/*
   extends: .ssh_setup
 
-terminate-mr-droplet-after-merge:
-  stage: cleanup
-  before_script:
-    - echo "We leave MR droplets running even when the pipeline is successful \
-      to be able to investigate a MR. We need to terminate them when the MR \
-      is merged into master."
+# Terminates a droplet once the branch for it is deleted
+terminate-droplet:
+  # Stage has to be the same as the step that created the VPS
+  # https://docs.gitlab.com/ee/ci/environments.html#automatically-stopping-an-environment
+  stage: create-vps
+  # Gets triggered by on_stop of create-vps job
+  when: manual
+  variables:
+    GIT_STRATEGY: none
   script:
     - *debug_information
-    - |
-      if [ "$(git show -s --pretty=%p HEAD | wc -w)" -gt 1 ]
-      then
-        commit_message="$(git show -s --format=%s)"
-        tmp="${commit_message#*\'}"
-        merged_branch="${tmp%%\'*}"
-        echo "Current HEAD is a merge commit, removing droplet from related merge request branch name '#${merged_branch}'."
-        python3 -c "import greenhost_cloud; greenhost_cloud.terminate_droplets_by_name(\"^${merged_branch}\.\")"
-      else
-        echo "Current HEAD is NOT a merge commit, nothing to do."
-      fi
-  only:
-    refs:
-      - master
+    - python3 -c "import greenhost_cloud; greenhost_cloud.terminate_droplets_by_name(\"^${CI_COMMIT_REF_SLUG}\")"
+  environment:
+    name: staging/$CI_COMMIT_REF_NAME
+    action: stop
 
 terminate-old-droplets:
   stage: cleanup
diff --git a/.gitlab/ci_scripts/create_vps.sh b/.gitlab/ci_scripts/create_vps.sh
index a949eec13..88ff6efc1 100644
--- a/.gitlab/ci_scripts/create_vps.sh
+++ b/.gitlab/ci_scripts/create_vps.sh
@@ -26,4 +26,5 @@ python3 -m openappstack $HOSTNAME create \
   --create-hostname $HOSTNAME \
   --ssh-key-id $SSH_KEY_ID \
   --create-domain-records \
-  --subdomain $SUBDOMAIN
+  --subdomain $SUBDOMAIN \
+  --truncate-subdomain
diff --git a/openappstack/__main__.py b/openappstack/__main__.py
index 04400584e..9efad5bc2 100755
--- a/openappstack/__main__.py
+++ b/openappstack/__main__.py
@@ -28,6 +28,10 @@ from openappstack import name, cluster, ansible
 
 ALL_TESTS = ['behave']
 
+# We're limiting to 56, because we use subdomains, the longest of which
+# is 7 characters (`office.`). Max CN length is 63 characters.
+MAX_DOMAIN_LENGTH = 56
+
 
 def main():  # pylint: disable=too-many-statements,too-many-branches,too-many-locals
     """
@@ -115,6 +119,14 @@ def main():  # pylint: disable=too-many-statements,too-many-branches,too-many-lo
         help=('Use a custom subdomain for the generated domain records. '
               'Defaults to no subdomain'))
 
+    # Use this option to make sure that you won't run into troubles with
+    # certificates. Let's Encrypt requires a CN for each certificate, and a
+    # CN has a max length of 63 characters.
+    droplet_creation_group.add_argument(
+        '--truncate-subdomain',
+        action='store_true',
+        help=('Add a subdomain that is shorter than 56 characters'))
+
     droplet_creation_group.add_argument(
         '--acme-staging',
         action='store_true',
@@ -251,10 +263,37 @@ def create(clus, args):  # pylint: disable=too-many-branches
             sys.exit(1)
 
     if args.subdomain:
+        # Check if {subdomain}.{domain} is not too long to fit in CN
+        if len(args.domain) > MAX_DOMAIN_LENGTH:
+            log.error(('ERROR: domain argument is too long. Domain will not '
+                       'fit in a CN, please make sure your subdomain + '
+                       'domain + 1 do not exceed %d '
+                       'characters'), MAX_DOMAIN_LENGTH)
+            sys.exit(1)
+
+        if len(args.domain) + len(args.subdomain) + 1 > MAX_DOMAIN_LENGTH:
+            if args.truncate_subdomain:
+                required_length = MAX_DOMAIN_LENGTH - len(args.domain) - 1
+                # UGLY WORKAROUND, REMOVE BEFORE MERGING:
+                if args.subdomain[-3:] == '.ci':
+                    subdomain = args.subdomain[0:required_length-3] + '.ci'
+                else:
+                # END OF UGLY WORKAROUND
+                    subdomain = args.subdomain[0:required_length]
+                log.warning('Subdomain truncated to "%s"', subdomain)
+            else:
+                log.error(('ERROR: --subdomain argument is too long. Domain '
+                           'will not fit in a CN, please make sure your '
+                           'subdoman + domain + 1 do not exceed %d '
+                           'characters'), MAX_DOMAIN_LENGTH)
+                sys.exit(1)
+
         domain = '{subdomain}.{domain}'.format(
-            subdomain=args.subdomain, domain=args.domain)
+            subdomain=subdomain, domain=args.domain)
     else:
         domain = args.domain
+        subdomain = None
+
     clus.domain = domain
 
     # Set acme_staging to False so we use Let's Encrypt's live environment
@@ -283,7 +322,7 @@ def create(clus, args):  # pylint: disable=too-many-branches
 
     if args.create_domain_records:
         create_domain_records(
-            args.domain, clus.ip_address, subdomain=args.subdomain)
+            args.domain, clus.ip_address, subdomain=subdomain)
         if args.verbose:
             greenhost_cloud.list_domain_records(args.domain)
 
diff --git a/openappstack/cluster.py b/openappstack/cluster.py
index 444f5e162..aada9794f 100644
--- a/openappstack/cluster.py
+++ b/openappstack/cluster.py
@@ -165,6 +165,25 @@ class Cluster:
             stream.write(file_contents)
             log.info("Created %s", self.settings_file)
 
+        dotenv_file = """CLUSTER_NAME={name}
+CLUSTER_DIR={cluster_dir}
+IP_ADDRESS={ip_address}
+HOSTNAME={hostname}
+DOMAIN={domain}
+LOCAL_FLUX={local_flux}
+"""
+
+        with open(self.dotenv_file, 'w') as stream:
+            stream.write(dotenv_file.format(
+                name=self.name,
+                cluster_dir=self.cluster_dir,
+                ip_address=self.ip_address,
+                hostname=self.hostname,
+                domain=self.domain,
+                local_flux=self.local_flux
+            ))
+            log.info("Created %s", self.dotenv_file)
+
         # Set self.data_loaded to True because the data in the class now
         # reflects the data in the file.
         self.data_loaded = True
@@ -184,6 +203,12 @@ class Cluster:
         return os.path.join(self.cluster_dir, 'group_vars', 'all',
                             'settings.yml')
 
+    @property
+    def dotenv_file(self):
+        """Path to the .env file that can be used by gitlab-ci"""
+        return os.path.join(self.cluster_dir, '.env')
+
+
     @property
     def behave_file(self):
         """Path to 'behave.ini' which is used for acceptance tests"""
-- 
GitLab