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