Codebase list bundlewrap / d467aea
new upstream release Jonathan Carter 5 years ago
17 changed file(s) with 353 addition(s) and 112 deletion(s). Raw diff Collapse all Expand all
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
016 # 3.3.0
117
218 2018-03-09
00 # -*- coding: utf-8 -*-
11 from __future__ import unicode_literals
22
3 VERSION = (3, 3, 0)
3 VERSION = (3, 4, 0)
44 VERSION_STRING = ".".join([str(v) for v in VERSION])
2727
2828 def bw_items(repo, args):
2929 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("!!!")))
3232 exit(1)
3333 elif args['file_preview_path']:
3434 if args['item']:
7575 ))
7676 elif args['item']:
7777 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:
8085 io.stderr(_(
81 "{x} cannot preview {item} on {node} (not a file)"
86 "{x} cannot preview {item} on {node} (doesn't support previews)"
8287 ).format(x=red("!!!"), item=item.id, node=node.name))
8388 exit(1)
84 if (
85 item.attributes['content_type'] in ('any', 'base64', 'binary') or
86 item.attributes['delete'] is True
87 ):
89 except ValueError:
8890 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))
9193 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)
10399 else:
104100 if args['show_sdict']:
105101 statedict = item.sdict()
289289 )
290290 parser_items.add_argument(
291291 "-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"),
296297 )
297298 parser_items.add_argument(
298299 "-w",
4848 ))
4949 return None
5050
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 )
5657
5758 node.repo.hooks.node_run_end(
5859 node.repo,
99 from ..utils.text import (
1010 blue,
1111 bold,
12 cyan,
13 cyan_unless_zero,
1214 error_summary,
1315 format_duration,
1416 green,
2224
2325 def stats_summary(node_stats, total_duration):
2426 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 ])
2632 try:
2733 node_stats[node]['health'] = \
2834 (node_stats[node]['good'] / float(node_stats[node]['total'])) * 100.0
3339 'items': 0,
3440 'good': 0,
3541 'bad': 0,
42 'unknown': 0,
3643 }
3744 node_ranking = []
3845
4047 totals['items'] += stats['total']
4148 totals['good'] += stats['good']
4249 totals['bad'] += stats['bad']
50 totals['unknown'] += stats['unknown']
4351 node_ranking.append((
4452 stats['health'],
4553 node_name,
4654 stats['total'],
4755 stats['good'],
4856 stats['bad'],
57 stats['unknown'],
4958 stats['duration'],
5059 ))
5160
6170 _("items"),
6271 green(_("good")),
6372 red(_("bad")),
73 cyan(_("unknown")),
6474 _("health"),
6575 _("duration"),
6676 ], ROW_SEPARATOR]
6777
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:
6979 rows.append([
7080 node_name,
7181 str(items),
7282 green_unless_zero(good),
7383 red_unless_zero(bad),
84 cyan_unless_zero(unknown),
7485 "{0:.1f}%".format(health),
7586 format_duration(duration),
7687 ])
8293 str(totals['items']),
8394 green_unless_zero(totals['good']),
8495 red_unless_zero(totals['bad']),
96 cyan_unless_zero(totals['unknown']),
8597 "{0:.1f}%".format(totals['health']),
8698 format_duration(total_duration),
8799 ])
93105 4: 'right',
94106 5: 'right',
95107 6: 'right',
108 7: 'right',
96109 }
97110
98111 for line in render_table(rows, alignments=alignments):
696696 """
697697 return attributes
698698
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
699708 def sdict(self):
700709 """
701710 Return a statedict that describes the actual state of this item
373373 attributes['group'] = 'wheel'
374374 return attributes
375375
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
376384 def test(self):
377385 if self.attributes['source'] and not exists(self.template):
378386 raise BundleError(_(
1414 from bundlewrap.utils.text import force_text, mark_for_translation as _
1515 from six import add_metaclass
1616 import yaml
17
18
19 NAME_REGEX = r"[a-z0-9-]+/[a-z0-9-]{1,253}"
20 NAME_REGEX_COMPILED = re.compile(NAME_REGEX)
2117
2218
2319 def log_error(run_result):
4036 'context': None,
4137 }
4238 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)
4542
4643 def __init__(self, *args, **kwargs):
4744 super(KubernetesItem, self).__init__(*args, **kwargs)
6461
6562 def fix(self, status):
6663 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])
7565 log_error(result)
7666 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 )
8571 log_error(result)
8672
8773 def get_auto_deps(self, items, _secrets=True):
11096 return deps
11197
11298 @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):
114110 if self.attributes['manifest_processor'] == 'jinja2':
115111 content_processor = content_processor_jinja2
116112 elif self.attributes['manifest_processor'] == 'mako':
128124 elif self.attributes['manifest_file'].endswith(".json"):
129125 user_manifest = json.loads(content_processor(self))
130126
131 return json.dumps(merge_dict(
127 merged_manifest = merge_dict(
132128 {
133129 'apiVersion': self.KUBERNETES_APIVERSION,
134130 'kind': self.KIND,
137133 },
138134 },
139135 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)
141148
142149 @property
143150 def namespace(self):
144 return self.name.split("/", 1)[0]
151 return self.name.split("/", 1)[0] or None
145152
146153 def patch_attributes(self, attributes):
147154 if 'context' not in attributes:
148155 attributes['context'] = {}
149156 return attributes
150157
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
151163 @property
152164 def resource_name(self):
153 return self.name.split("/", 1)[1]
165 return self.name.split("/", 1)[-1]
154166
155167 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])
166169 if result.return_code == 0:
167170 full_json_response = json.loads(result.stdout)
168171 if full_json_response.get("status", {}).get("phase") == "Terminating":
200203
201204 @classmethod
202205 def validate_name(cls, bundle, name):
203 if not NAME_REGEX_COMPILED.match(name):
206 if not cls.NAME_REGEX_COMPILED.match(name):
204207 raise BundleError(_(
205208 "name for {item_type}:{name} (bundle '{bundle}') "
206209 "on {node} doesn't match {regex}"
209212 name=name,
210213 bundle=bundle.name,
211214 node=bundle.node.name,
212 refex=NAME_REGEX,
215 regex=cls.NAME_REGEX,
213216 ))
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
214301
215302
216303 class KubernetesConfigMap(KubernetesItem):
217304 BUNDLE_ATTRIBUTE_NAME = "k8s_configmaps"
218305 KIND = "ConfigMap"
219 KUBECTL_RESOURCE_TYPE = "configmaps"
220306 KUBERNETES_APIVERSION = "v1"
221307 ITEM_TYPE_NAME = "k8s_configmap"
222308
224310 class KubernetesCronJob(KubernetesItem):
225311 BUNDLE_ATTRIBUTE_NAME = "k8s_cronjobs"
226312 KIND = "CronJob"
227 KUBECTL_RESOURCE_TYPE = "cronjobs"
228313 KUBERNETES_APIVERSION = "batch/v1beta1"
229314 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
230331
231332
232333 class KubernetesDaemonSet(KubernetesItem):
233334 BUNDLE_ATTRIBUTE_NAME = "k8s_daemonsets"
234335 KIND = "DaemonSet"
235 KUBECTL_RESOURCE_TYPE = "daemonsets"
236336 KUBERNETES_APIVERSION = "v1"
237337 ITEM_TYPE_NAME = "k8s_daemonset"
238338
250350 class KubernetesDeployment(KubernetesItem):
251351 BUNDLE_ATTRIBUTE_NAME = "k8s_deployments"
252352 KIND = "Deployment"
253 KUBECTL_RESOURCE_TYPE = "deployments"
254353 KUBERNETES_APIVERSION = "extensions/v1beta1"
255354 ITEM_TYPE_NAME = "k8s_deployment"
256355
268367 class KubernetesIngress(KubernetesItem):
269368 BUNDLE_ATTRIBUTE_NAME = "k8s_ingresses"
270369 KIND = "Ingress"
271 KUBECTL_RESOURCE_TYPE = "ingresses"
272370 KUBERNETES_APIVERSION = "extensions/v1beta1"
273371 ITEM_TYPE_NAME = "k8s_ingress"
274372
286384 class KubernetesNamespace(KubernetesItem):
287385 BUNDLE_ATTRIBUTE_NAME = "k8s_namespaces"
288386 KIND = "Namespace"
289 KUBECTL_RESOURCE_TYPE = "namespaces"
290387 KUBERNETES_APIVERSION = "v1"
291388 ITEM_TYPE_NAME = "k8s_namespace"
389 NAME_REGEX = r"^[a-z0-9-\.]{1,253}$"
390 NAME_REGEX_COMPILED = re.compile(NAME_REGEX)
292391
293392 def get_auto_deps(self, items):
294393 return []
295394
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)
307403
308404
309405 class KubernetesPersistentVolumeClain(KubernetesItem):
310406 BUNDLE_ATTRIBUTE_NAME = "k8s_pvc"
311407 KIND = "PersistentVolumeClaim"
312 KUBECTL_RESOURCE_TYPE = "persistentvolumeclaims"
313408 KUBERNETES_APIVERSION = "v1"
314409 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
315429
316430
317431 class KubernetesSecret(KubernetesItem):
318432 BUNDLE_ATTRIBUTE_NAME = "k8s_secrets"
319433 KIND = "Secret"
320 KUBECTL_RESOURCE_TYPE = "secrets"
321434 KUBERNETES_APIVERSION = "v1"
322435 ITEM_TYPE_NAME = "k8s_secret"
323436
328441 class KubernetesService(KubernetesItem):
329442 BUNDLE_ATTRIBUTE_NAME = "k8s_services"
330443 KIND = "Service"
331 KUBECTL_RESOURCE_TYPE = "services"
332444 KUBERNETES_APIVERSION = "v1"
333445 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"
334453
335454
336455 class KubernetesStatefulSet(KubernetesItem):
337456 BUNDLE_ATTRIBUTE_NAME = "k8s_statefulsets"
338457 KIND = "StatefulSet"
339 KUBECTL_RESOURCE_TYPE = "statefulsets"
340458 KUBERNETES_APIVERSION = "apps/v1"
341459 ITEM_TYPE_NAME = "k8s_statefulset"
342460
707707 # multiplexed connection.
708708 if self._ssh_first_conn_lock.acquire(False):
709709 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)
711712 self._ssh_conn_established = True
712713 finally:
713714 self._ssh_first_conn_lock.release()
744745 )
745746
746747 def verify(self, show_all=False, workers=4):
747 bad = 0
748 good = 0
748 result = []
749749 start = datetime.now()
750750
751751 if not self.items:
752752 io.stdout(_("{x} {node} has no items").format(node=bold(self.name), x=yellow("!")))
753753 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 }
765762
766763
767764 def build_attr_property(attr, default):
840837 'task_id': node.name + ":" + item.bundle.name + ":" + item.id,
841838 'target': item.verify,
842839 }
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"
843859
844860 def handle_result(task_id, return_value, duration):
845861 io.progress_advance()
874890 tasks_available,
875891 next_task,
876892 handle_result,
893 handle_exception=handle_exception,
877894 pool_id="verify_{}".format(node.name),
878895 workers=workers,
879896 )
196196 add_host_keys=False,
197197 data_stdin=None,
198198 ignore_failure=False,
199 raise_for_return_codes=(
200 126, # command not executable
201 127, # command not found
202 255, # SSH error
203 ),
199204 log_function=None,
200205 wrapper_inner="{}",
201206 wrapper_outer="{}",
232237 result=force_text(result.stdout) + force_text(result.stderr),
233238 )
234239 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:
236241 raise RemoteException(error_msg)
237242 return result
238243
6666 @ansi_wrapper
6767 def yellow(text):
6868 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))
6976
7077
7178 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
07 bundlewrap (3.3.0-1) unstable; urgency=medium
18
29 * New upstream release
99 python3-setuptools,
1010 python3-requests,
1111 python3-cryptography
12 Standards-Version: 4.1.3
12 Standards-Version: 4.1.4
1313 X-Python3-Version: >= 3.4
1414 Homepage: http://bundlewrap.org/
1515 Vcs-Git: https://salsa.debian.org/python-team/applications/bundlewrap.git
0 bundlewrap_3.4.0-1_source.buildinfo python optional
2020 },
2121 }
2222
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.
2424
2525 <br>
2626
2828
2929 <table>
3030 <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>
3133 <tr><td>Config Map</td><td>k8s_configmaps</td><td>v1</td></tr>
3234 <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>
3336 <tr><td>Daemon Set</td><td>k8s_daemonsets</td><td>v1</td></tr>
3437 <tr><td>Deployment</td><td>k8s_deployments</td><td>extensions/v1beta1</td></tr>
3538 <tr><td>Ingress</td><td>k8s_ingresses</td><td>extensions/v1beta1</td></tr>
3639 <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>
3741 <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>
3844 <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>
3946 <tr><td>Secret</td><td>k8s_secrets</td><td>v1</td></tr>
4047 <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>
4149 </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 `/`).
4284
4385 <br>
4486
1616
1717 setup(
1818 name="bundlewrap",
19 version="3.3.0",
19 version="3.4.0",
2020 description="Config management with Python",
2121 long_description=(
2222 "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"