...

Text file src/github.com/datawire/ambassador/v2/python/ambassador_cli/grab_snapshots.py

Documentation: github.com/datawire/ambassador/v2/python/ambassador_cli

     1#!python
     2
     3# Copyright 2019-2020 Datawire. All rights reserved.
     4#
     5# Licensed under the Apache License, Version 2.0 (the "License");
     6# you may not use this file except in compliance with the License.
     7# You may obtain a copy of the License at
     8#
     9#     http://www.apache.org/licenses/LICENSE-2.0
    10#
    11# Unless required by applicable law or agreed to in writing, software
    12# distributed under the License is distributed on an "AS IS" BASIS,
    13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14# See the License for the specific language governing permissions and
    15# limitations under the License
    16
    17########
    18# This is a debugging tool that can grab snapshots and Envoy configs from
    19# Ambassador's configuration directory, sanitize secrets out of the snapshots,
    20# and hand back a compressed tarfile that the user can hand back to Datawire.
    21########
    22
    23import functools
    24import glob
    25import json
    26import os
    27import sys
    28import tarfile
    29
    30import click
    31
    32from ambassador.utils import dump_json
    33
    34# Use this instead of click.option
    35click_option = functools.partial(click.option, show_default=True)
    36click_option_no_default = functools.partial(click.option, show_default=False)
    37
    38
    39def sanitize_snapshot(snapshot: dict):
    40    sanitized = {}
    41
    42    # Consul is pretty easy. Just sort, using service-dc as the sort key.
    43    consul_elements = snapshot.get("Consul")
    44
    45    if consul_elements:
    46        csorted = {}
    47
    48        for key, value in consul_elements.items():
    49            csorted[key] = sorted(value, key=lambda x: f'{x["Service"]-x["Id"]}')
    50
    51        sanitized["Consul"] = csorted
    52
    53    # Make sure we grab Deltas and Invalid -- these should really be OK as-is.
    54
    55    for key in ["Deltas", "Invalid"]:
    56        if key in snapshot:
    57            sanitized[key] = snapshot[key]
    58
    59    # Kube is harder because we need to sanitize Kube secrets.
    60    kube_elements = snapshot.get("Kubernetes")
    61
    62    if kube_elements:
    63        ksorted = {}
    64
    65        for key, value in kube_elements.items():
    66            if not value:
    67                continue
    68
    69            if key == "secret":
    70                for secret in value:
    71                    if "data" in secret:
    72                        data = secret["data"]
    73
    74                        for k in data.keys():
    75                            data[k] = f"-sanitized-{k}-"
    76
    77                    metadata = secret.get("metadata", {})
    78                    annotations = metadata.get("annotations", {})
    79
    80                    # Wipe the last-applied-configuration annotation, too, because it
    81                    # often contains the secret data.
    82                    if "kubectl.kubernetes.io/last-applied-configuration" in annotations:
    83                        annotations[
    84                            "kubectl.kubernetes.io/last-applied-configuration"
    85                        ] = "--sanitized--"
    86
    87            # All the sanitization above happened in-place in value, so we can just
    88            # sort it.
    89            ksorted[key] = sorted(value, key=lambda x: x.get("metadata", {}).get("name"))
    90
    91        sanitized["Kubernetes"] = ksorted
    92
    93    return sanitized
    94
    95
    96# Helper to open a snapshot.yaml and sanitize it.
    97def helper_snapshot(path: str) -> str:
    98    snapshot = json.loads(open(path, "r").read())
    99
   100    return dump_json(sanitize_snapshot(snapshot))
   101
   102
   103# Helper to open a problems.json and sanitize the snapshot it contains.
   104def helper_problems(path: str) -> str:
   105    bad_dict = json.loads(open(path, "r").read())
   106
   107    bad_dict["snapshot"] = sanitize_snapshot(bad_dict["snapshot"])
   108
   109    return dump_json(bad_dict)
   110
   111
   112# Helper to just copy a file.
   113def helper_copy(path: str) -> str:
   114    return open(path, "r").read()
   115
   116
   117# Open a tarfile for output...
   118@click.command(help="Grab, and sanitize, Ambassador snapshots for later debugging")
   119@click_option("--debug/--no-debug", default=True, help="enable debugging")
   120@click_option(
   121    "-o",
   122    "--output-path",
   123    "--output",
   124    type=click.Path(writable=True),
   125    default="sanitized.tgz",
   126    help="output path",
   127)
   128@click_option(
   129    "-s",
   130    "--snapshot-dir",
   131    "--snapshot",
   132    type=click.Path(exists=True, dir_okay=True, file_okay=False),
   133    help="snapshot directory to read",
   134)
   135def main(snapshot_dir: str, debug: bool, output_path: str) -> None:
   136    if not snapshot_dir:
   137        config_base_dir = os.environ.get("AMBASSADOR_CONFIG_BASE_DIR", "/ambassador")
   138        snapshot_dir = os.path.join(config_base_dir, "snapshots")
   139
   140    if debug:
   141        print(f"Saving sanitized snapshots from {snapshot_dir} to {output_path}")
   142
   143    with tarfile.open(output_path, "w:gz") as archive:
   144        # ...then iterate any snapshots, sanitize, and stuff 'em in the tarfile.
   145        # Note that the '.yaml' on the snapshot file name is a misnomer: when
   146        # watt is involved, they're actually JSON. It's a long story.
   147
   148        some_found = False
   149
   150        interesting_things = [
   151            ("snap*yaml", helper_snapshot),
   152            ("problems*json", helper_problems),
   153            ("econf*json", helper_copy),
   154            ("diff*txt", helper_copy),
   155        ]
   156
   157        for pattern, helper in interesting_things:
   158            for path in glob.glob(os.path.join(snapshot_dir, pattern)):
   159                some_found = True
   160
   161                # The tarfile can be flat, rather than embedding everything
   162                # in a directory with a fixed name.
   163                b = os.path.basename(path)
   164
   165                if debug:
   166                    print(f"...{b}")
   167
   168                sanitized = helper(path)
   169
   170                if sanitized:
   171                    _, ext = os.path.splitext(path)
   172                    sanitized_name = f"sanitized{ext}"
   173
   174                    with open(sanitized_name, "w") as tmp:
   175                        tmp.write(sanitized)
   176
   177                    archive.add(sanitized_name, arcname=b)
   178                    os.unlink(sanitized_name)
   179
   180        if not some_found:
   181            sys.stderr.write(f"No snapshots found in {snapshot_dir}?\n")
   182            sys.exit(1)
   183
   184        sys.exit(0)
   185
   186
   187if __name__ == "__main__":
   188    main()

View as plain text