utils.cdk

cdk

Common code useful for a CDK diff/deploy.

Example Usage: from utils import cdk from utils.cdk import get_cdk_required_version

  1#!/usr/bin/env python3
  2"""
  3cdk
  4
  5Common code useful for a CDK diff/deploy.
  6
  7Example Usage:
  8    from utils import cdk
  9    from utils.cdk import get_cdk_required_version
 10"""
 11
 12import subprocess
 13import os
 14from pathlib import Path
 15import re
 16import typing
 17import glob
 18import json
 19import yaml
 20
 21try:
 22    import loggy
 23except ImportError:
 24    from utils import loggy
 25try:
 26    from .common import subprocess_long as _long_run, ChDir as _chdir
 27except ImportError:
 28    from utils.common import subprocess_long as _long_run, ChDir as _chdir
 29
 30
 31
 32
 33def deploy(properties_env: str, lang: typing.Optional[str] = None, path: typing.Optional[str] = None) -> bool:
 34    """
 35    deploy()
 36
 37    Runs `cdk deploy` using the properties_env to locate the `properties.env.json` file
 38
 39    properties_env: String containing env name to locate the properties file
 40    lang: String representing the type of CDK code to deploy
 41    path: String (Optional) Will search for the cdk.json file recursively if this path is not set.
 42
 43    Returns: True/False
 44    """
 45
 46    _CDK_PATH = path if path is not None else os.path.dirname(glob.glob('**/cdk.json', recursive=True)[0])
 47    _PROPS_FILE = f"properties.{properties_env}.json"
 48    _CDK_DEPLOY_FILE = f"{os.getcwd()}/cdk.deploy.txt"
 49
 50    with _chdir(_CDK_PATH):
 51        env_file = Path(_PROPS_FILE).read_text()
 52        # env_data = json.load(env_file)
 53        loggy.info(f"cdk.deploy(): {env_file}")
 54
 55        if not install_cdk_requirements(cdk_lang=lang):
 56            loggy.info("cdk.deploy(): Failed to install cdk requirements. Check logs.")
 57            return False
 58
 59        my_env = os.environ.copy()
 60        my_env["ENVIRONMENT"] = properties_env
 61
 62        _process_output = _long_run(['cdk', 'deploy', '--require-approval', 'never', '--all'], env=my_env, check=False)
 63        loggy.info("----------------------------------")
 64        loggy.info(f"cdk.deploy(): CDK returned {str(_process_output.returncode)}")
 65
 66        with open(_CDK_DEPLOY_FILE, 'w') as file:
 67            if _process_output.stderr:
 68                file.write(_process_output.stderr)
 69
 70            if _process_output.stdout:
 71                file.write(_process_output.stdout)
 72
 73        if _process_output.returncode != 0:
 74            return False
 75
 76    return True
 77
 78
 79def diff(properties_env: str, lang: typing.Optional[str] = None, path: typing.Optional[str] = None) -> bool:
 80    """
 81    diff()
 82
 83    Runs `cdk diff` using the properties_env to locate the `properties.env.json` file
 84
 85    properties_env: String containing env name to locate the properties file
 86    lang: String representing the type of CDK code to deploy (python, ts, typescript)
 87    path: String (Optional) Will search for the cdk.json file recursively if this path is not set.
 88
 89    Returns: True/False
 90    """
 91
 92    _CDK_PATH = path if path is not None else os.path.dirname(glob.glob('**/cdk.json', recursive=True)[0])
 93    _PROPS_FILE = f"properties.{properties_env}.json"
 94    _CDK_DIFF_FILE = f"{os.getcwd()}/cdk.diff.txt"
 95
 96    _EXIT = True
 97    with _chdir(_CDK_PATH):
 98        env_file = Path(_PROPS_FILE).read_text()
 99        # env_data = json.load(env_file)
