new upstream release
Jonathan Carter
5 years ago
0 | # 3.4.0 | |
1 | ||
2 | 2018-05-02 | |
3 | ||
4 | * added k8s_clusterrole items | |
5 | * added k8s_clusterrolebinding items | |
6 | * added k8s_crd items | |
7 | * added k8s_networkpolicy items | |
8 | * added k8s_raw items | |
9 | * added k8s_role items | |
10 | * added k8s_rolebinding items | |
11 | * added Kubernetes item preview with `bw items -f` | |
12 | * improved handling of exceptions during `bw verify` and `bw apply` | |
13 | * improved progress display during `bw run` | |
14 | ||
15 | ||
0 | 16 | # 3.3.0 |
1 | 17 | |
2 | 18 | 2018-03-09 |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | from __future__ import unicode_literals |
2 | 2 | |
3 | VERSION = (3, 3, 0) | |
3 | VERSION = (3, 4, 0) | |
4 | 4 | VERSION_STRING = ".".join([str(v) for v in VERSION]) |
27 | 27 | |
28 | 28 | def bw_items(repo, args): |
29 | 29 | node = get_node(repo, args['node'], adhoc_nodes=args['adhoc_nodes']) |
30 | if args['file_preview'] and not args['item']: | |
31 | io.stderr(_("{x} no ITEM given for file preview").format(x=red("!!!"))) | |
30 | if args['preview'] and not args['item']: | |
31 | io.stderr(_("{x} no ITEM given for preview").format(x=red("!!!"))) | |
32 | 32 | exit(1) |
33 | 33 | elif args['file_preview_path']: |
34 | 34 | if args['item']: |
75 | 75 | )) |
76 | 76 | elif args['item']: |
77 | 77 | item = get_item(node, args['item']) |
78 | if args['file_preview']: | |
79 | if item.ITEM_TYPE_NAME != 'file': | |
78 | if args['preview']: | |
79 | try: | |
80 | io.stdout( | |
81 | item.preview(), | |
82 | append_newline=False, | |
83 | ) | |
84 | except NotImplementedError: | |
80 | 85 | io.stderr(_( |
81 | "{x} cannot preview {item} on {node} (not a file)" | |
86 | "{x} cannot preview {item} on {node} (doesn't support previews)" | |
82 | 87 | ).format(x=red("!!!"), item=item.id, node=node.name)) |
83 | 88 | exit(1) |
84 | if ( | |
85 | item.attributes['content_type'] in ('any', 'base64', 'binary') or | |
86 | item.attributes['delete'] is True | |
87 | ): | |
89 | except ValueError: | |
88 | 90 | io.stderr(_( |
89 | "{x} cannot preview {file} on {node} (unsuitable content_type or deleted)" | |
90 | ).format(x=red("!!!"), file=item.id, node=node.name)) | |
91 | "{x} cannot preview {item} on {node} (not available for this item config)" | |
92 | ).format(x=red("!!!"), item=item.id, node=node.name)) | |
91 | 93 | exit(1) |
92 | else: | |
93 | try: | |
94 | io.stdout( | |
95 | item.content.decode(item.attributes['encoding']), | |
96 | append_newline=False, | |
97 | ) | |
98 | except FaultUnavailable: | |
99 | io.stderr(_( | |
100 | "{x} skipped {path} (Fault unavailable)" | |
101 | ).format(x=yellow("ยป"), path=bold(item.name))) | |
102 | exit(1) | |
94 | except FaultUnavailable: | |
95 | io.stderr(_( | |
96 | "{x} cannot preview {item} on {node} (Fault unavailable)" | |
97 | ).format(x=red("!!!"), item=item.id, node=node.name)) | |
98 | exit(1) | |
103 | 99 | else: |
104 | 100 | if args['show_sdict']: |
105 | 101 | statedict = item.sdict() |
289 | 289 | ) |
290 | 290 | parser_items.add_argument( |
291 | 291 | "-f", |
292 | "--file-preview", | |
293 | action='store_true', | |
294 | dest='file_preview', | |
295 | help=_("print preview of given file ITEM"), | |
292 | "--preview", | |
293 | "--file-preview", # TODO 4.0 remove | |
294 | action='store_true', | |
295 | dest='preview', | |
296 | help=_("print preview of given ITEM"), | |
296 | 297 | ) |
297 | 298 | parser_items.add_argument( |
298 | 299 | "-w", |
48 | 48 | )) |
49 | 49 | return None |
50 | 50 | |
51 | result = node.run( | |
52 | command, | |
53 | may_fail=True, | |
54 | log_output=True, | |
55 | ) | |
51 | with io.job(_("{} running command...").format(bold(node.name))): | |
52 | result = node.run( | |
53 | command, | |
54 | may_fail=True, | |
55 | log_output=True, | |
56 | ) | |
56 | 57 | |
57 | 58 | node.repo.hooks.node_run_end( |
58 | 59 | node.repo, |
9 | 9 | from ..utils.text import ( |
10 | 10 | blue, |
11 | 11 | bold, |
12 | cyan, | |
13 | cyan_unless_zero, | |
12 | 14 | error_summary, |
13 | 15 | format_duration, |
14 | 16 | green, |
22 | 24 | |
23 | 25 | def stats_summary(node_stats, total_duration): |
24 | 26 | for node in node_stats.keys(): |
25 | node_stats[node]['total'] = node_stats[node]['good'] + node_stats[node]['bad'] | |
27 | node_stats[node]['total'] = sum([ | |
28 | node_stats[node]['good'], | |
29 | node_stats[node]['bad'], | |
30 | node_stats[node]['unknown'], | |
31 | ]) | |
26 | 32 | try: |
27 | 33 | node_stats[node]['health'] = \ |
28 | 34 | (node_stats[node]['good'] / float(node_stats[node]['total'])) * 100.0 |
33 | 39 | 'items': 0, |
34 | 40 | 'good': 0, |
35 | 41 | 'bad': 0, |
42 | 'unknown': 0, | |
36 | 43 | } |
37 | 44 | node_ranking = [] |
38 | 45 | |
40 | 47 | totals['items'] += stats['total'] |
41 | 48 | totals['good'] += stats['good'] |
42 | 49 | totals['bad'] += stats['bad'] |
50 | totals['unknown'] += stats['unknown'] | |
43 | 51 | node_ranking.append(( |
44 | 52 | stats['health'], |
45 | 53 | node_name, |
46 | 54 | stats['total'], |
47 | 55 | stats['good'], |
48 | 56 | stats['bad'], |
57 | stats['unknown'], | |
49 | 58 | stats['duration'], |
50 | 59 | )) |
51 | 60 | |
61 | 70 | _("items"), |
62 | 71 | green(_("good")), |
63 | 72 | red(_("bad")), |
73 | cyan(_("unknown")), | |
64 | 74 | _("health"), |
65 | 75 | _("duration"), |
66 | 76 | ], ROW_SEPARATOR] |
67 | 77 | |
68 | for health, node_name, items, good, bad, duration in node_ranking: | |
78 | for health, node_name, items, good, bad, unknown, duration in node_ranking: | |
69 | 79 | rows.append([ |
70 | 80 | node_name, |
71 | 81 | str(items), |
72 | 82 | green_unless_zero(good), |
73 | 83 | red_unless_zero(bad), |
84 | cyan_unless_zero(unknown), | |
74 | 85 | "{0:.1f}%".format(health), |
75 | 86 | format_duration(duration), |
76 | 87 | ]) |
82 | 93 | str(totals['items']), |
83 | 94 | green_unless_zero(totals['good']), |
84 | 95 | red_unless_zero(totals['bad']), |
96 | cyan_unless_zero(totals['unknown']), | |
85 | 97 | "{0:.1f}%".format(totals['health']), |
86 | 98 | format_duration(total_duration), |
87 | 99 | ]) |
93 | 105 | 4: 'right', |
94 | 106 | 5: 'right', |
95 | 107 | 6: 'right', |
108 | 7: 'right', | |
96 | 109 | } |
97 | 110 | |
98 | 111 | for line in render_table(rows, alignments=alignments): |
696 | 696 | """ |
697 | 697 | return attributes |
698 | 698 | |
699 | def preview(self): | |
700 | """ | |
701 | Can return a preview of this item as a Unicode string. | |
702 | BundleWrap will NOT add a trailing newline. | |
703 | ||
704 | MAY be overridden by subclasses. | |
705 | """ | |
706 | raise NotImplementedError() | |
707 | ||
699 | 708 | def sdict(self): |
700 | 709 | """ |
701 | 710 | Return a statedict that describes the actual state of this item |
373 | 373 | attributes['group'] = 'wheel' |
374 | 374 | return attributes |
375 | 375 | |
376 | def preview(self): | |
377 | if ( | |
378 | self.attributes['content_type'] in ('any', 'base64', 'binary') or | |
379 | self.attributes['delete'] is True | |
380 | ): | |
381 | raise ValueError | |
382 | return self.content.decode(self.attributes['encoding']) | |
383 | ||
376 | 384 | def test(self): |
377 | 385 | if self.attributes['source'] and not exists(self.template): |
378 | 386 | raise BundleError(_( |
14 | 14 | from bundlewrap.utils.text import force_text, mark_for_translation as _ |
15 | 15 | from six import add_metaclass |
16 | 16 | import yaml |
17 | ||
18 | ||
19 | NAME_REGEX = r"[a-z0-9-]+/[a-z0-9-]{1,253}" | |
20 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
21 | 17 | |
22 | 18 | |
23 | 19 | def log_error(run_result): |
40 | 36 | 'context': None, |
41 | 37 | } |
42 | 38 | KIND = None |
43 | KUBECTL_RESOURCE_TYPE = None | |
44 | KUBERNETES_APIVERSION = "v1" | |
39 | KUBERNETES_APIVERSION = "v1" | |
40 | NAME_REGEX = r"^[a-z0-9-\.]{1,253}/[a-z0-9-\.]{1,253}$" | |
41 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
45 | 42 | |
46 | 43 | def __init__(self, *args, **kwargs): |
47 | 44 | super(KubernetesItem, self).__init__(*args, **kwargs) |
64 | 61 | |
65 | 62 | def fix(self, status): |
66 | 63 | if status.must_be_deleted: |
67 | result = run_local([ | |
68 | "kubectl", | |
69 | "--context={}".format(self.node.kubectl_context), | |
70 | "--namespace={}".format(self.namespace), | |
71 | "delete", | |
72 | self.KUBECTL_RESOURCE_TYPE, | |
73 | self.resource_name, | |
74 | ]) | |
64 | result = run_local(self._kubectl + ["delete", self.KIND, self.resource_name]) | |
75 | 65 | log_error(result) |
76 | 66 | else: |
77 | result = run_local([ | |
78 | "kubectl", | |
79 | "--context={}".format(self.node.kubectl_context), | |
80 | "--namespace={}".format(self.namespace), | |
81 | "apply", | |
82 | "-f", | |
83 | "-", | |
84 | ], data_stdin=self.manifest.encode('utf-8')) | |
67 | result = run_local( | |
68 | self._kubectl + ["apply", "-f", "-"], | |
69 | data_stdin=self.manifest.encode('utf-8'), | |
70 | ) | |
85 | 71 | log_error(result) |
86 | 72 | |
87 | 73 | def get_auto_deps(self, items, _secrets=True): |
110 | 96 | return deps |
111 | 97 | |
112 | 98 | @property |
113 | def manifest(self): | |
99 | def _kubectl(self): | |
100 | cmdline = [ | |
101 | "kubectl", | |
102 | "--context={}".format(self.node.kubectl_context), | |
103 | ] | |
104 | if self.namespace: | |
105 | cmdline.append("--namespace={}".format(self.namespace)) | |
106 | return cmdline | |
107 | ||
108 | @property | |
109 | def _manifest_dict(self): | |
114 | 110 | if self.attributes['manifest_processor'] == 'jinja2': |
115 | 111 | content_processor = content_processor_jinja2 |
116 | 112 | elif self.attributes['manifest_processor'] == 'mako': |
128 | 124 | elif self.attributes['manifest_file'].endswith(".json"): |
129 | 125 | user_manifest = json.loads(content_processor(self)) |
130 | 126 | |
131 | return json.dumps(merge_dict( | |
127 | merged_manifest = merge_dict( | |
132 | 128 | { |
133 | 129 | 'apiVersion': self.KUBERNETES_APIVERSION, |
134 | 130 | 'kind': self.KIND, |
137 | 133 | }, |
138 | 134 | }, |
139 | 135 | user_manifest, |
140 | ), indent=4, sort_keys=True) | |
136 | ) | |
137 | ||
138 | if merged_manifest['apiVersion'] is None: | |
139 | raise BundleError(_( | |
140 | "{item} from bundle '{bundle}' needs an apiVersion in its manifest" | |
141 | ).format(item=self.id, bundle=self.bundle.name)) | |
142 | ||
143 | return merged_manifest | |
144 | ||
145 | @property | |
146 | def manifest(self): | |
147 | return json.dumps(self._manifest_dict, indent=4, sort_keys=True) | |
141 | 148 | |
142 | 149 | @property |
143 | 150 | def namespace(self): |
144 | return self.name.split("/", 1)[0] | |
151 | return self.name.split("/", 1)[0] or None | |
145 | 152 | |
146 | 153 | def patch_attributes(self, attributes): |
147 | 154 | if 'context' not in attributes: |
148 | 155 | attributes['context'] = {} |
149 | 156 | return attributes |
150 | 157 | |
158 | def preview(self): | |
159 | if self.attributes['delete'] is True: | |
160 | raise ValueError | |
161 | return yaml.dump(self._manifest_dict, default_flow_style=False) | |
162 | ||
151 | 163 | @property |
152 | 164 | def resource_name(self): |
153 | return self.name.split("/", 1)[1] | |
165 | return self.name.split("/", 1)[-1] | |
154 | 166 | |
155 | 167 | def sdict(self): |
156 | result = run_local([ | |
157 | "kubectl", | |
158 | "--context={}".format(self.node.kubectl_context), | |
159 | "--namespace={}".format(self.namespace), | |
160 | "get", | |
161 | "-o", | |
162 | "json", | |
163 | self.KUBECTL_RESOURCE_TYPE, | |
164 | self.resource_name, | |
165 | ]) | |
168 | result = run_local(self._kubectl + ["get", "-o", "json", self.KIND, self.resource_name]) | |
166 | 169 | if result.return_code == 0: |
167 | 170 | full_json_response = json.loads(result.stdout) |
168 | 171 | if full_json_response.get("status", {}).get("phase") == "Terminating": |
200 | 203 | |
201 | 204 | @classmethod |
202 | 205 | def validate_name(cls, bundle, name): |
203 | if not NAME_REGEX_COMPILED.match(name): | |
206 | if not cls.NAME_REGEX_COMPILED.match(name): | |
204 | 207 | raise BundleError(_( |
205 | 208 | "name for {item_type}:{name} (bundle '{bundle}') " |
206 | 209 | "on {node} doesn't match {regex}" |
209 | 212 | name=name, |
210 | 213 | bundle=bundle.name, |
211 | 214 | node=bundle.node.name, |
212 | refex=NAME_REGEX, | |
215 | regex=cls.NAME_REGEX, | |
213 | 216 | )) |
217 | ||
218 | ||
219 | class KubernetesRawItem(KubernetesItem): | |
220 | BUNDLE_ATTRIBUTE_NAME = "k8s_raw" | |
221 | ITEM_TYPE_NAME = "k8s_raw" | |
222 | KUBERNETES_APIVERSION = None | |
223 | NAME_REGEX = r"^([a-z0-9-\.]{1,253}/)?[a-zA-Z0-9-\.]{1,253}/[a-z0-9-\.]{1,253}$" | |
224 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
225 | ||
226 | def get_auto_deps(self, items): | |
227 | deps = super(KubernetesRawItem, self).get_auto_deps(items) | |
228 | for item in items: | |
229 | if ( | |
230 | item.ITEM_TYPE_NAME == 'k8s_crd' and | |
231 | item._manifest_dict.get('spec', {}).get('names', {}).get('kind') == self.KIND | |
232 | ): | |
233 | deps.append(item.id) | |
234 | return deps | |
235 | ||
236 | @property | |
237 | def KIND(self): | |
238 | name = self.name.split("/", 2)[1] | |
239 | if name.lower() in ( | |
240 | "clusterrole", | |
241 | "clusterrolebinding", | |
242 | "configmap", | |
243 | "cronjob", | |
244 | "customresourcedefinition", | |
245 | "daemonset", | |
246 | "deployment", | |
247 | "ingress", | |
248 | "namespace", | |
249 | "persistentvolumeclaim", | |
250 | "service", | |
251 | "secret", | |
252 | "statefulset", | |
253 | ): | |
254 | raise BundleError(_( | |
255 | "Kind of {item_type}:{name} (bundle '{bundle}') " | |
256 | "on {node} clashes with builtin k8s_* item" | |
257 | ).format( | |
258 | item_type=self.ITEM_TYPE_NAME, | |
259 | name=self.name, | |
260 | bundle=self.bundle.name, | |
261 | node=self.bundle.node.name, | |
262 | regex=self.NAME_REGEX, | |
263 | )) | |
264 | else: | |
265 | return name | |
266 | ||
267 | @property | |
268 | def resource_name(self): | |
269 | return self.name.split("/", 2)[2] | |
270 | ||
271 | ||
272 | class KubernetesClusterRole(KubernetesItem): | |
273 | BUNDLE_ATTRIBUTE_NAME = "k8s_clusterroles" | |
274 | KIND = "ClusterRole" | |
275 | KUBERNETES_APIVERSION = "rbac.authorization.k8s.io/v1" | |
276 | ITEM_TYPE_NAME = "k8s_clusterrole" | |
277 | NAME_REGEX = r"^[a-z0-9-\.]{1,253}$" | |
278 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
279 | ||
280 | @property | |
281 | def namespace(self): | |
282 | return None | |
283 | ||
284 | ||
285 | class KubernetesClusterRoleBinding(KubernetesItem): | |
286 | BUNDLE_ATTRIBUTE_NAME = "k8s_clusterrolebindings" | |
287 | KIND = "ClusterRoleBinding" | |
288 | KUBERNETES_APIVERSION = "rbac.authorization.k8s.io/v1" | |
289 | ITEM_TYPE_NAME = "k8s_clusterrolebinding" | |
290 | NAME_REGEX = r"^[a-z0-9-\.]{1,253}$" | |
291 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
292 | ||
293 | def get_auto_deps(self, items): | |
294 | deps = super(KubernetesClusterRoleBinding, self).get_auto_deps(items) | |
295 | deps.append("k8s_clusterrole:") | |
296 | return deps | |
297 | ||
298 | @property | |
299 | def namespace(self): | |
300 | return None | |
214 | 301 | |
215 | 302 | |
216 | 303 | class KubernetesConfigMap(KubernetesItem): |
217 | 304 | BUNDLE_ATTRIBUTE_NAME = "k8s_configmaps" |
218 | 305 | KIND = "ConfigMap" |
219 | KUBECTL_RESOURCE_TYPE = "configmaps" | |
220 | 306 | KUBERNETES_APIVERSION = "v1" |
221 | 307 | ITEM_TYPE_NAME = "k8s_configmap" |
222 | 308 | |
224 | 310 | class KubernetesCronJob(KubernetesItem): |
225 | 311 | BUNDLE_ATTRIBUTE_NAME = "k8s_cronjobs" |
226 | 312 | KIND = "CronJob" |
227 | KUBECTL_RESOURCE_TYPE = "cronjobs" | |
228 | 313 | KUBERNETES_APIVERSION = "batch/v1beta1" |
229 | 314 | ITEM_TYPE_NAME = "k8s_cronjob" |
315 | ||
316 | ||
317 | class KubernetesCustomResourceDefinition(KubernetesItem): | |
318 | BUNDLE_ATTRIBUTE_NAME = "k8s_crd" | |
319 | KIND = "CustomResourceDefinition" | |
320 | KUBERNETES_APIVERSION = "apiextensions.k8s.io/v1beta1" | |
321 | ITEM_TYPE_NAME = "k8s_crd" | |
322 | NAME_REGEX = r"^[a-z0-9-\.]{1,253}$" | |
323 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
324 | ||
325 | def get_auto_deps(self, items): | |
326 | return [] | |
327 | ||
328 | @property | |
329 | def namespace(self): | |
330 | return None | |
230 | 331 | |
231 | 332 | |
232 | 333 | class KubernetesDaemonSet(KubernetesItem): |
233 | 334 | BUNDLE_ATTRIBUTE_NAME = "k8s_daemonsets" |
234 | 335 | KIND = "DaemonSet" |
235 | KUBECTL_RESOURCE_TYPE = "daemonsets" | |
236 | 336 | KUBERNETES_APIVERSION = "v1" |
237 | 337 | ITEM_TYPE_NAME = "k8s_daemonset" |
238 | 338 | |
250 | 350 | class KubernetesDeployment(KubernetesItem): |
251 | 351 | BUNDLE_ATTRIBUTE_NAME = "k8s_deployments" |
252 | 352 | KIND = "Deployment" |
253 | KUBECTL_RESOURCE_TYPE = "deployments" | |
254 | 353 | KUBERNETES_APIVERSION = "extensions/v1beta1" |
255 | 354 | ITEM_TYPE_NAME = "k8s_deployment" |
256 | 355 | |
268 | 367 | class KubernetesIngress(KubernetesItem): |
269 | 368 | BUNDLE_ATTRIBUTE_NAME = "k8s_ingresses" |
270 | 369 | KIND = "Ingress" |
271 | KUBECTL_RESOURCE_TYPE = "ingresses" | |
272 | 370 | KUBERNETES_APIVERSION = "extensions/v1beta1" |
273 | 371 | ITEM_TYPE_NAME = "k8s_ingress" |
274 | 372 | |
286 | 384 | class KubernetesNamespace(KubernetesItem): |
287 | 385 | BUNDLE_ATTRIBUTE_NAME = "k8s_namespaces" |
288 | 386 | KIND = "Namespace" |
289 | KUBECTL_RESOURCE_TYPE = "namespaces" | |
290 | 387 | KUBERNETES_APIVERSION = "v1" |
291 | 388 | ITEM_TYPE_NAME = "k8s_namespace" |
389 | NAME_REGEX = r"^[a-z0-9-\.]{1,253}$" | |
390 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
292 | 391 | |
293 | 392 | def get_auto_deps(self, items): |
294 | 393 | return [] |
295 | 394 | |
296 | @property | |
297 | def namespace(self): | |
298 | return self.name | |
299 | ||
300 | @property | |
301 | def resource_name(self): | |
302 | return self.name | |
303 | ||
304 | @classmethod | |
305 | def validate_name(cls, bundle, name): | |
306 | pass | |
395 | ||
396 | class KubernetesNetworkPolicy(KubernetesItem): | |
397 | BUNDLE_ATTRIBUTE_NAME = "k8s_networkpolicies" | |
398 | KIND = "NetworkPolicy" | |
399 | KUBERNETES_APIVERSION = "networking.k8s.io/v1" | |
400 | ITEM_TYPE_NAME = "k8s_networkpolicy" | |
401 | NAME_REGEX = r"^([a-z0-9-\.]{1,253}/)?[a-z0-9-\.]{1,253}$" | |
402 | NAME_REGEX_COMPILED = re.compile(NAME_REGEX) | |
307 | 403 | |
308 | 404 | |
309 | 405 | class KubernetesPersistentVolumeClain(KubernetesItem): |
310 | 406 | BUNDLE_ATTRIBUTE_NAME = "k8s_pvc" |
311 | 407 | KIND = "PersistentVolumeClaim" |
312 | KUBECTL_RESOURCE_TYPE = "persistentvolumeclaims" | |
313 | 408 | KUBERNETES_APIVERSION = "v1" |
314 | 409 | ITEM_TYPE_NAME = "k8s_pvc" |
410 | ||
411 | ||
412 | class KubernetesRole(KubernetesItem): | |
413 | BUNDLE_ATTRIBUTE_NAME = "k8s_roles" | |
414 | KIND = "Role" | |
415 | KUBERNETES_APIVERSION = "rbac.authorization.k8s.io/v1" | |
416 | ITEM_TYPE_NAME = "k8s_role" | |
417 | ||
418 | ||
419 | class KubernetesRoleBinding(KubernetesItem): | |
420 | BUNDLE_ATTRIBUTE_NAME = "k8s_rolebindings" | |
421 | KIND = "RoleBinding" | |
422 | KUBERNETES_APIVERSION = "rbac.authorization.k8s.io/v1" | |
423 | ITEM_TYPE_NAME = "k8s_rolebinding" | |
424 | ||
425 | def get_auto_deps(self, items): | |
426 | deps = super(KubernetesRoleBinding, self).get_auto_deps(items) | |
427 | deps.append("k8s_role:") | |
428 | return deps | |
315 | 429 | |
316 | 430 | |
317 | 431 | class KubernetesSecret(KubernetesItem): |
318 | 432 | BUNDLE_ATTRIBUTE_NAME = "k8s_secrets" |
319 | 433 | KIND = "Secret" |
320 | KUBECTL_RESOURCE_TYPE = "secrets" | |
321 | 434 | KUBERNETES_APIVERSION = "v1" |
322 | 435 | ITEM_TYPE_NAME = "k8s_secret" |
323 | 436 | |
328 | 441 | class KubernetesService(KubernetesItem): |
329 | 442 | BUNDLE_ATTRIBUTE_NAME = "k8s_services" |
330 | 443 | KIND = "Service" |
331 | KUBECTL_RESOURCE_TYPE = "services" | |
332 | 444 | KUBERNETES_APIVERSION = "v1" |
333 | 445 | ITEM_TYPE_NAME = "k8s_service" |
446 | ||
447 | ||
448 | class KubernetesServiceAccount(KubernetesItem): | |
449 | BUNDLE_ATTRIBUTE_NAME = "k8s_serviceaccounts" | |
450 | KIND = "ServiceAccount" | |
451 | KUBERNETES_APIVERSION = "v1" | |
452 | ITEM_TYPE_NAME = "k8s_serviceaccount" | |
334 | 453 | |
335 | 454 | |
336 | 455 | class KubernetesStatefulSet(KubernetesItem): |
337 | 456 | BUNDLE_ATTRIBUTE_NAME = "k8s_statefulsets" |
338 | 457 | KIND = "StatefulSet" |
339 | KUBECTL_RESOURCE_TYPE = "statefulsets" | |
340 | 458 | KUBERNETES_APIVERSION = "apps/v1" |
341 | 459 | ITEM_TYPE_NAME = "k8s_statefulset" |
342 | 460 |
707 | 707 | # multiplexed connection. |
708 | 708 | if self._ssh_first_conn_lock.acquire(False): |
709 | 709 | try: |
710 | operations.run(self.hostname, "true", add_host_keys=self._add_host_keys) | |
710 | with io.job(_("{} establishing connection...").format(bold(self.name))): | |
711 | operations.run(self.hostname, "true", add_host_keys=self._add_host_keys) | |
711 | 712 | self._ssh_conn_established = True |
712 | 713 | finally: |
713 | 714 | self._ssh_first_conn_lock.release() |
744 | 745 | ) |
745 | 746 | |
746 | 747 | def verify(self, show_all=False, workers=4): |
747 | bad = 0 | |
748 | good = 0 | |
748 | result = [] | |
749 | 749 | start = datetime.now() |
750 | 750 | |
751 | 751 | if not self.items: |
752 | 752 | io.stdout(_("{x} {node} has no items").format(node=bold(self.name), x=yellow("!"))) |
753 | 753 | else: |
754 | for item_status in verify_items( | |
755 | self, | |
756 | show_all=show_all, | |
757 | workers=workers, | |
758 | ): | |
759 | if item_status: | |
760 | good += 1 | |
761 | else: | |
762 | bad += 1 | |
763 | ||
764 | return {'good': good, 'bad': bad, 'duration': datetime.now() - start} | |
754 | result = verify_items(self, show_all=show_all, workers=workers) | |
755 | ||
756 | return { | |
757 | 'good': result.count(True), | |
758 | 'bad': result.count(False), | |
759 | 'unknown': result.count(None), | |
760 | 'duration': datetime.now() - start, | |
761 | } | |
765 | 762 | |
766 | 763 | |
767 | 764 | def build_attr_property(attr, default): |
840 | 837 | 'task_id': node.name + ":" + item.bundle.name + ":" + item.id, |
841 | 838 | 'target': item.verify, |
842 | 839 | } |
840 | ||
841 | def handle_exception(task_id, exception, traceback): | |
842 | # Unlike with `bw apply`, it is OK for `bw verify` to encounter | |
843 | # exceptions when getting an item's status. `bw verify` doesn't | |
844 | # care about dependencies and therefore cannot know that looking | |
845 | # up a database user requires the database to be installed in | |
846 | # the first place. | |
847 | io.progress_advance() | |
848 | io.debug("exception while verifying {}:".format(task_id)) | |
849 | io.debug(traceback) | |
850 | io.debug(repr(exception)) | |
851 | node_name, bundle_name, item_id = task_id.split(":", 2) | |
852 | io.stdout(_("{x} {node} {bundle} {item} (unable to get status, check --debug for details)").format( | |
853 | bundle=bold(bundle_name), | |
854 | item=item_id, | |
855 | node=bold(node_name), | |
856 | x=cyan("?"), | |
857 | )) | |
858 | return None # count this result as "unknown" | |
843 | 859 | |
844 | 860 | def handle_result(task_id, return_value, duration): |
845 | 861 | io.progress_advance() |
874 | 890 | tasks_available, |
875 | 891 | next_task, |
876 | 892 | handle_result, |
893 | handle_exception=handle_exception, | |
877 | 894 | pool_id="verify_{}".format(node.name), |
878 | 895 | workers=workers, |
879 | 896 | ) |
196 | 196 | add_host_keys=False, |
197 | 197 | data_stdin=None, |
198 | 198 | ignore_failure=False, |
199 | raise_for_return_codes=( | |
200 | 126, # command not executable | |
201 | 127, # command not found | |
202 | 255, # SSH error | |
203 | ), | |
199 | 204 | log_function=None, |
200 | 205 | wrapper_inner="{}", |
201 | 206 | wrapper_outer="{}", |
232 | 237 | result=force_text(result.stdout) + force_text(result.stderr), |
233 | 238 | ) |
234 | 239 | io.debug(error_msg) |
235 | if not ignore_failure or result.return_code == 255: | |
240 | if not ignore_failure or result.return_code in raise_for_return_codes: | |
236 | 241 | raise RemoteException(error_msg) |
237 | 242 | return result |
238 | 243 |
66 | 66 | @ansi_wrapper |
67 | 67 | def yellow(text): |
68 | 68 | return "\033[33m{}\033[0m".format(text) |
69 | ||
70 | ||
71 | def cyan_unless_zero(number): | |
72 | if number == 0: | |
73 | return "0" | |
74 | else: | |
75 | return cyan(str(number)) | |
69 | 76 | |
70 | 77 | |
71 | 78 | def green_unless_zero(number): |
0 | bundlewrap (3.4.0-1) unstable; urgency=medium | |
1 | ||
2 | * New upstream release | |
3 | * Update standards version to 4.1.4 | |
4 | ||
5 | -- Jonathan Carter <jcc@debian.org> Mon, 07 May 2018 15:15:23 +0200 | |
6 | ||
0 | 7 | bundlewrap (3.3.0-1) unstable; urgency=medium |
1 | 8 | |
2 | 9 | * New upstream release |
9 | 9 | python3-setuptools, |
10 | 10 | python3-requests, |
11 | 11 | python3-cryptography |
12 | Standards-Version: 4.1.3 | |
12 | Standards-Version: 4.1.4 | |
13 | 13 | X-Python3-Version: >= 3.4 |
14 | 14 | Homepage: http://bundlewrap.org/ |
15 | 15 | Vcs-Git: https://salsa.debian.org/python-team/applications/bundlewrap.git |
0 | bundlewrap_3.4.0-1_source.buildinfo python optional |
20 | 20 | }, |
21 | 21 | } |
22 | 22 | |
23 | Note that all item names (except namespaces themselves) must be prefixed with the name of a namespace and a forward slash `/`. Resource items will automatically depend on their namespace if you defined it. | |
23 | Note that the names of all items in a namespace must be prefixed with the name of their namespace and a forward slash `/`. Resource items will automatically depend on their namespace if you defined it. | |
24 | 24 | |
25 | 25 | <br> |
26 | 26 | |
28 | 28 | |
29 | 29 | <table> |
30 | 30 | <tr><th>Resource type</th><th>Bundle attribute</th><th>apiVersion</th></tr> |
31 | <tr><td>Cluster Role</td><td>k8s_clusterroles</td><td>rbac.authorization.k8s.io/v1</td></tr> | |
32 | <tr><td>Cluster Role Binding</td><td>k8s_clusterrolebindings</td><td>rbac.authorization.k8s.io/v1</td></tr> | |
31 | 33 | <tr><td>Config Map</td><td>k8s_configmaps</td><td>v1</td></tr> |
32 | 34 | <tr><td>Cron Job</td><td>k8s_cronjobs</td><td>batch/v1beta1</td></tr> |
35 | <tr><td>Custom Resource Definition</td><td>k8s_crd</td><td>apiextensions.k8s.io/v1beta1</td></tr> | |
33 | 36 | <tr><td>Daemon Set</td><td>k8s_daemonsets</td><td>v1</td></tr> |
34 | 37 | <tr><td>Deployment</td><td>k8s_deployments</td><td>extensions/v1beta1</td></tr> |
35 | 38 | <tr><td>Ingress</td><td>k8s_ingresses</td><td>extensions/v1beta1</td></tr> |
36 | 39 | <tr><td>Namespace</td><td>k8s_namespaces</td><td>v1</td></tr> |
40 | <tr><td>Network Policy</td><td>k8s_networkpolicies</td><td>networking.k8s.io/v1</td></tr> | |
37 | 41 | <tr><td>Persistent Volume Claim</td><td>k8s_pvc</td><td>v1</td></tr> |
42 | <tr><td>Role</td><td>k8s_roles</td><td>rbac.authorization.k8s.io/v1</td></tr> | |
43 | <tr><td>Role Binding</td><td>k8s_rolebindings</td><td>rbac.authorization.k8s.io/v1</td></tr> | |
38 | 44 | <tr><td>Service</td><td>k8s_services</td><td>v1</td></tr> |
45 | <tr><td>Service Account</td><td>k8s_serviceaccounts</td><td>v1</td></tr> | |
39 | 46 | <tr><td>Secret</td><td>k8s_secrets</td><td>v1</td></tr> |
40 | 47 | <tr><td>StatefulSet</td><td>k8s_statefulsets</td><td>apps/v1</td></tr> |
48 | <tr><td>(any)</td><td>k8s_raw</td><td>(any)</td></tr> | |
41 | 49 | </table> |
50 | ||
51 | You can define [Custom Resources](https://kubernetes.io/docs/concepts/api-extension/custom-resources/) like this: | |
52 | ||
53 | k8s_crd = { | |
54 | "custom-thing": { | |
55 | 'manifest': { | |
56 | 'spec': { | |
57 | 'names': { | |
58 | 'kind': "CustomThing", | |
59 | }, | |
60 | }, | |
61 | }, | |
62 | }, | |
63 | } | |
64 | ||
65 | k8s_raw = { | |
66 | "foo/CustomThing/baz": { | |
67 | 'manifest': { | |
68 | 'apiVersion': "example.com/v1", | |
69 | }, | |
70 | }, | |
71 | } | |
72 | ||
73 | The special `k8s_raw` items can also be used to create resources that BundleWrap does not support natively: | |
74 | ||
75 | k8s_raw = { | |
76 | "foo/HorizontalPodAutoscaler/baz": { | |
77 | 'manifest': { | |
78 | 'apiVersion': "autoscaling/v2beta1", | |
79 | }, | |
80 | }, | |
81 | } | |
82 | ||
83 | Resources outside any namespace can be created with `k8s_raw` by omitting the namespace in the item name (so that the name starts with `/`). | |
42 | 84 | |
43 | 85 | <br> |
44 | 86 |
16 | 16 | |
17 | 17 | setup( |
18 | 18 | name="bundlewrap", |
19 | version="3.3.0", | |
19 | version="3.4.0", | |
20 | 20 | description="Config management with Python", |
21 | 21 | long_description=( |
22 | 22 | "By allowing for easy and low-overhead config management, BundleWrap fills the gap between complex deployments using Chef or Puppet and old school system administration over SSH.\n" |