100        loggy.info(f"cdk.diff(): {env_file}")
101
102        if not install_cdk_requirements(cdk_lang=lang):
103            loggy.info("cdk.diff(): Failed to install cdk requirements. Check logs.")
104            return False
105
106        my_env = os.environ.copy()
107        my_env["ENVIRONMENT"] = properties_env
108
109        _process_output = _long_run(['cdk', 'diff', '--fail', '--verbose'], env=my_env, check=False)
110        loggy.info("----------------------------------")
111        loggy.info(f"cdk.diff(): CDK returned {str(_process_output.returncode)}")
112        # loggy.info("----------------------------------")
113        # loggy.info(f"cdk.diff(): CDK returned {str(_process_output.stderr)}")
114        # loggy.info("----------------------------------")
115        # loggy.info(f"cdk.diff(): CDK returned {str(_process_output.stdout)}")
116        # loggy.info("----------------------------------")
117
118        if _process_output.returncode != 0:
119            loggy.info("cdk.diff(): Testing for CDK Diff or Error.")
120
121            with open(_CDK_DIFF_FILE, 'w') as file:
122                file.write(_process_output.stderr)
123
124            if '[~]' in _process_output.stderr or '[+]' in _process_output.stderr or '[-]' in _process_output.stderr or '[=]' in _process_output.stderr:
125                loggy.info("cdk.diff(): CDK Diff found!")
126                _EXIT = True
127            else:
128                loggy.info("cdk.diff(): CDK ERROR!")
129                for stack in glob.glob('cdk.out/*.json'):
130                    loggy.info("----------------------------------")
131                    loggy.info("STACK: " + stack)
132                    _stack_yaml = yaml.dump(json.loads(Path(stack).read_text()))
133                    loggy.info(_stack_yaml)
134                _EXIT = False
135
136        else:
137            # if 'DEPLOY_OVERRIDE' in os.environ.keys():
138            #     print("cdkDiff.__main__(): No CDK diff! Overriding CDK_DIFF because DEPLOY_OVERRIDE")
139            #     _EXIT = 0
140            # else:
141            with open(_CDK_DIFF_FILE, 'w') as file:
142                file.write("NO CDK diff found. This could be an AMI SSM Param change. Deploy proceding.")
143
144            loggy.info("cdk.diff(): NO CDK diff found. This could be an AMI SSM Param change. Deploy proceding.")
145            _EXIT = True
146
147    return _EXIT
148 
149
150def diff_pretty(diff_file: str='cdk.diff.txt', output_file: str='cdk.diff.html', verbose: bool=False) -> bool:
151
152    _CDK_DIFF_FILE = diff_file
153    _CDK_HTML_OUTPUT_FILE = output_file
154    _VERBOSE = verbose
155
156    html_template = """
157<!DOCTYPE html>
158<html>
159  <head>
160    <meta charset="utf-8" />
161    <title>prettyplan</title>
162    <meta name="viewport" content="width=device-width, initial-scale=1" />
163    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
164    <style>
165      body {
166        font-family: Arial, Helvetica, sans-serif;
167        text-rendering: optimizeLegibility;
168        background: #ecf7fe;
169        color: #000000c0;
170        font-size: 15px;
171        margin: 0;
172      }
173      @keyframes fade-in {
174        0% {
175          opacity: 0;
176        }
177        100% {
178          opacity: 1;
179        }
180      }
181      .stripe {
182        width: 100%;
183        height: 5px;
184        background: #5c4ce4;
185        animation-name: wipe-in;
186        animation-duration: 1s;
187      }
188      @keyframes wipe-in {
189        0% {
190          width: 0%;
191        }
192        100% {
193          width: 100%;
194        }
195      }
196      #release-notification {
197        background: #5c4ce4;
198        color: white;
199        font-weight: bold;
200        text-align: center;
201        overflow: hidden;
202        padding: 10px 0 15px 0;
203        height: 20px;
204        animation-name: notification-pop-in;
205        animation-duration: 2s;
206      }
207      #release-notification a {
208        color: white;
209      }
210      #release-notification.dismissed {
211        animation-name: notification-pop-out;
212        animation-duration: 0.5s;
213        height: 0;
214        padding: 0;
215      }
216      @keyframes notification-pop-in {
217        0% {
218          height: 0;
219          padding: 0;
220        }
221        50% {
222          height: 0;
223          padding: 0;
224        }
225      }
226      @keyframes notification-pop-out {
227        0% {
228          height: 20px;
229          padding: 10px 0 15px 0;
230        }
231        100% {
232          height: 0;
233          padding: 0;
234        }
235      }
236      #modal-container {
237        animation-name: fade-in;
238        animation-duration: 0.2s;
239      }
240      .modal-pane {
241        position: fixed;
242        top: 0;
243        left: 0;
244        width: 100%;
245        height: 100%;
246        background: #ffffffe6;
247        z-index: 10;
248      }
249      .modal-content {
250        position: fixed;
251        width: 60%;
252        height: 60%;
253        top: 50%;
254        left: 50%;
255        transform: translate(-50%, -50%);
256        background: #ffffff;
257        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
258        z-index: 20;
259      }
260      .modal-close {
261        position: absolute;
262        right: 0;
263        padding: 10px;
264      }
265      .modal-close button.text-button {
266        color: #4526ac;
267        text-decoration: none;
268        font-weight: normal;
269      }
270      .release-notes {
271        max-width: 80%;
272        margin: 0 auto 0 auto;
273        overflow-y: auto;
274        max-height: 100%;
275      }
276      #branding {
277        float: right;
278        padding-top: 10px;
279        padding-right: 10px;
280        font-size: 10px;
281        color: #4526ac;
282        text-align: right;
283      }
284      #branding a {
285        color: #4526ac;
286      }
287      .container {
288        margin: 10px 10px 0 10px;
289        animation-name: fade-in;
290        animation-duration: 1s;
291      }
292      @media only screen and (min-width: 600px) {
293        .container {
294          max-width: 80%;
295          margin-left: auto;
296          margin-right: auto;
297        }
298      }
299      h1,
300      h2 {
301        text-align: center;
302        color: #4526ac;
303      }
304      #terraform-plan {
305        width: 100%;
306        min-height: 300px;
307        border: none;
308        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
309        padding: 10px;
310        margin-bottom: 10px;
311        resize: none;
312        background: #ffffffe6;
313      }
314      button {
315        font-size: 18px;
316        background: #5c4ce4;
317        color: #fff;
318        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
319        border: none;
320        border-radius: 2px;
321        min-width: 170px;
322        height: 40px;
323      }
324      button:hover {
325        background: #6567ea;
326        cursor: pointer;
327      }
328      button:active {
329        background: #5037ca;
330      }
331      button.text-button {
332        background: none;
333        box-shadow: none;
334        border-radius: 0;
335        width: auto;
336        height: auto;
337        text-decoration: underline;
338        font-size: inherit;
339        font-weight: inherit;
340        font-family: Arial, Helvetica, sans-serif;
341        color: inherit;
342        text-align: inherit;
343        padding: 0;
344      }
345      #parsing-error-message {
346        background-color: #ffffff;
347        padding: 10px;
348        color: #000000c0;
349        margin: 4px;
350        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
351        font-weight: bold;
352        border-left: 2px solid red;
353        animation-name: error;
354        animation-duration: 1s;
355      }
356      @keyframes error {
357        0% {
358          background-color: red;
359        }
360        100% {
361          background-color: white;
362        }
363      }
364      .prettyplan ul {
365        padding-left: 0;
366        font-size: 13px;
367      }
368      .prettyplan li {
369        list-style: none;
370        background: #ffffffe6;
371        padding: 10px;
372        color: #000000c0;
373        margin: 4px;
374        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
375      }
376      .prettyplan ul.warnings li {
377        border-left: 3px solid #757575;
378      }
379      .prettyplan ul.actions li.update {
380        border-left: 3px solid #ff8f00;
381      }
382      .prettyplan ul.actions li.create {
383        border-left: 3px solid #2e7d32;
384      }
385      .prettyplan ul.actions li.addition {
386        border-left: 3px solid #2e7d32;
387      }
388      .prettyplan ul.actions li.destroy {
389        border-left: 3px solid #b71c1c;
390      }
391      .prettyplan ul.actions li.removal {
392        border-left: 3px solid #b71c1c;
393      }
394      .prettyplan ul.actions li.recreate {
395        border-left: 3px solid #1565c0;
396      }
397      .prettyplan ul.actions li.read {
398        border-left: 3px solid #519bf0;
399      }
400      .badge {
401        display: inline-block;
402        text-transform: uppercase;
403        margin-right: 10px;
404        padding: 3px;
405        font-size: 12px;
406        font-weight: bold;
407      }
408      .warnings .badge {
409        color: #757575;
410      }
411      li.update .badge {
412        color: #ff8f00;
413      }
414      li.create .badge {
415        color: #2e7d32;
416      }
417      li.addition .badge {
418        color: #2e7d32;
419      }
420      li.destroy .badge {
421        color: #b71c1c;
422      }
423      li.removal .badge {
424        color: #b71c1c;
425      }
426      li.recreate .badge {
427        color: #1565c0;
428      }
429      li.read .badge {
430        color: #519bf0;
431      }
432      .id-segment:not(:last-child)::after {
433        content: ' > ';
434      }
435      .id-segment.name,
436      .id-segment.type {
437        font-weight: bold;
438      }
439      .change-count {
440        float: right;
441      }
442      .summary {
443        cursor: pointer;
444      }
445      .no-diff-changes-breakdown {
446        margin: 5px auto 0 auto;
447        padding: 5px;
448      }
449      .no-diff-changes-breakdown table {
450        width: 100%;
451        word-break: break-all;
452        font-size: 13px;
453      }
454      .no-diff-changes-breakdown table td {
455        padding: 10px;
456        width: 40%;
457      }
458      pre {
459        white-space: pre-wrap;
460        background: #f3f3f3;
461      }
462      .no-diff-changes-breakdown table td.property {
463        width: 20%;
464        text-align: right;
465        font-weight: bold;
466      }
467      .no-diff-changes-breakdown table tr:nth-child(even) {
468        background-color: #f5f5f5;
469      }
470      .forces-new-resource {
471        color: #b71c1c;
472      }
473      .collapsed,
474      .hidden {
475        display: none;
476      }
477      .actions button {
478        background: none;
479        border: none;
480        text-decoration: underline;
481        color: black;
482        box-shadow: none;
483        font-weight: bold;
484        font-size: 14px;
485      }
486      .d2h-icon {
487        display: none;
488      }
489    </style>
490  </head>
491  <body>
492    <div class="stripe"></div>
493    <div class="container">
494      <h1>prettyplan</h1>
495      <div id="parsing-error-message" class="hidden">
496        That doesn't look like a Terraform plan. Did you copy the entire output (without colouring) from the plan
497        command?
498      </div>
499      <div id="prettyplan" class="prettyplan">
500        <ul id="errors" class="errors"></ul>
501        <ul id="warnings" class="warnings"></ul>
502        <button class="expand-all" onclick="expandAll()">Expand all</button>
503        <button class="collapse-all hidden" onclick="collapseAll()">Collapse all</button>
504        <div id="stacks"></div>
505        <ul id="actions" class="actions"></ul>
506        <pre id="diff"></pre>
507      </div>
508    </div>
509    <script>
510      function accordion(element) {
511        const changes = element.parentElement.getElementsByClassName('changes');
512        for (var i = 0; i < changes.length; i++) {
513          toggleClass(changes[i], 'collapsed');
514        }
515      }
516      function toggleClass(element, className) {
517        if (!element.className.match(className)) {
518          element.className += ' ' + className;
519        } else {
520          element.className = element.className.replace(className, '');
521        }
522      }
523      function addClass(element, className) {
524        if (!element.className.match(className)) element.className += ' ' + className;
525      }
526      function removeClass(element, className) {
527        element.className = element.className.replace(className, '');
528      }
529      function expandAll() {
530        const sections = document.querySelectorAll('.changes.collapsed');
531        for (var i = 0; i < sections.length; i++) {
532          toggleClass(sections[i], 'collapsed');
533        }
534        toggleClass(document.querySelector('.expand-all'), 'hidden');
535        toggleClass(document.querySelector('.collapse-all'), 'hidden');
536      }
537      function collapseAll() {
538        const sections = document.querySelectorAll('.changes:not(.collapsed)');
539        for (var i = 0; i < sections.length; i++) {
540          toggleClass(sections[i], 'collapsed');
541        }
542        toggleClass(document.querySelector('.expand-all'), 'hidden');
543        toggleClass(document.querySelector('.collapse-all'), 'hidden');
544      }
545      function removeChildren(element) {
546        while (element.lastChild) {
547          element.removeChild(element.lastChild);
548        }
549      }
550      function createModalContainer() {
551        const modalElement = document.createElement('div');
552        modalElement.id = 'modal-container';
553        document.body.appendChild(modalElement);
554        return modalElement;
555      }
556      function closeModal() {
557        const modalElement = document.getElementById('modal-container');
558        document.body.removeChild(modalElement);
559      }
560    </script>
561  </body>
562</html>
563"""
564
565    if _VERBOSE:
566        loggy.info("diff_pretty()): BEGIN")
567
568    if not Path(_CDK_DIFF_FILE).exists():
569        loggy.info(f"diff_pretty(): ERROR. INPUT FILE ({_CDK_DIFF_FILE}) does NOT exist.")
570        return False
571
572    if _VERBOSE:
573        loggy.info(f"diff_pretty(): Reading input file: {_CDK_DIFF_FILE}")
574
575    cdk_out = Path(_CDK_DIFF_FILE).read_text()
576
577    if _VERBOSE:
578        loggy.info(f"diff_pretty(): {cdk_out}")
579
580    #
581    # Here, we split the stack output into an array based on `Stack xyz` lines
582    #
583    stacks = [f"Stack {e}" for e in cdk_out.split('Stack ') if e]
584
585    #
586    # This is a Hack to get rid of any warnings at the top of the output before
587    # we encounter our first `Stack xyz` line.
588    #
589    for idx, l in enumerate(stacks):
590        match = False
591        for hack_line in cdk_out.split('\n'):
592            if l.split('\n')[0] in hack_line:
593                match = True
594                break
595
596        if not match:
597            del stacks[idx]
598            continue
599
600    #
601    # Convert each `Stack xyz` line to our HTML format
602    #
603    for idx, l in enumerate(stacks):
604        new_line = []
605        diff_type = ""
606        divs_open = 0
607        ul_li_open = 0
608        for ndx, n in enumerate(l.split('\n')):
609            if _VERBOSE:
610                loggy.info(f"diff_pretty(): DEBUG: ({str(idx)})-({str(ndx)}) - {str(n)}")
611
612            if ndx == 0:
613                # new_line.append(f"<div class=\"stack\"><h2>{n}</h2><div class=\"raw-diff\"><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
614
615                #
616                # 2 open divs
617                #
618                divs_open += 2
619                new_line.append(f"<div class=\"stack\"><h2>{n}</h2><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
620            else:
621                if n.startswith('IAM Policy Changes'):
622                    diff_type = "IAM"
623                elif n.startswith('Resources'):
624                    diff_type = "Resources"
625                elif n.startswith('Outputs'):
626                    diff_type = "Outputs"
627                elif n.startswith('There were no differences'):
628                    diff_type = "NoDiff"
629
630                if 'Resources' in diff_type or 'Outputs' in diff_type:
631                    if n.startswith('[+]'):
632                        if ul_li_open > 0:
633                            ul_li_open = 0
634                            divs_open -= 1
635                            new_line.append('<pre></div></li></ul>')
636
637                        ul_li_open += 1
638                        divs_open += 1
639                        new_line.append(f"<ul class=\"actions\"><li class=\"create\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Addition</span></div><div class\"changes\"><pre>{n}")
640                    elif n.startswith('[-]'):
641                        if ul_li_open > 0:
642                            ul_li_open = 0
643                            divs_open -= 1
644                            new_line.append('</pre></div></li></ul>')
645
646                        ul_li_open += 1
647                        divs_open += 1
648                        new_line.append(f"<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Delete</span></div><div class\"changes\"><pre>{n}")
649                    elif n.startswith('[~]'):
650                        if ul_li_open > 0:
651                            ul_li_open = 0
652                            divs_open -= 1
653                            new_line.append('</pre></div></li></ul>')
654
655                        ul_li_open += 1
656                        divs_open += 1
657                        if 'replace' in n or ((len(l)-1) >= ndx+1 and 'replace' in l[ndx+1]):
658                            new_line.append(f"<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">REPLACEMENT</span></div><div class\"changes\"><pre>{n}")
659                        else:
660                            new_line.append(f"<ul class=\"actions\"><li class=\"update\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Update</span></div><div class\"changes\"><pre>{n}")
661
662                    else:
663                        new_line.append(f"{n}")
664
665                elif 'IAM' in diff_type:
666                    if divs_open <= 2:
667                        ul_li_open += 1
668                        divs_open += 1
669                        new_line.append(f"<ul class=\"actions\"><li class=\"update\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Update</span></div><div class\"changes\"><pre>{n}")
670                    else:
671                        new_line.append(f"{n}")
672
673                elif 'NoDiff' in diff_type:
674                    if divs_open <= 2:
675                        ul_li_open += 1
676                        divs_open += 1
677                        new_line.append(f"<ul class=\"actions\"><li class=\"read\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">No Diff</span></div><div class\"changes\"><pre>{n}")
678                    else:
679                        new_line.append(f"{n}")
680
681        while ul_li_open > 0:
682            ul_li_open -= 1
683            divs_open -= 1
684            new_line.append('</pre></div></li></ul>')
685
686        while divs_open > 0:
687            divs_open -= 1
688            new_line.append('</div>')
689
690        stacks[idx] = '\n'.join(new_line)
691
692    #
693    # If there are no stacks, then we can print this as a giant error
694    #
695    if not stacks:
696        print("cdk-diff-pretty.__main__(): ERROR: No stacks found. Outputting diff as ERROR.")
697        stacks.append("<div class=\"stack\"><h2>CDK DIFF ERROR</h2><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
698        stacks.append("<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">CDK DIFF ERROR</span></div><div class\"changes\"><pre>")
699        stacks.append(f"{cdk_out}")
700        stacks.append('</pre></div></li></ul>')
701        stacks.append('</div></div>')
702
703    html_template = html_template.replace('<h1>prettyplan</h1>', '<h1>CDK Diff</h1>')
704    html_template = html_template.replace('<title>prettyplan</title>', '<title>CDK Diff</title>')
705
706    html_template = html_template.replace('<div id="stacks"></div>', f"<div id=\"stacks\">{' '.join(stacks)}</div>")
707
708    if _VERBOSE:
709        loggy.info(f"diff_pretty(): Writing output file to {_CDK_HTML_OUTPUT_FILE}")
710
711    Path(_CDK_HTML_OUTPUT_FILE).write_text(html_template)
712    if _VERBOSE:
713        loggy.info("diff_pretty(): END")
714
715    return True
716
717
718def get_cdk_installed_version() -> str:
719    """
720    get_cdk_installed_version()
721
722    Get the CDK installed version.
723
724    Returns: String representing the installed version
725    """
726
727    loggy.info("cdk.get_cdk_installed_version(): BEGIN")
728    process_output = subprocess.run(['cdk', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
729    loggy.info("cdk.get_cdk_installed_version(): BEGIN")
730    return process_output.stdout.decode().split(' ')[0]
731
732
733def get_cdk_required_version() -> str:
734    """
735    get_cdk_required_version()
736
737    Get the CDK required version. If `cdk_lock_version` file exists, pulls the
738    version value from there. Otherwise, returns the installed version of CDK.
739
740    Returns: String representing the required version
741    """
742    loggy.info("cdk.get_cdk_required_version(): BEGIN")
743    if os.path.exists('cdk_lock_version'):
744        _CDK_REQUIRED_VERSION = Path('cdk_lock_version').read_text().strip()
745    else:
746        _CDK_REQUIRED_VERSION = get_cdk_installed_version()
747
748    loggy.info("cdk.get_cdk_required_version(): END")
749    return _CDK_REQUIRED_VERSION
750
751
752def set_cdk_installed_version() -> str:
753    """
754    set_cdk_installed_version()
755
756    Set the CDK installed version. If the required version of CDK is different
757    from the installed version, run the npm commands to install the correct
758    version.
759
760    Returns: String representing the required version
761    """
762
763    loggy.info("cdk.set_cdk_installed_version(): BEGIN")
764    _CDK_REQUIRED_VERSION = get_cdk_required_version()
765    _CDK_INSTALLED_VERSION = get_cdk_installed_version()
766    loggy.info("cdk.set_cdk_installed_version(): _CDK_REQUIRED_VERSION: " + _CDK_REQUIRED_VERSION)
767    loggy.info("cdk.set_cdk_installed_version(): _CDK_INSTALLED_VERSION: " + _CDK_INSTALLED_VERSION)
768
769    if _CDK_REQUIRED_VERSION != _CDK_INSTALLED_VERSION:
770        subprocess.run(['sudo', 'npm', 'install', '-g', 'aws-cdk@' + _CDK_REQUIRED_VERSION], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
771
772    loggy.info("cdk.set_cdk_installed_version(): END")
773    return _CDK_REQUIRED_VERSION
774
775
776def install_cdk_requirements(cdk_lang: str) -> bool:
777    """
778    install_cdk_requirements()
779
780    Install requirements for the specific type/language of cdk deployment.
781
782    cdk_lang: String representing the type of CDK code to deploy
783
784    Returns: True/False
785    """
786    loggy.info("cdk.install_cdk_requirements(): BEGIN")
787    _cdk_required_version = set_cdk_installed_version()
788    if cdk_lang == 'python':
789        loggy.info("cdk.install_cdk_requirements(): Installing python requirements.")
790
791        if os.path.exists('setup.py'):
792            loggy.info("cdk.install_cdk_requirements(): Original setup.py file.")
793            _file_contents = Path('setup.py').read_text()
794            loggy.info(_file_contents)
795
796            #
797            # TAW 20220529 - Setting required version changes between cdk v1 and v2
798            #
799            if _cdk_required_version.startswith('1'):
800                loggy.info("cdk.install_cdk_requirements(): Detected cdk v1. Adding version number to aws_cdk.aws_*.")
801                _file_contents = re.sub('aws_cdk.aws_(.*)"', r'aws_cdk.aws_\1=='+_cdk_required_version+'"', _file_contents)
802            elif _cdk_required_version.startswith('2'):
803                loggy.info("cdk.install_cdk_requirements(): Detected cdk v2. Adding version number to aws-cdk-lib*.")
804                _file_contents = re.sub('aws-cdk-lib(.*)"', r'aws-cdk-lib\1=='+_cdk_required_version+'"', _file_contents)
805            else:
806                loggy.info("cdk.install_cdk_requirements(): Detected unknown cdk version. You might need to modify cdk.py in gocd library to support this.")
807
808            with open('setup.py', 'w') as file:
809                file.write(_file_contents)
810
811            # subprocess.run(['sed', '-i', '-E', '\'s|aws_cdk.aws_(.*)"|aws_cdk.aws_\1=='+_cdk_required_version+'"|g\'', 'setup.py'], check=True)
812            # sed -i -E 's|aws_cdk.aws_(.*)"|aws_cdk.aws_\1=='$_CDK_REQUIRED_VERSION'"|g' setup.py
813
814            loggy.info("cdk.install_cdk_requirements(): Modified setup.py file.")
815            _new_file_contents = Path('setup.py').read_text()
816            loggy.info(_new_file_contents)
817
818            subprocess.run(['pip3', 'install', '-r', 'requirements.txt'], check=True)
819        else:
820            loggy.info("cdk.install_cdk_requirements(): Error. setup.py does not exist.")
821    elif cdk_lang == 'ts' or cdk_lang == 'typescript':
822        loggy.info("cdk.install_cdk_requirements(): Installing npm requirements.")
823        subprocess.run(['npm', 'install'], check=True)
824    else:
825        loggy.info(f"cdk_lang ({cdk_lang}) unsupported.")
826        return False
827
828    loggy.info("cdk.install_cdk_requirements(): END")
829    return True
def deploy( properties_env: str, lang: Union[str, NoneType] = None, path: Union[str, NoneType] = None) -> bool:
34def deploy(properties_env: str, lang: typing.Optional[str] = None, path: typing.Optional[str] = None) -> bool:
35    """
36    deploy()
37
38    Runs `cdk deploy` using the properties_env to locate the `properties.env.json` file
39
40    properties_env: String containing env name to locate the properties file
41    lang: String representing the type of CDK code to deploy
42    path: String (Optional) Will search for the cdk.json file recursively if this path is not set.
43
44    Returns: True/False
45    """
46
47    _CDK_PATH = path if path is not None else os.path.dirname(glob.glob('**/cdk.json', recursive=True)[0])
48    _PROPS_FILE = f"properties.{properties_env}.json"
49    _CDK_DEPLOY_FILE = f"{os.getcwd()}/cdk.deploy.txt"
50
51    with _chdir(_CDK_PATH):
52        env_file = Path(_PROPS_FILE).read_text()
53        # env_data = json.load(env_file)
54        loggy.info(f"cdk.deploy(): {env_file}")
55
56        if not install_cdk_requirements(cdk_lang=lang):
57            loggy.info("cdk.deploy(): Failed to install cdk requirements. Check logs.")
58            return False
59
60        my_env = os.environ.copy()
61        my_env["ENVIRONMENT"] = properties_env
62
63        _process_output = _long_run(['cdk', 'deploy', '--require-approval', 'never', '--all'], env=my_env, check=False)
64        loggy.info("----------------------------------")
65        loggy.info(f"cdk.deploy(): CDK returned {str(_process_output.returncode)}")
66
67        with open(_CDK_DEPLOY_FILE, 'w') as file:
68            if _process_output.stderr:
69                file.write(_process_output.stderr)
70
71            if _process_output.stdout:
72                file.write(_process_output.stdout)
73
74        if _process_output.returncode != 0:
75            return False
76
77    return True

deploy()

Runs cdk deploy using the properties_env to locate the properties.env.json file

properties_env: String containing env name to locate the properties file lang: String representing the type of CDK code to deploy path: String (Optional) Will search for the cdk.json file recursively if this path is not set.

Returns: True/False

def diff( properties_env: str, lang: Union[str, NoneType] = None, path: Union[str, NoneType] = None) -> bool:
 80def diff(properties_env: str, lang: typing.Optional[str] = None, path: typing.Optional[str] = None) -> bool:
 81    """
 82    diff()
 83
 84    Runs `cdk diff` using the properties_env to locate the `properties.env.json` file
 85
 86    properties_env: String containing env name to locate the properties file
 87    lang: String representing the type of CDK code to deploy (python, ts, typescript)
 88    path: String (Optional) Will search for the cdk.json file recursively if this path is not set.
 89
 90    Returns: True/False
 91    """
 92
 93    _CDK_PATH = path if path is not None else os.path.dirname(glob.glob('**/cdk.json', recursive=True)[0])
 94    _PROPS_FILE = f"properties.{properties_env}.json"
 95    _CDK_DIFF_FILE = f"{os.getcwd()}/cdk.diff.txt"
 96
 97    _EXIT = True
 98    with _chdir(_CDK_PATH):
 99        env_file = Path(_PROPS_FILE).read_text()
100        # env_data = json.load(env_file)
101        loggy.info(f"cdk.diff(): {env_file}")
102
103        if not install_cdk_requirements(cdk_lang=lang):
104            loggy.info("cdk.diff(): Failed to install cdk requirements. Check logs.")
105            return False
106
107        my_env = os.environ.copy()
108        my_env["ENVIRONMENT"] = properties_env
109
110        _process_output = _long_run(['cdk', 'diff', '--fail', '--verbose'], env=my_env, check=False)
111        loggy.info("----------------------------------")
112        loggy.info(f"cdk.diff(): CDK returned {str(_process_output.returncode)}")
113        # loggy.info("----------------------------------")
114        # loggy.info(f"cdk.diff(): CDK returned {str(_process_output.stderr)}")
115        # loggy.info("----------------------------------")
116        # loggy.info(f"cdk.diff(): CDK returned {str(_process_output.stdout)}")
117        # loggy.info("----------------------------------")
118
119        if _process_output.returncode != 0:
120            loggy.info("cdk.diff(): Testing for CDK Diff or Error.")
121
122            with open(_CDK_DIFF_FILE, 'w') as file:
123                file.write(_process_output.stderr)
124
125            if '[~]' in _process_output.stderr or '[+]' in _process_output.stderr or '[-]' in _process_output.stderr or '[=]' in _process_output.stderr:
126                loggy.info("cdk.diff(): CDK Diff found!")
127                _EXIT = True
128            else:
129                loggy.info("cdk.diff(): CDK ERROR!")
130                for stack in glob.glob('cdk.out/*.json'):
131                    loggy.info("----------------------------------")
132                    loggy.info("STACK: " + stack)
133                    _stack_yaml = yaml.dump(json.loads(Path(stack).read_text()))
134                    loggy.info(_stack_yaml)
135                _EXIT = False
136
137        else:
138            # if 'DEPLOY_OVERRIDE' in os.environ.keys():
139            #     print("cdkDiff.__main__(): No CDK diff! Overriding CDK_DIFF because DEPLOY_OVERRIDE")
140            #     _EXIT = 0
141            # else:
142            with open(_CDK_DIFF_FILE, 'w') as file:
143                file.write("NO CDK diff found. This could be an AMI SSM Param change. Deploy proceding.")
144
145            loggy.info("cdk.diff(): NO CDK diff found. This could be an AMI SSM Param change. Deploy proceding.")
146            _EXIT = True
147
148    return _EXIT

diff()

Runs cdk diff using the properties_env to locate the properties.env.json file

properties_env: String containing env name to locate the properties file lang: String representing the type of CDK code to deploy (python, ts, typescript) path: String (Optional) Will search for the cdk.json file recursively if this path is not set.

Returns: True/False

def diff_pretty( diff_file: str = 'cdk.diff.txt', output_file: str = 'cdk.diff.html', verbose: bool = False) -> bool:
151def diff_pretty(diff_file: str='cdk.diff.txt', output_file: str='cdk.diff.html', verbose: bool=False) -> bool:
152
153    _CDK_DIFF_FILE = diff_file
154    _CDK_HTML_OUTPUT_FILE = output_file
155    _VERBOSE = verbose
156
157    html_template = """
158<!DOCTYPE html>
159<html>
160  <head>
161    <meta charset="utf-8" />
162    <title>prettyplan</title>
163    <meta name="viewport" content="width=device-width, initial-scale=1" />
164    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
165    <style>
166      body {
167        font-family: Arial, Helvetica, sans-serif;
168        text-rendering: optimizeLegibility;
169        background: #ecf7fe;
170        color: #000000c0;
171        font-size: 15px;
172        margin: 0;
173      }
174      @keyframes fade-in {
175        0% {
176          opacity: 0;
177        }
178        100% {
179          opacity: 1;
180        }
181      }
182      .stripe {
183        width: 100%;
184        height: 5px;
185        background: #5c4ce4;
186        animation-name: wipe-in;
187        animation-duration: 1s;
188      }
189      @keyframes wipe-in {
190        0% {
191          width: 0%;
192        }
193        100% {
194          width: 100%;
195        }
196      }
197      #release-notification {
198        background: #5c4ce4;
199        color: white;
200        font-weight: bold;
201        text-align: center;
202        overflow: hidden;
203        padding: 10px 0 15px 0;
204        height: 20px;
205        animation-name: notification-pop-in;
206        animation-duration: 2s;
207      }
208      #release-notification a {
209        color: white;
210      }
211      #release-notification.dismissed {
212        animation-name: notification-pop-out;
213        animation-duration: 0.5s;
214        height: 0;
215        padding: 0;
216      }
217      @keyframes notification-pop-in {
218        0% {
219          height: 0;
220          padding: 0;
221        }
222        50% {
223          height: 0;
224          padding: 0;
225        }
226      }
227      @keyframes notification-pop-out {
228        0% {
229          height: 20px;
230          padding: 10px 0 15px 0;
231        }
232        100% {
233          height: 0;
234          padding: 0;
235        }
236      }
237      #modal-container {
238        animation-name: fade-in;
239        animation-duration: 0.2s;
240      }
241      .modal-pane {
242        position: fixed;
243        top: 0;
244        left: 0;
245        width: 100%;
246        height: 100%;
247        background: #ffffffe6;
248        z-index: 10;
249      }
250      .modal-content {
251        position: fixed;
252        width: 60%;
253        height: 60%;
254        top: 50%;
255        left: 50%;
256        transform: translate(-50%, -50%);
257        background: #ffffff;
258        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
259        z-index: 20;
260      }
261      .modal-close {
262        position: absolute;
263        right: 0;
264        padding: 10px;
265      }
266      .modal-close button.text-button {
267        color: #4526ac;
268        text-decoration: none;
269        font-weight: normal;
270      }
271      .release-notes {
272        max-width: 80%;
273        margin: 0 auto 0 auto;
274        overflow-y: auto;
275        max-height: 100%;
276      }
277      #branding {
278        float: right;
279        padding-top: 10px;
280        padding-right: 10px;
281        font-size: 10px;
282        color: #4526ac;
283        text-align: right;
284      }
285      #branding a {
286        color: #4526ac;
287      }
288      .container {
289        margin: 10px 10px 0 10px;
290        animation-name: fade-in;
291        animation-duration: 1s;
292      }
293      @media only screen and (min-width: 600px) {
294        .container {
295          max-width: 80%;
296          margin-left: auto;
297          margin-right: auto;
298        }
299      }
300      h1,
301      h2 {
302        text-align: center;
303        color: #4526ac;
304      }
305      #terraform-plan {
306        width: 100%;
307        min-height: 300px;
308        border: none;
309        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
310        padding: 10px;
311        margin-bottom: 10px;
312        resize: none;
313        background: #ffffffe6;
314      }
315      button {
316        font-size: 18px;
317        background: #5c4ce4;
318        color: #fff;
319        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
320        border: none;
321        border-radius: 2px;
322        min-width: 170px;
323        height: 40px;
324      }
325      button:hover {
326        background: #6567ea;
327        cursor: pointer;
328      }
329      button:active {
330        background: #5037ca;
331      }
332      button.text-button {
333        background: none;
334        box-shadow: none;
335        border-radius: 0;
336        width: auto;
337        height: auto;
338        text-decoration: underline;
339        font-size: inherit;
340        font-weight: inherit;
341        font-family: Arial, Helvetica, sans-serif;
342        color: inherit;
343        text-align: inherit;
344        padding: 0;
345      }
346      #parsing-error-message {
347        background-color: #ffffff;
348        padding: 10px;
349        color: #000000c0;
350        margin: 4px;
351        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
352        font-weight: bold;
353        border-left: 2px solid red;
354        animation-name: error;
355        animation-duration: 1s;
356      }
357      @keyframes error {
358        0% {
359          background-color: red;
360        }
361        100% {
362          background-color: white;
363        }
364      }
365      .prettyplan ul {
366        padding-left: 0;
367        font-size: 13px;
368      }
369      .prettyplan li {
370        list-style: none;
371        background: #ffffffe6;
372        padding: 10px;
373        color: #000000c0;
374        margin: 4px;
375        box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
376      }
377      .prettyplan ul.warnings li {
378        border-left: 3px solid #757575;
379      }
380      .prettyplan ul.actions li.update {
381        border-left: 3px solid #ff8f00;
382      }
383      .prettyplan ul.actions li.create {
384        border-left: 3px solid #2e7d32;
385      }
386      .prettyplan ul.actions li.addition {
387        border-left: 3px solid #2e7d32;
388      }
389      .prettyplan ul.actions li.destroy {
390        border-left: 3px solid #b71c1c;
391      }
392      .prettyplan ul.actions li.removal {
393        border-left: 3px solid #b71c1c;
394      }
395      .prettyplan ul.actions li.recreate {
396        border-left: 3px solid #1565c0;
397      }
398      .prettyplan ul.actions li.read {
399        border-left: 3px solid #519bf0;
400      }
401      .badge {
402        display: inline-block;
403        text-transform: uppercase;
404        margin-right: 10px;
405        padding: 3px;
406        font-size: 12px;
407        font-weight: bold;
408      }
409      .warnings .badge {
410        color: #757575;
411      }
412      li.update .badge {
413        color: #ff8f00;
414      }
415      li.create .badge {
416        color: #2e7d32;
417      }
418      li.addition .badge {
419        color: #2e7d32;
420      }
421      li.destroy .badge {
422        color: #b71c1c;
423      }
424      li.removal .badge {
425        color: #b71c1c;
426      }
427      li.recreate .badge {
428        color: #1565c0;
429      }
430      li.read .badge {
431        color: #519bf0;
432      }
433      .id-segment:not(:last-child)::after {
434        content: ' > ';
435      }
436      .id-segment.name,
437      .id-segment.type {
438        font-weight: bold;
439      }
440      .change-count {
441        float: right;
442      }
443      .summary {
444        cursor: pointer;
445      }
446      .no-diff-changes-breakdown {
447        margin: 5px auto 0 auto;
448        padding: 5px;
449      }
450      .no-diff-changes-breakdown table {
451        width: 100%;
452        word-break: break-all;
453        font-size: 13px;
454      }
455      .no-diff-changes-breakdown table td {
456        padding: 10px;
457        width: 40%;
458      }
459      pre {
460        white-space: pre-wrap;
461        background: #f3f3f3;
462      }
463      .no-diff-changes-breakdown table td.property {
464        width: 20%;
465        text-align: right;
466        font-weight: bold;
467      }
468      .no-diff-changes-breakdown table tr:nth-child(even) {
469        background-color: #f5f5f5;
470      }
471      .forces-new-resource {
472        color: #b71c1c;
473      }
474      .collapsed,
475      .hidden {
476        display: none;
477      }
478      .actions button {
479        background: none;
480        border: none;
481        text-decoration: underline;
482        color: black;
483        box-shadow: none;
484        font-weight: bold;
485        font-size: 14px;
486      }
487      .d2h-icon {
488        display: none;
489      }
490    </style>
491  </head>
492  <body>
493    <div class="stripe"></div>
494    <div class="container">
495      <h1>prettyplan</h1>
496      <div id="parsing-error-message" class="hidden">
497        That doesn't look like a Terraform plan. Did you copy the entire output (without colouring) from the plan
498        command?
499      </div>
500      <div id="prettyplan" class="prettyplan">
501        <ul id="errors" class="errors"></ul>
502        <ul id="warnings" class="warnings"></ul>
503        <button class="expand-all" onclick="expandAll()">Expand all</button>
504        <button class="collapse-all hidden" onclick="collapseAll()">Collapse all</button>
505        <div id="stacks"></div>
506        <ul id="actions" class="actions"></ul>
507        <pre id="diff"></pre>
508      </div>
509    </div>
510    <script>
511      function accordion(element) {
512        const changes = element.parentElement.getElementsByClassName('changes');
513        for (var i = 0; i < changes.length; i++) {
514          toggleClass(changes[i], 'collapsed');
515        }
516      }
517      function toggleClass(element, className) {
518        if (!element.className.match(className)) {
519          element.className += ' ' + className;
520        } else {
521          element.className = element.className.replace(className, '');
522        }
523      }
524      function addClass(element, className) {
525        if (!element.className.match(className)) element.className += ' ' + className;
526      }
527      function removeClass(element, className) {
528        element.className = element.className.replace(className, '');
529      }
530      function expandAll() {
531        const sections = document.querySelectorAll('.changes.collapsed');
532        for (var i = 0; i < sections.length; i++) {
533          toggleClass(sections[i], 'collapsed');
534        }
535        toggleClass(document.querySelector('.expand-all'), 'hidden');
536        toggleClass(document.querySelector('.collapse-all'), 'hidden');
537      }
538      function collapseAll() {
539        const sections = document.querySelectorAll('.changes:not(.collapsed)');
540        for (var i = 0; i < sections.length; i++) {
541          toggleClass(sections[i], 'collapsed');
542        }
543        toggleClass(document.querySelector('.expand-all'), 'hidden');
544        toggleClass(document.querySelector('.collapse-all'), 'hidden');
545      }
546      function removeChildren(element) {
547        while (element.lastChild) {
548          element.removeChild(element.lastChild);
549        }
550      }
551      function createModalContainer() {
552        const modalElement = document.createElement('div');
553        modalElement.id = 'modal-container';
554        document.body.appendChild(modalElement);
555        return modalElement;
556      }
557      function closeModal() {
558        const modalElement = document.getElementById('modal-container');
559        document.body.removeChild(modalElement);
560      }
561    </script>
562  </body>
563</html>
564"""
565
566    if _VERBOSE:
567        loggy.info("diff_pretty()): BEGIN")
568
569    if not Path(_CDK_DIFF_FILE).exists():
570        loggy.info(f"diff_pretty(): ERROR. INPUT FILE ({_CDK_DIFF_FILE}) does NOT exist.")
571        return False
572
573    if _VERBOSE:
574        loggy.info(f"diff_pretty(): Reading input file: {_CDK_DIFF_FILE}")
575
576    cdk_out = Path(_CDK_DIFF_FILE).read_text()
577
578    if _VERBOSE:
579        loggy.info(f"diff_pretty(): {cdk_out}")
580
581    #
582    # Here, we split the stack output into an array based on `Stack xyz` lines
583    #
584    stacks = [f"Stack {e}" for e in cdk_out.split('Stack ') if e]
585
586    #
587    # This is a Hack to get rid of any warnings at the top of the output before
588    # we encounter our first `Stack xyz` line.
589    #
590    for idx, l in enumerate(stacks):
591        match = False
592        for hack_line in cdk_out.split('\n'):
593            if l.split('\n')[0] in hack_line:
594                match = True
595                break
596
597        if not match:
598            del stacks[idx]
599            continue
600
601    #
602    # Convert each `Stack xyz` line to our HTML format
603    #
604    for idx, l in enumerate(stacks):
605        new_line = []
606        diff_type = ""
607        divs_open = 0
608        ul_li_open = 0
609        for ndx, n in enumerate(l.split('\n')):
610            if _VERBOSE:
611                loggy.info(f"diff_pretty(): DEBUG: ({str(idx)})-({str(ndx)}) - {str(n)}")
612
613            if ndx == 0:
614                # new_line.append(f"<div class=\"stack\"><h2>{n}</h2><div class=\"raw-diff\"><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
615
616                #
617                # 2 open divs
618                #
619                divs_open += 2
620                new_line.append(f"<div class=\"stack\"><h2>{n}</h2><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
621            else:
622                if n.startswith('IAM Policy Changes'):
623                    diff_type = "IAM"
624                elif n.startswith('Resources'):
625                    diff_type = "Resources"
626                elif n.startswith('Outputs'):
627                    diff_type = "Outputs"
628                elif n.startswith('There were no differences'):
629                    diff_type = "NoDiff"
630
631                if 'Resources' in diff_type or 'Outputs' in diff_type:
632                    if n.startswith('[+]'):
633                        if ul_li_open > 0:
634                            ul_li_open = 0
635                            divs_open -= 1
636                            new_line.append('<pre></div></li></ul>')
637
638                        ul_li_open += 1
639                        divs_open += 1
640                        new_line.append(f"<ul class=\"actions\"><li class=\"create\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Addition</span></div><div class\"changes\"><pre>{n}")
641                    elif n.startswith('[-]'):
642                        if ul_li_open > 0:
643                            ul_li_open = 0
644                            divs_open -= 1
645                            new_line.append('</pre></div></li></ul>')
646
647                        ul_li_open += 1
648                        divs_open += 1
649                        new_line.append(f"<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Delete</span></div><div class\"changes\"><pre>{n}")
650                    elif n.startswith('[~]'):
651                        if ul_li_open > 0:
652                            ul_li_open = 0
653                            divs_open -= 1
654                            new_line.append('</pre></div></li></ul>')
655
656                        ul_li_open += 1
657                        divs_open += 1
658                        if 'replace' in n or ((len(l)-1) >= ndx+1 and 'replace' in l[ndx+1]):
659                            new_line.append(f"<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">REPLACEMENT</span></div><div class\"changes\"><pre>{n}")
660                        else:
661                            new_line.append(f"<ul class=\"actions\"><li class=\"update\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Update</span></div><div class\"changes\"><pre>{n}")
662
663                    else:
664                        new_line.append(f"{n}")
665
666                elif 'IAM' in diff_type:
667                    if divs_open <= 2:
668                        ul_li_open += 1
669                        divs_open += 1
670                        new_line.append(f"<ul class=\"actions\"><li class=\"update\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">Update</span></div><div class\"changes\"><pre>{n}")
671                    else:
672                        new_line.append(f"{n}")
673
674                elif 'NoDiff' in diff_type:
675                    if divs_open <= 2:
676                        ul_li_open += 1
677                        divs_open += 1
678                        new_line.append(f"<ul class=\"actions\"><li class=\"read\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">No Diff</span></div><div class\"changes\"><pre>{n}")
679                    else:
680                        new_line.append(f"{n}")
681
682        while ul_li_open > 0:
683            ul_li_open -= 1
684            divs_open -= 1
685            new_line.append('</pre></div></li></ul>')
686
687        while divs_open > 0:
688            divs_open -= 1
689            new_line.append('</div>')
690
691        stacks[idx] = '\n'.join(new_line)
692
693    #
694    # If there are no stacks, then we can print this as a giant error
695    #
696    if not stacks:
697        print("cdk-diff-pretty.__main__(): ERROR: No stacks found. Outputting diff as ERROR.")
698        stacks.append("<div class=\"stack\"><h2>CDK DIFF ERROR</h2><button onclick=\"accordion(this)\">Expand this Stack</button><div class=\"changes\">")
699        stacks.append("<ul class=\"actions\"><li class=\"destroy\"><div class=\"summary\" onclick=\"accordion(this)\"><span class=\"badge\">CDK DIFF ERROR</span></div><div class\"changes\"><pre>")
700        stacks.append(f"{cdk_out}")
701        stacks.append('</pre></div></li></ul>')
702        stacks.append('</div></div>')
703
704    html_template = html_template.replace('<h1>prettyplan</h1>', '<h1>CDK Diff</h1>')
705    html_template = html_template.replace('<title>prettyplan</title>', '<title>CDK Diff</title>')
706
707    html_template = html_template.replace('<div id="stacks"></div>', f"<div id=\"stacks\">{' '.join(stacks)}</div>")
708
709    if _VERBOSE:
710        loggy.info(f"diff_pretty(): Writing output file to {_CDK_HTML_OUTPUT_FILE}")
711
712    Path(_CDK_HTML_OUTPUT_FILE).write_text(html_template)
713    if _VERBOSE:
714        loggy.info("diff_pretty(): END")
715
716    return True
def get_cdk_installed_version() -> str:
719def get_cdk_installed_version() -> str:
720    """
721    get_cdk_installed_version()
722
723    Get the CDK installed version.
724
725    Returns: String representing the installed version
726    """
727
728    loggy.info("cdk.get_cdk_installed_version(): BEGIN")
729    process_output = subprocess.run(['cdk', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
730    loggy.info("cdk.get_cdk_installed_version(): BEGIN")
731    return process_output.stdout.decode().split(' ')[0]

get_cdk_installed_version()

Get the CDK installed version.

Returns: String representing the installed version

def get_cdk_required_version() -> str:
734def get_cdk_required_version() -> str:
735    """
736    get_cdk_required_version()
737
738    Get the CDK required version. If `cdk_lock_version` file exists, pulls the
739    version value from there. Otherwise, returns the installed version of CDK.
740
741    Returns: String representing the required version
742    """
743    loggy.info("cdk.get_cdk_required_version(): BEGIN")
744    if os.path.exists('cdk_lock_version'):
745        _CDK_REQUIRED_VERSION = Path('cdk_lock_version').read_text().strip()
746    else:
747        _CDK_REQUIRED_VERSION = get_cdk_installed_version()
748
749    loggy.info("cdk.get_cdk_required_version(): END")
750    return _CDK_REQUIRED_VERSION

get_cdk_required_version()

Get the CDK required version. If cdk_lock_version file exists, pulls the version value from there. Otherwise, returns the installed version of CDK.

Returns: String representing the required version

def set_cdk_installed_version() -> str:
753def set_cdk_installed_version() -> str:
754    """
755    set_cdk_installed_version()
756
757    Set the CDK installed version. If the required version of CDK is different
758    from the installed version, run the npm commands to install the correct
759    version.
760
761    Returns: String representing the required version
762    """
763
764    loggy.info("cdk.set_cdk_installed_version(): BEGIN")
765    _CDK_REQUIRED_VERSION = get_cdk_required_version()
766    _CDK_INSTALLED_VERSION = get_cdk_installed_version()
767    loggy.info("cdk.set_cdk_installed_version(): _CDK_REQUIRED_VERSION: " + _CDK_REQUIRED_VERSION)
768    loggy.info("cdk.set_cdk_installed_version(): _CDK_INSTALLED_VERSION: " + _CDK_INSTALLED_VERSION)
769
770    if _CDK_REQUIRED_VERSION != _CDK_INSTALLED_VERSION:
771        subprocess.run(['sudo', 'npm', 'install', '-g', 'aws-cdk@' + _CDK_REQUIRED_VERSION], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
772
773    loggy.info("cdk.set_cdk_installed_version(): END")
774    return _CDK_REQUIRED_VERSION

set_cdk_installed_version()

Set the CDK installed version. If the required version of CDK is different from the installed version, run the npm commands to install the correct version.

Returns: String representing the required version

def install_cdk_requirements(cdk_lang: str) -> bool:
777def install_cdk_requirements(cdk_lang: str) -> bool:
778    """
779    install_cdk_requirements()
780
781    Install requirements for the specific type/language of cdk deployment.
782
783    cdk_lang: String representing the type of CDK code to deploy
784
785    Returns: True/False
786    """
787    loggy.info("cdk.install_cdk_requirements(): BEGIN")
788    _cdk_required_version = set_cdk_installed_version()
789    if cdk_lang == 'python':
790        loggy.info("cdk.install_cdk_requirements(): Installing python requirements.")
791
792        if os.path.exists('setup.py'):
793            loggy.info("cdk.install_cdk_requirements(): Original setup.py file.")
794            _file_contents = Path('setup.py').read_text()
795            loggy.info(_file_contents)
796
797            #
798            # TAW 20220529 - Setting required version changes between cdk v1 and v2
799            #
800            if _cdk_required_version.startswith('1'):
801                loggy.info("cdk.install_cdk_requirements(): Detected cdk v1. Adding version number to aws_cdk.aws_*.")
802                _file_contents = re.sub('aws_cdk.aws_(.*)"', r'aws_cdk.aws_\1=='+_cdk_required_version+'"', _file_contents)
803            elif _cdk_required_version.startswith('2'):
804                loggy.info("cdk.install_cdk_requirements(): Detected cdk v2. Adding version number to aws-cdk-lib*.")
805                _file_contents = re.sub('aws-cdk-lib(.*)"', r'aws-cdk-lib\1=='+_cdk_required_version+'"', _file_contents)
806            else:
807                loggy.info("cdk.install_cdk_requirements(): Detected unknown cdk version. You might need to modify cdk.py in gocd library to support this.")
808
809            with open('setup.py', 'w') as file:
810                file.write(_file_contents)
811
812            # subprocess.run(['sed', '-i', '-E', '\'s|aws_cdk.aws_(.*)"|aws_cdk.aws_\1=='+_cdk_required_version+'"|g\'', 'setup.py'], check=True)
813            # sed -i -E 's|aws_cdk.aws_(.*)"|aws_cdk.aws_\1=='$_CDK_REQUIRED_VERSION'"|g' setup.py
814
815            loggy.info("cdk.install_cdk_requirements(): Modified setup.py file.")
816            _new_file_contents = Path('setup.py').read_text()
817            loggy.info(_new_file_contents)
818
819            subprocess.run(['pip3', 'install', '-r', 'requirements.txt'], check=True)
820        else:
821            loggy.info("cdk.install_cdk_requirements(): Error. setup.py does not exist.")
822    elif cdk_lang == 'ts' or cdk_lang == 'typescript':
823        loggy.info("cdk.install_cdk_requirements(): Installing npm requirements.")
824        subprocess.run(['npm', 'install'], check=True)
825    else:
826        loggy.info(f"cdk_lang ({cdk_lang}) unsupported.")
827        return False
828
829    loggy.info("cdk.install_cdk_requirements(): END")
830    return True

install_cdk_requirements()

Install requirements for the specific type/language of cdk deployment.

cdk_lang: String representing the type of CDK code to deploy

Returns: True/False