From 091b265ff6ef87110299ba713193095fe827fce6 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 2 Feb 2023 17:31:35 +0800 Subject: [PATCH] Add new resource filters can separate cluster and namespace scope resources. Signed-off-by: Xun Jiang --- changelogs/unreleased/5838-blackpiglet | 1 + config/crd/v1/bases/velero.io_backups.yaml | 33 ++ config/crd/v1/bases/velero.io_schedules.yaml | 34 ++ config/crd/v1/crds/crds.go | 4 +- pkg/apis/velero/v1/backup.go | 30 ++ pkg/apis/velero/v1/zz_generated.deepcopy.go | 20 ++ pkg/backup/backup.go | 17 +- pkg/backup/backup_test.go | 297 +++++++++++++++++- pkg/backup/item_backupper.go | 57 +++- pkg/backup/item_backupper_test.go | 39 +++ pkg/backup/item_collector.go | 61 +++- pkg/backup/request.go | 24 +- pkg/builder/backup_builder.go | 24 ++ pkg/cmd/cli/backup/create.go | 82 +++-- pkg/cmd/cli/schedule/create.go | 30 +- pkg/cmd/util/output/backup_describer.go | 26 ++ pkg/controller/backup_controller.go | 38 +++ pkg/controller/backup_controller_test.go | 6 + pkg/util/collections/includes_excludes.go | 111 +++++++ .../collections/includes_excludes_test.go | 258 +++++++++++++++ 20 files changed, 1113 insertions(+), 79 deletions(-) create mode 100644 changelogs/unreleased/5838-blackpiglet diff --git a/changelogs/unreleased/5838-blackpiglet b/changelogs/unreleased/5838-blackpiglet new file mode 100644 index 0000000000..6156f606b0 --- /dev/null +++ b/changelogs/unreleased/5838-blackpiglet @@ -0,0 +1 @@ +Add new resource filters can separate cluster and namespace scope resources. \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 2fb76533a1..51f1c1e7ca 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -54,6 +54,22 @@ spec: Use DefaultVolumesToFsBackup instead." nullable: true type: boolean + excludedClusterScopeResources: + description: ExcludedClusterScopeResources is a slice of cluster scope + resource type names to exclude from the backup. If set to "*", all + cluster scope resource types are excluded. + items: + type: string + nullable: true + type: array + excludedNamespacedResources: + description: ExcludedNamespacedResources is a slice of namespace scope + resource type names to exclude from the backup. If set to "*", all + namespace scope resource types are excluded. + items: + type: string + nullable: true + type: array excludedNamespaces: description: ExcludedNamespaces contains a list of namespaces that are not included in the backup. @@ -259,6 +275,23 @@ spec: resources should be included for consideration in the backup. nullable: true type: boolean + includedClusterScopeResources: + description: IncludedClusterScopeResources is a slice of cluster scope + resource type names to include in the backup. If set to "*", all + cluster scope resource types are included. The default value is + empty, which means only related cluster scope resources are included. + items: + type: string + nullable: true + type: array + includedNamespacedResources: + description: IncludedNamespacedResources is a slice of namespace scope + resource type names to include in the backup. The default value + is "*". + items: + type: string + nullable: true + type: array includedNamespaces: description: IncludedNamespaces is a slice of namespace names to include objects from. If empty, all namespaces are included. diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 9e7454d517..34a0996c98 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -84,6 +84,22 @@ spec: entirely in future. Use DefaultVolumesToFsBackup instead." nullable: true type: boolean + excludedClusterScopeResources: + description: ExcludedClusterScopeResources is a slice of cluster + scope resource type names to exclude from the backup. If set + to "*", all cluster scope resource types are excluded. + items: + type: string + nullable: true + type: array + excludedNamespacedResources: + description: ExcludedNamespacedResources is a slice of namespace + scope resource type names to exclude from the backup. If set + to "*", all namespace scope resource types are excluded. + items: + type: string + nullable: true + type: array excludedNamespaces: description: ExcludedNamespaces contains a list of namespaces that are not included in the backup. @@ -294,6 +310,24 @@ spec: resources should be included for consideration in the backup. nullable: true type: boolean + includedClusterScopeResources: + description: IncludedClusterScopeResources is a slice of cluster + scope resource type names to include in the backup. If set to + "*", all cluster scope resource types are included. The default + value is empty, which means only related cluster scope resources + are included. + items: + type: string + nullable: true + type: array + includedNamespacedResources: + description: IncludedNamespacedResources is a slice of namespace + scope resource type names to include in the backup. The default + value is "*". + items: + type: string + nullable: true + type: array includedNamespaces: description: IncludedNamespaces is a slice of namespace names to include objects from. If empty, all namespaces are included. diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 0119746624..c672af671e 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WAo\xdc6\x13\xbd\xebW\f\xf2\x1dr\xf9\xa4M\xd0C\v\xddR\xb7\x05\x82&\x86a\a\xbe\x14=P\xe4\xec.c\x8ad\xc9\xe1\xa6ۢ\xff\xbd\x18R\xf2j%\xd9\x1b\a\xa8n\"\x87of\xde\xcc\x1bQU]ו\xf0\xfa\x1eC\xd4ζ \xbc\xc6?\t-\xbf\xc5\xe6\xe1\x87\xd8h\xb79\xbc\xad\x1e\xb4U-\\\xa5H\xae\xbf\xc5\xe8R\x90\xf8\x13n\xb5դ\x9d\xadz$\xa1\x04\x89\xb6\x02\x10\xd6:\x12\xbc\x1c\xf9\x15@:K\xc1\x19\x83\xa1ޡm\x1eR\x87]\xd2Fa\xc8\xe0\xa3\xebÛ\xe6\xfb\xe6M\x05 \x03\xe6\xe3\x9ft\x8f\x91D\xef[\xb0ɘ\n\xc0\x8a\x1e[\xe8\x84|H>\xa0wQ\x93\v\x1acs@\x83\xc15\xdaUѣd\xb7\xbb\xe0\x92o\xe1\xb4QN\x0f!\x95t~\xcc@\xb7#\xd01o\x19\x1d\xe9\xd7\xd5\xed\x0f:R6\xf1&\x05a\xd6\x02\xc9\xdbQ\xdb]2\",\f\xd8A\x94\xcec\v\xd7\x1c\x8b\x17\x12U\x050P\x90c\xabA(\x95I\x15\xe6&hK\x18\xae\x9cI\xfdHf\r\x9f\xa3\xb37\x82\xf6-4#\xed͂\xb2l;\x12\xf6n\x87\xc3;\x1dٹ\x12\x84K0f\xae9\xc5\xfa\xe9\xe8\xf1\f\xe5D\x04L\xf6\nb\xa4\xa0\xed\xae:\x19\x1f\xde\x16*\xe4\x1e{\xd1\x0e\xb6Σ}w\xf3\xfe\xfe\xbb\xbb\xb3e\x00\x1f\x9c\xc7@z,Oy&}9Y\x05P\x18eОr\u05fcf\xc0b\x05\x8a\x1b\x12#\xd0\x1eGNQ\r1\x80\xdb\x02\xedu\x84\x80>`D[Z\xf4\f\x18\xd8HXp\xddg\x94\xd4\xc0\x1d\x06\x86\x81\xb8w\xc9(\xee\xe3\x03\x06\x82\x80\xd2\xed\xac\xfe\xeb\x11;\x02\xb9\xec\xd4\b¡GNO\xae\xa1\x15\x06\x0e\xc2$\xfc?\b\xab\xa0\x17G\b\xc8^ \xd9\t^6\x89\r|t\x01AۭkaO\xe4c\xbb\xd9\xec4\x8dz\x94\xae\xef\x93\xd5t\xdcdi\xe9.\x91\vq\xa3\xf0\x80f\x13\xf5\xae\x16A\xee5\xa1\xa4\x14p#\xbc\xaes\xe86k\xb2\xe9\xd5\xff\u00a0\xe0\xf8\xfa,\xd6E-˓\xc5\xf2L\x05X-\xa0#\x88\xe1h\xc9\xe2D4/1;\xb7?\xdf}\x82\xd1u.Ɯ\xfd\xcc\xfb\xe9`<\x95\x80\t\xd3v\x8b\xa1\x14q\x1b\\\x9f1\xd1*ﴥ\xfc\"\x8dF;\xa7?\xa6\xae\xd7\xc4u\xff#a$\xaeU\x03WyHA\x87\x90<\xabA5\xf0\xde\u0095\xe8\xd1\\\x89\x88\xffy\x01\x98\xe9X3\xb1_W\x82\xe9|\x9d\x1b\x17\xd6&\x1b\xe3\b|\xa2^\xf3\xb1v\xe7Qr\xf9\x98A>\xaa\xb7Zfm\xc0\xd6\x05\x10\v\xfb\xe6\fz]\xba\xfc\x94\xe1wG.\x88\x1d~p\x05sn\xb4\x1a\xdb\xec\xcc\x18\x1cO\x96\"c\\7\\`\x03\xd0^\xd0D\xbf$\xb4}\x1c\x03\xab\xf90\x92-S\bhi\x80\xc97\x88o\x1e\x99FD\x9a\x8c\v\xbe\xcd]\xe8\x80\x0f\xcb\x13c`\f\x06\xc4\v\xd3\xf9\xf2E̿\xba\xb9hk\x93e\xebB/\xa8\\\x17k\x06ZX\xf0\xb5\\t\x06[\xa0\x90\x96\xdb\xcf\xcdQ\x8cQ\xec.e\xf7\xb1X\x95\xcb\xc5p\x04D\xe7\x12=A=\xed\x97Q\xc0\x85r\\\x88\xd4\xefE\xbc\x14\xe7\r۬5\xc4\xec{\xf5\\\bO\xcd\xcck\xfc\xb2\xb2z\x8bB-u\\õ\xa3\xf5\xad'3\\U\xc5b1\xf2=LM\xea\x1c\x8b\x90\xa7+\xa9{\xbcW\xb6\xf0\xf7?\xd5IXBJ\xf4\x84\xeaz\xfe\a6\xcc\xf7\xf1\x87*\xbfJg\xcb\x0fPl\xe1\xb7߫\xe2\n\xd5\xfd\xf8\x93ċ\xff\x06\x00\x00\xff\xff\xc8p\x98۸\x0e\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms\xdc:rw\xfd\x8a.\xe5\xe0\xdd*\xcd\xe8\xb9rHJ7?Y\xae\xa8ދ\xad\xb2\xb4\xdaC\x92\x03\x86\xec\x99\xc1\x13\bp\x01p\xe4I*\xff=\xd5\x00\xf8\r\x92\x18Y\xda}/e\\l\x91@\x03\xe8n\xf4\x17\x9a=g\xab\xd5ꌕ\xfc\x11\xb5\xe1J^\x01+9~\xb3(\xe9/\xb3~\xfaW\xb3\xe6\xea\xf2\xf0\xfe\xec\x89\xcb\xfc\n\xae+cU\xf1\x15\x8d\xaat\x86\x1fq\xcb%\xb7\\ɳ\x02-˙eWg\x00LJe\x19=6\xf4'@\xa6\xa4\xd5J\bԫ\x1d\xca\xf5S\xb5\xc1M\xc5E\x8e\xda\x01\xaf\xa7>\xfc\xb4\xfe\x97\xf5Og\x00\x99F7\xfc\x81\x17h,+\xca+\x90\x95\x10g\x00\x92\x15x\x05\x1b\x96=U\xa5Y\x1fP\xa0Vk\xae\xceL\x89\x19͵Ӫ*\xaf\xa0}ᇄu\xf8=\xfc\xecF\xbb\a\x82\x1b\xfbK\xe7\xe1\xaf\xdcX\xf7\xa2\x14\x95f\xa2\x99\xc9=3\\\xee*\xc1t\xfd\xf4\f\xc0d\xaa\xc4+\xf8LS\x94,\xc3\xfc\f l\xc7M\xb9\n\v>\xbc\xf7\x10\xb2=\x16̯\x05@\x95(?\xdc\xdd>\xfe\xf3}\xef1@\x8e&Ӽ\xb4\x0e)~a\xc0\r0xt\xdb\x02\x1d\xd0\x0fv\xcf,h,5\x1a\x94ր\xdd#d\xac\xb4\x95FP[\xf8\xa5ڠ\x96h\xd14\xa0\x012Q\x19\x8b\x1a\x8ce\x16\x81Y`P*.-p\t\x96\x17\b\u007f\xfapw\vj\xf3\x1bf\xd6\x00\x9390cTƙ\xc5\x1c\x0eJT\x05\xfa\xb1\u007f^7PK\xadJԖ\xd7x\xf6\xad\xc3U\x9d\xa7\x83\xed\xbd#\f\xf8^\x90\x13;\xa1\xdfF\xc0\"\xe6\x01i\xb4\x1f\xbb\xe7\xa6ݮ\xe3\x90\x1e`\xa0NL\x86ů\xe1\x1e5\x81\x01\xb3W\x95ȉ\v\x0f\xa8\ta\x99\xdaI\xfe\xdf\rl\x03V\xb9I\x05\xb3\x18\x18\xa0m\\ZԒ\t80Q\xe1\x85CI\xc1\x8e\xa0\x91f\x81Jv\xe0\xb9.f\r\xff\xae4\x02\x97[u\x05{kKsuy\xb9\xe3\xb6>M\x99*\x8aJr{\xbct\a\x83o*\xab\xb4\xb9\xcc\xf1\x80\xe2\xd2\xf0݊\xe9l\xcf-fD\xc8KV\xf2\x95[\xbat'j]\xe4\xffT3\x80y\xd7[\xab=\x123\x1a\xab\xb9\xdcu^8\xae\x9f\xa1\x00\x1d\x00\xcf_~\xa8\xdfE\x8bhzD\xd8\xf9zs\xff\xd0\xe5=n\x86\xd8wx\xef0dK\x02B\x18\x97[Ԟ\x88[\xad\n\a\x13e\xee\xb9ϱ\xae\xe0(\x87\xe87զ\xe0\x96\xe8\xfe\xb7\n\r1\xb9Zõ\x131\xb0A\xa8ʜ8s\r\xb7\x12\xaeY\x81\xe2\x9a\x19|s\x02\x10\xa6͊\x10\x9bF\x82\xaet\x1cv\xf6X뼨e\xd9\x04\xbd\xbc@\xb8/1\xeb\x1d\x18\x1aŷ\xd8V$K\xc6x\x06 ^\x9e\xe4\x01.\x8dE\x96\xaf\xcf_\x93@\xf8-\x13U\x8eyc\xb6\x8cd\xc0\x8087\xa3\x01Τc\\\x92\xd6 #\x8a\x90+۷d\x98D\xb6\xca4\x02\xc9m.=\x97\xf3\xeeGZ.͋3h\xfa\x192\x13\"\xfa\xd4+\xa3\xf4D\xe1\xf4\x1c\x99\xf9\xa4\x96S2c\x86y/\x93@\x97\xf3aR<Džܗ\x17d\xbc$f;~\xf7\xc5XJNˋ2Y\x16\x13\x02\x13\xf3W\xfa\x99)\xf3 O\xc8ZIB\xcer\x86\xca\xc9y)!\x0fdv\x1f\xc9\xd9(\x91<\x93Y\xc0\x939(s\xd9%\vQ\xa9q\xe6IzN\xc9,h\x97o\xb2\x9cI\xf2z\xf9\xa2\xafa\x03O\x8b\x9a\xc5l\x90E\x1by~}\x8b\xf9\x1e\xa7dy,b\xec\x85\x19\x1dM\xc6\xc6ļ\xa7\xe6q\xf4\xf34&\x80\xa6doLdgL@\x9c\xcd\xd9H\xcdɘ\x80\xbd\xa0vg\xb9d\xe6e\xfcC`X\xd4o\xe2\xef\xc5Q/ݘ\xd2=sq\xc9B\xff2\xe8N\xb4\xac\xad\xa6y\xf33fyr\xbb?\xdd\xfc,*ay)\\8\xff\xc0\xf3\xa8\xd3h\xf7xl>\xeb\xfcM\xb9Ϝ6G\a\xe9\xcb׆=\xd7\x03#\x9a\x19xF!\x80Řk\xb4\xf3\xcc\u007f˞\xa9\x15\x92̧\x03\x17>X\r\x9f\xbc_x\x0ev_r\xc5\"\x9ev\x8f\x05A\xa9\xbf|=\xc1\xfd\x987\x10\xbd-\xeb\x9e\xfd\xadB}\x04u@\xddZ\f\v\xdf\x11\xf8\x83f*\xd1&n\x05\xf9\xe1+(\f\f\xe7\xf6\xc0\xc1\a\xe9UX\x14\xec`\x8d\x0e\x0e\x9dy\xd1К\xc4\x1b\xf9\x01\x13]\xe3\x81\x0fՌ\x8e\xbc_\xb2=S\x93\xf0\xdf\xd6u8\xddyXT\xdbo\xe2@\xbc܅\x98\x01\x99\x9aT\x9fv\x01\xb5\x98D\xffV\xaeĒ3\x91lE\xa5%ɿEr\xfc\tI\xf1'8\x15\xa7\xb9\x15\xc9hJI~\u007f\x13\xe7\xe2\r\u074b\xb7p0^\xe6b,\x80\x1c$\xb5\xa7\xa4\xab']\xae&\xdf/\xa4\\\x8e._\x01̧\xa1'\xa4\x9f'\\\x0e,\xad4!\xcd\xfc\xb4\xf4\xf2\x04\x1c\xbe\x91\xf3\xf1F\xee\xc7[8 o\xeb\x82,:!\x8b\x9c3\xfb\xfa\xc5\xd1e\xa5sԳ\xc1\xf8TV\x9be\xb2\x81\xbfПs\xf0Em]\xe1\x85z\xf5L\xd3XH\xb9\xf9\xfa3\x83_\xb8\xcc==\x88\xa9:z\xbcwC\xd0\x1a\x16\xf1\x04\x81\xd6j\v\x15\xb0\xfc\xb5\x82\xc1\x92iW6ms\xf4W\x93f\r7,\xdb\x0f\xa0\xef\xa3~\xc2V\xe9\x82Y8o\xeed.=p\xfa\xfb|\r\xf0I5\x97^݊\n\x86\x17\xa58\x92\x1f\x10\x81y\xde\x05\xf12\x86\x882\x93\t\xe5\x9aB\xfd\x9a\x05\xdf\xef\xbe\xdf;r\x99W\x97\xee\xa9ᚸ\xe3\xc3\xe4\x11\xee\x1e\x9du\xe2\n\x86dm\xf1\x94`\u007f\xd4\xde߰\xb6\xcaϯ\u007f\xadg\xac\xd2l\x87\xbf*_\x81k\t\a\xfd\u07bd\xf2kAj\xd4\xd7\xec\xf5W\x181m\x1aj\x81\r\x80\xb5\xd93\xa3:P\xb4ʘ8\x999\x89֊\x85\xcd<<\xfc\xea7`y\x81돕\xbfB]\x95L\x1b$l\xd6\x1b\xf3\x836\xf4߽z\x8eE9T\xd8\xf3\xcf\xc3ukt\x19:\xee\xa6\xf6\xa4\xd5\x1fz\xf5\xc4j\x14-\xb1\xe8c|T\xc7E\xeb\x10ɟ\xf6(\x87N\xc1\xe9\x94Tt\xc1\v\xf7\x85\xcd\xeb\x16\xfc\x99\x92\xdfSE\xe7\\\xa1\xb5\xe5\xb2s\xbe\x1e[(2\x19\xf2\xbc*\xed\xaa\xf5\x84Zm\xae\xba\xcd\xcb*\xcf\xf9\xb4\x94^\xe1\xcfy:]\x8fG\xb8\xf2\x8e:\xefT\x9ek\n\x80=3Ӥ\xbeDUj\vΏt6-A\xc3\x1c\xf0\x80\x12\x94t\x99.\xae\x0e\x8e/A:\x1c\x13\x81څ\x12Ri\xaaR(\x96\xd7'\xbc\xd6^\xa1l僓_\xfa\x80\xfa\x9d\x99\x81ٔt\x8b a\xacP\xbc:\xb9\x82\x9cY\\E\x81&ɾ(\xb3e\x86\xf7\x19\xdd|\xb0\x96<\x84\x98\xd5<,\x1d85\xb2\xd6\xc4VY&@V\xc5ƫvVw\x88\xd1oT@Єܧ\x99\xe3\xe57ƥ\xc5\xdd(\xba8\xde\xd9u\xcd?'\xef\xac\x199\xb53Se\x19\x1a\xb3\xad\x84\x88\x19\xf9\r\xe7\xbe\xfe6]V\xdfb\xb53\xd7ɋ@\x97\x12X\x17\xd4\xf39\x81\x05\x1a\xc3vu\x99\xb3g\xd2@;\x94\xe8\f\xa0X\xe4ѻ\x88m\x0eY\xbfȗ\x8fe\xb1\xccV,LP\xe7\x00tz\xbd\x8b\xd9MB\xed|)D^\x17\x82\xadU\xf3\x898\xf9Vr\x9d\xa2\xcao\x9a\x8e\x84\x1b\x17\x86v\x84h\v\xf7\xa2\xe0;Nz\x90\x88\xb4cz\xc3v\xb8ʔ\x10\xe8\x12\xce\xc7\xebz\xcb\xc3\x1a2\xf5\xbe\"3\x8b[\xfb\xd4\xed\x1bb\x1e\x9eھF\x06\xf3\x85\x16]\x1dW\xcb5\xb6\x85\x91G\vRn\xe2\x93T\xb7\xc7B\xb4\x84\xf0x\xa5ݾ\xf5\x01\vr5Xҡ\xa2\xf0E0\x06\xe3~m\xc1~S\xfa\x02\n.\xe9\x1f\xb2\xfb]P\xa2\x1e|\xd2\xfa]\xf5\xba\x85u\xdfQ\x9f&a\xba\xa3H\xb1>\x10S\xa6jM\x92e\x19Y\xf5xi,\x13\x11A\xfa]iWξ%6\xc7\xfc/\x11\x83o\x84\xf0\xdbn\xff\xe6[\xf5F\x8d:p\x1es.\x8d\xdd+\x91\xa8J\x05\x97܌\x12\x9e5\xb7\x96\x04w\xf7\x96\x10,\x89j!\xc0\x90\xf0\x9a\xa8L8\xa7B\xdc{R\xf2\xb7\xd3Q˾#\xd5t\x9e\xb2\x11\xc2\xe6\x14\x91e\xe3P0\xb1-\xff\x99\x147\xf5X\"e\xb6grGL\xa5U\xb5\xdb\xd7|9\xa1\x82\xa7\x82~\x15-\nJw\xb0M}Cc+-;Q\x9fpg\x93w\x96˲\xa7ɕ\x86(t]\xb3\xff2\xd4\x1a\\m\xb5*V\x81\x16\xeeb\xe5\"Db4W\xe4h\xd8}\x14\xe5\xe0\x8b!\x87\xa2^\x8e\r\xca\x12%0\x13֓\xf0\r\xd7H\bvU\xb1\x9b.\xc1\xe0\x8c\xe0\xe3O|\xeb\xaf\xf12Z\xf5\x9f\xffቾ\x87$\xe3\xe8ݬ]\xe4L\x9e\xc6\xc0Y\xa8\x81}'\x90\f\x16\x83\xd87\xb9ޝd[\x1f^\xe6-\xbe\xa6\xabX\xff\x92\xc5\xeb8P\x87\x979\x89o\xe6!\xbe\xee\ue799\xab\x9e\xbft\xc6\xfe\x1a\xbaE\\\xc4\x00!\xe2$F\xb6Ѹ\x8d\x8bNb\xc7G\xac\xd78Q`{\xe07\xbe\x92\x97\x18\xd5\x03\xa3\x87N\x80杳\x1df\nO\xda\xc8\x1b\xcb2$v\xfd<\xfcŚs_\xfe\xbd\xfeQ\x1a\xf7g\xa6\xa4W\xb7\xe6\n\xfe\xe3\xbf\xce \x84v\x1f\xeb_\x9f\xa1\x87\xff\x17\x00\x00\xff\xffp,\xdd\xe3\xddg\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms\x1b\xb9rw\xfd\x8a.\xe5\xe0\xf7R\"\xb5\xae\x1c\x92\xd2\xcd+\xdb\x15\xd5nl\x95\xa5\xa7wHr\x00g\x9a$V\x18`\x16\xc0PfR\xf9\xef)|\xcd'f\x06CS\x1boʸ\xd8\"\x81\x06\xd0\xdd\xe8/4\x9a\x17\xab\xd5ꂔ\xf4\t\xa5\xa2\x82\xdf\x00))~\xd5\xc8\xcd_j\xfd\xfc/jM\xc5\xf5\xe1\xed\xc53\xe5\xf9\r\xdcVJ\x8b\xe2\v*Q\xc9\f\xdf\xe3\x96r\xaa\xa9\xe0\x17\x05j\x92\x13Mn.\x00\b\xe7B\x13\xf3\xb12\u007f\x02d\x82k)\x18C\xb9\xda!_?W\x1b\xdcT\x94\xe5(-\xf00\xf5\xe1\xa7\xf5?\xaf\u007f\xba\x00\xc8$\xdaᏴ@\xa5IQ\xde\x00\xaf\x18\xbb\x00\xe0\xa4\xc0\x1bؐ\xec\xb9*\xd5\xfa\x80\f\xa5XSq\xa1J\xcc\xcc\\;)\xaa\xf2\x06\x9a/\xdc\x10\xbf\x0e\xb7\x87\x9f\xedh\xfb\x01\xa3J\xff\xd2\xfa\xf0W\xaa\xb4\xfd\xa2d\x95$\xac\x9e\xc9~\xa6(\xdfU\x8c\xc8\xf0\xe9\x05\x80\xcaD\x897\xf0\xc9LQ\x92\f\xf3\v\x00\xbf\x1d;\xe5\xca/\xf8\xf0\xd6A\xc8\xf6X\x10\xb7\x16\x00Q\"\u007fw\u007f\xf7\xf4O\x0f\x9d\x8f\x01rT\x99\xa4\xa5\xb6Hq\v\x03\xaa\x80\xc0\x93\xdd\x16H\x8f~\xd0{\xa2Ab)Q!\xd7\n\xf4\x1e!#\xa5\xae$\x82\xd8\xc2/\xd5\x06%G\x8d\xaa\x06\r\x90\xb1Ji\x94\xa04\xd1\bD\x03\x81RP\xae\x81rд@\xf8˻\xfb;\x10\x9b\xdf0\xd3\n\bρ(%2J4\xe6p\x10\xac*Ѝ\xfd뺆ZJQ\xa2\xd44\xe0ٵ\x16W\xb5>\xedm\xef\x8d\xc1\x80\xeb\x05\xb9a't\xdb\xf0X\xc4\xdc#\xcd\xecG\xef\xa9j\xb6k9\xa4\x03\x18L'\xc2\xfd\xe2\xd7\xf0\x80Ҁ\x01\xb5\x17\x15\xcb\r\x17\x1eP\x1a\x84eb\xc7\xe9\u007fհ\x15ha'eD\xa3g\x80\xa6Q\xaeQr\xc2\xe0@X\x85W\x16%\x059\x82D3\vT\xbc\x05\xcfvQk\xf87!\x11(ߊ\x1b\xd8k]\xaa\x9b\xeb\xeb\x1d\xd5\xe14e\xa2(*N\xf5\xf1\xda\x1e\f\xba\xa9\xb4\x90\xea:\xc7\x03\xb2kEw+\"\xb3=\u0558\x19B^\x93\x92\xae\xecҹ=Q\xeb\"\xff\x87\xc0\x00\xeaMg\xad\xfah\x98QiI\xf9\xae\xf5\x85\xe5\xfa\t\n\x98\x03\xe0\xf8\xcb\ru\xbbh\x10m>2\xd8\xf9\xf2\xe1\xe1\xb1\xcd{T\xf5\xb1o\xf1\xdebȆ\x04\x06a\x94oQ:\"n\xa5(,L\xe4\xb9\xe3>˺\x8c\"\xef\xa3_U\x9b\x82jC\xf7\xdf+T\x86\xc9\xc5\x1an\xad\x88\x81\rBU\xe6\x863\xd7p\xc7\xe1\x96\x14\xc8n\x89\xc2W'\x80\xc1\xb4Z\x19Ħ\x91\xa0-\x1d\xfb\x9d\x1d\xd6Z_\x04Y6B/'\x10\x1eJ\xcc:\aƌ\xa2[\x9a\xd9c\x01[!\x1by\xe1\xc4պ\x032~dM\xcb\x14}\xe0\xa4T{\xa1\x8d\xfc\x15\x95\xee\xf7\xe8-\xe8\xf6\xe1\xae7 ,\xc6/͊\x95Jan\xce\xd9\v\xa1\xda,o\x00\x13\f x\xb2\x12&\xc0\xb3\x92\xa6R\xa0+\xc9\xed)\xfd\x82$?>\x8a\xbf)\x84\xbc\xb2\xcc\x1at\xc5\x15lp+$F\xe0J4\xe3Mg\x94\xd2 F\xd9%\x89J\xaf\xe1q\x8f\x06\x8d\xa4b\xda\xf3=U\xf0\xf6'((\xaf4\xae\a\xd0F\b\xec\x90b\xc1\xb8\x1d\xa8G\xf1Q9R͠\xef\xfdȰ\x16\x12_\xf6\xa8\xf7(\xa1\x14A\x04Gv\xb9\xa5\fA\x1d\x95\xc6\xc2S<\b\xbe\x8dǾe\n\xc6<\b\x05\x9bcX\xf3p\x9fFߒ\r\xc3\x1bв\x1aN\xe7а\x11\x82!\xe9\v\xe1>\x1e\xbe\xa0\xd24\x9b\xc1\xc2e\x1f\rnT\x04\t\xd2\u007fa\xf7\x16\xc1æ\xe15M\x9e\x11H\xc0\x86Q\x0e\x8c\xb5\x90\xd8\xc1\x00\xfc\a\x87\xf7FreF\x9e\fW\v^rQdVZr\x01L\xf0\x1dJ7\x9b\xd1\n/\x9413\xbd\xc4B\x1c0\a#0$2#\xf9`[\x19Y2\xc43\x80\xe1\xe5Q\x1e\xa0\\i$\xf9\xfa\xf2\x9c\x04¯\x19\xabr\xcco\x9d)\xf0`\x8c\x98`\xd2\r\xc4A\x8fN\x1f\xa6\xc6z-\xc2hf\xed\x8f\xda\xd40\x9d\xa2\xe72\xe8\x92c\x89\xceN3T\xf3\xcbk\x94\x84\x97_p\xb7\x05\x85\xdat\xb9\xfc\xc7\xcb+C\xcc\b\xd0Τ\xdd)\x14\x10\x89\xf5\ue1f4\xa0\x1a\x8b\xc8\xfe'\x0f}\"!\x88\x94\xe48B\x86\xc6z\\J\x84\xc8\xc8\x1e\tx\xe8\xf1\xc7\x12\xa17ퟋ\f\x8b\xb1\xaf\xac\x83C(7\xa87.E\a\xf3ʚ\xe9\x91\xed\x18,\x18+\x86r\a\xcfZ\xe0\r\xa6\xbf\x17\xbc,e\xca1V\xacy\xc0\xf3\x98\xf1]HT_\u007f\xc7H\xd9\v\xf1<\x87\x88\u007f5}\x1a+\x182\xeb3\xc3\x06\xf7\xe4@\x85\xf4[ot3~Ŭ\xd2\x18\xd3eDCN\xb7[\x94\x06N\xb9'\n\x95s\x84\xc6\x112n\xd8A\xeb\xb4G\xbf\xec\xed\xa3!\xa4\xe1T\xbb\xf3\xb1\xa5\x1b\xe5\xdc\xd72\xa1\x99\x85\x1a\xdb\xcbj\xb3\x9c\x1eh^\x11f\x15\x1b\xe1\x99\xdb\x0f\xa9\xd7\x15S\x90\x13D\x1e\xac٩ͰrC\x89\x8e\xa1,8\x82\x90P\x18\xef`ص\xef\xcf4ml\xdb\x1bbt\xbfp,*+\x86\xcaO匭F\x06\\\x8d\x82\xae)\xe2\x9d\x83h\x1c\x8ff\xfe?1a\x96s\xfc]\u007f\xe4Y9~\x92*s\x10\rU\xea\xe9\xff\x84D\xb1\xca\xe2\xc1\xeb\x8ad\x82\xfc\xda\x1eu\x05t[\x13$\xbf\x82-e\x1ae\x8f2\xdft^\u0381\x8c\x14}gZAt\xb6\xff\xf0\xd5X^\xaa\xb9~H\xc4K\u007f\xb0\xb3_\x83=\xdfU\xcc3p\xc1\x86+\xa9\xc4\u0085A\x1f-6\x9bO\xacE\xf5\xee\xd3\xfb\x98\xfb\xd3m\t\x9c7\xd8Ȼ\xdeb\xdbS{\xa3\x19\xa7iw\xffHh\xecjx\xd8\x16\x92M\x8f%%\xc5Z\x87l!;\xa9\x9d6U\x90\xaf\xb4\xa8\n \x85A}\xaa۳u9M\x1d\x8aיM\x16\xaeU#Z\x98CU2ԩ'\xd2\xe50\x99c\xa2h\x8e\xb5b\xf6\\ 8\x10\xd8\x12\xcaFRI\x86m\x11n\x97\xf8\x1a^X\x9cωH\x9b|eQ\x91\x10\x88M4\x16\xa7\xa5u)\xd3M\xc5{\x89i\xe6\xd9\\P:\x98g\xa5\xa4\x86\x97Ĺ-4\xcfb\x84\x1f\u007f\x98h\x83\xf6\xc3D\x9bi?L\xb4\xd1\xf6\xc3D\x9bo?L4\xdf~\x98h\xa1\xfd0\xd1~\x98hSݦ\xa4\xf5܊\xdc\xfb\xab\x91/gW\x91p==\xb5\xc4\t\xf8>\x9b\xc2'Q\xa7fX\xde\xc5GEr\xe4}\x12\xf4\xcaf\xdf\xc68\xa0I\xbahTI\x9dri\x0eH`o\xf7\x9cd&\t\xf3\x1br\xd1ä\xa7\xe4\xa2\xdfM\x8d=O.\xba_^\xdf\xe4>O&z\xd8{\xfc\x15J\x04\xa2Opq\xe9\x16\x05\x92\x10bw\xf7\xe9\xf9Ȍ\xbd\xc9\x06p\xff\xe0\x14\xdaA\x8aWr\x86\xf1 \xc3\xeb\xfci\xef#\xf4\x1e\xd0'\x02\x91*\xc3\f\xdf\x1fz\x17cu\x1c\x99}4E6\x15\x1e\x8e\x1a?\xb2\x9d\x94\xd5M\x80\xfb>9r)\x1f\xa6f\xbaO\xe1k(XZ\b\xfbNO\xf0L\xda\xd9x\xb2\x99\xbfZEM\x0eo\xd7\xddo\xb4\xf0\xa9g\xf0B\xf5>\xb2\x95\x97=r{)\xccw\xed<\xf2\xc0o\xfe\x05o\x1f\x8f $p\xca\xc6Dt\xfd̹#\x8d?\x97.&\xb2X\xd1M\xfb\xf3i\xc9i'\xa7\xa4uS\xceFl\x9e\xa5w\xb0\xe9\x99\xf7\xe9Ig\xd3YbKR\xcd\xfa\x89d\xa3@\xe7\x13\xccRB13\xc9d'\xa4\x90%\xa6\x0f\u007f\xf3MsJ\x92\xd8I\xa9a\xb3\x19\xb6\x89\ta\xddT\xafi\x90\v\xd2\xc0\x92\x903\x9f\xf2\xb58\xd1\xcb'VM\xee#9\xbd+\x92\xb85\tx4\xa9k*]k&\xcc;L\xe5JOҚ\x04m\x13\xb8\xe6S\xb3Η\x80}\x0e\xa7r\\\xd4̦W\xcd:\x9d\xd3\xeb\x9bM\xa0Z\x9265\x8b\xb1\x13S\xa4\xea\x14\xa8\x91y\x97&Fu\x13\x9fF\x80\xa6\xa4C\x8d\xa4;\x8d@\x9cL\x82JMr\x1a\x81=\xa3v'\xb9d\xe2\xcbx\x9d\t\x98\xd5o\xec\x8f\xe2\xa8S7&d\xc7\\\x9c\xb3\xd0?\xf7\xba\x1bZ\x06\xabi\xda\xfc\x8cY\x9eT\uf5db\x9fE\xc54-\x99\xbd\x1f;\xd0<\x1a\x85\xd1{<\xd6U\x03~\x13\xf6\xdd\xe0\xe6h!}\xfeR\xb3\xe7\xbagD\x13\x05/\xc8\x18\x90\x18s\rv\x9e\xb9R)\x99X\xa1\x91\xf9\xe6\xc0\xf9z\b\xbe\xa2ʕ\xe3`\xfb42v\x85\xa0\xf7X\x18(\xa1\xb0\xc2\x02\xf7c\xda@t\xb6\xac\xfd\xec\xf7\n\xe5\x11\xc4\x01ec1\xcc<\xccq\aM\x19\x8f<\x1c}/?\\\x81\x9e\x9e\xe1\xdc\x1c8xǝ\n\x8b\x82\xed\xad\xd1\xc21g\x9eմ6\xe2\xcd\xf8\x01#]\xe3\x91DQ\x8f\x8e|?g{\xa6\xbejy]\xd7a\xb9\xf30\xab\xb6_Ł8݅\x98\x00\x99\xfaJ%\xedFw\xf6U\xcak\xb9\x12s\xceD\xb2\x15\x95\xf6\xea\xe45^\x9b,xe\xb2\xc0\xa9X\xe6V$\xa3)\xe55ɫ8\x17\xaf\xe8^\xbc\x86\x83q\x9a\x8b1\x03\xb2\xf7J$\xe5\xfdGR\xb6B\xf2\x85]J\xb6\xc1\xfc\x9d\xda\xf4\xbb\x8e\x84\xf7\x1c\t\xb7ms+Mx\xb7\xb1\xec\xbdF\x02\x0e_\xc9\xf9x%\xf7\xe35\x1c\x90\xd7uAf\x9d\x90YΙ\xfc\xfa\xe4貐9\xca\xc9`|*\xabM2Y\xcf_\xe8\xce\xd9{\xa2\x1e\n\x88\x99^\x1d\xd34\x16R\xae\x9fSg\xf0\v\xe5\xfer\xcf0UK\x8fwn\b\x1a\xc3\"~\xeb\xd7Xm\xbe\xc0\xa2\xbbVPX\x12i\xaf\xfd6Gwׯ\xd6\xf0\x81d\xfb\x1e\xf4}\xd4O\xd8\nY\x10\r\x97\xf5\x9d̵\x03n\xfe\xbe\\\x03|\x14\xf5-r\xbbD\x89\xa2EɎ\xc6\x0f\x88\xc0\xbcl\x838\x8d!\xa2̤|5@_\x1em\xc6\xf7{\xe8\xf6\x8e\u070e\x87\xcap\x01\xae\x8a;>\x84\x1f\xe1\xfe\xc9Z'\xb6\x02O\xd6T#\xf2\xf6G\xf0\xfe\xfaŊ~>\xff=\xb9\xd2B\x92\x1d\xfe*\\\x81\xc79\x1ct{w\xaa{z\xa9\x11\xf2V³\xa6\x986\xf5\xa5&{\xc0\x9at\xb4A\x99A\xb3ʘ8\x998\x89Z\xb3\x99\xcd<>\xfe\xea6\xa0i\x81\xeb\xf7\x95\xcbIX\x95D*4\xd8\f\x1bs\x836\xe6\xbf{\xf1\x12\x8br\b\xbf\xe7\x9f\xfb\xeb\x96hS\xdel\xeaâ\xd5\x1f:\xe5*\x03\x8a\xe6X\xf4)>\xaa墵\x88\xe4N{\x94C\xc7\xe0\xb4*\xf6\xda\xe0\x85}\xb2v\xde\nZc\xf2{\xac\xa6\xa9\xad\xe39_\xd5ԕ\xfb\xf45\x8c}\xe2d%m\xf9+_\nԖ\x8b:\xad\xb0\xa9\xcb\xf3\xeaԕ\x9e\xa6\xd3\xedp\x84\xad\x1e,\xf3VaӺ\xbe\xe4\vQu.YT\xa56\xe0\xdcHk\xd3\x1ah\x98\x03\x1e\x90\x83\xe06u\xcc\x16\x96r\x15\xae\xfbc\"P\xdbP|nZU2A\xf2p\u0083\xf6\xf2U\x91\x1f\xad\xfc\x92\a\x94o\xd4\x04̺bh\x04\tC\x85\xe2\xd4\xc9\r\xe4D\xe3*\n4I\xf6E\x99-S\xb4\xcb\xe8\xea\x9d\xd6\xc6C\x88Y\xcd\xfdʴc#\x83&\xd6B\x13\x06\xbc*6N\xb5\x93\xd0!F\xbfA}Z\xe5\x93\t'\x8e\x97\xdb\x18\xe5\x1aw\x83\xe8\xe2pg\xb7\x81\u007f\x16\xef\xac\x1e9\xb63Ue\x19*\xb5\xad\x18\x8b\x19\xf95\xe7\x9e\u007f\x9b6Mv\xb6|\xa0\xed\xe4D\xa0ͱ\r\xf5Z]\x92m\x81J\x91]\xa8\x1b\xf8b4\xd0\x0e9Z\x03(\x16yt.b\x93\x94٭\x9a\xe7bY$\xd3\x15\xf1\x13\x84\x1c\x80V\xaf71\xbb\x89\x89\x9d\xab\xb4KC\x9d\xf1\xa0\x9a\x17\xe2\xe4kIe\x8a*\xffPw4\xb8\xb1ahK\x88\xa6.<2\xba\xa3F\x0f\x1a\"\xed\x88ܐ\x1d\xae2\xc1\x18\xda\x17\x1c\xc3u\xbd\xe6a\xf5\xa9\xaf_\x90\xa8٭}l\xf7\xf51\x0fGmWt\x86\xb8:\xbe\xb6L\xb8\xa6\x12\x9b\xba\xfb\x83\x05\t;\xf1\"\xd5\xed\xb0\x10\xadP?\\i\xbbo8`^\xaezK\xda\x17\xac\xbf\xf2\xc6`ܯ-\xc8oB^AA\xb9\xf9\xc7\xd8\xfd6(\x11\x06/Z\xbf-\a9\xb3\xee{ӧ~\x81\xd0R\xa4\x18\x0eĘ\xa9\x1a\xcf:_\xc1'\x1cZV.\x91\x1cs\x1b\x86\x8b\x95\xe57]\xee\xf8\xbd\x14;\xe3\x19G\xbe\xfc;\xa1\x9a\xf2\xddG!\xefY\xb5\xa3\xfcs\xe93O\x97u\xbe'RS\xc2\xd8ѭ'2\xb6\x96\x92\x91\xef\xe6G\x8f~\xf1\x1e\x8d\x86\x1c5\x93\xe2\xf4\xf3\xe8\x98#\xa1\xef\xd6x\xfb\x94;\x96\xb3\xb9\xe6\x1bQ\xe9\x8e\xccjd^\xfc\xa6\xc1\x02[\xc3'\xa11\x04\x91i\x17\xa6\x91\xf2\xa8\xf4\n\xb7[!\xb5\v.\xacV@\xb7\xde\xec\x8ay\x93\x842{\t\xe6\xca\xf1\x03\xd5M\x1e@sL\xacG%\xedi\xb7U\xda\nrty\xc7$ˌU\x8f\xd7J\x13\x16\x11\xa4ߔve\xed[\xc3\xe6\x98\xff-b\xf0\r\x10~\xd7\xee_\x17\u007f\xa8ը\x05\xe70g߅8%\x12U\xa9`_\v \x87\x17I\xb56\x82\xbb}K\bڈj\xc6@\x19\xe15R\xeasJ\x85\xd8\uf352\xbf\x1b\x8fZv\x1d\xa9\xba\xf3\x98\x8d\xe07'\fY6\x16\x05#\xdbrI\xb1T\x85\xb1\x86\x94ٞ\xf0\x9da*)\xaa\xdd>\xf0\xe5\x88\n\x1e\v\xfaUfQPڃ\xad\xc2\r\x8d\xae$oE}\xea|\xe7f\xb9${\x1e]\xa9\x8fB\x87\x9f\x84\xb9\xf6\xc5;W[)\x8a\x95\xa7\x85\xbdX\xb9\xf2\x91\x18I\x85q4\xf4>\x8arp\xb5\xf6}\x95<\xcb\x06e\x89\x1c\x88\xf2\xebIx\x149M֩P\x88&R\xa7\xba/\x0f\x9d\xce3\x9e\x8b\x85\x1c_\uf0cf4\xb9ǡ\xb7\xfd\x1f\xe7\xb9\x02Ey\xf85\x1a\x17\xc7r\xac\xa0\x8cC#\xd1\x06\x05\xa2wf\x03W\xa4\xe3xt\x97\xff\xc7\xfa\x1c\x87Z\x95}H1^\x9fz\xdd{\t\xc1\xf6G\x17\xea.\xde\xe0\x8c\xe0\xe3/t\xeb\xae\xf12\xb3\xea\xbf\xfe\x9f'\xfa\x1e\x92\x8c\xa37\x93v\x915yj\x03g\xe6'\x16\xee\x19\x1a\x83E!vM\xae7\x8bl\xeb\xc3i\xde\xe29]\xc5\xf0CI\xe7q\xa0\x0e\xa79\x89\xaf\xe6!\x9eww/\xc4\xfe8\xcb\xdc\x19\xfb\xbb\xef\x16q\x11=\x84\x88\x93\x18\xd9F\xed6\xce:\x89-\x1f1\xacq\xa4b}\xcfo<\x93\x97\x18\xd5\x03\x83\x0f\xad\x00\xcd[g\xdb\xcf\xe4?i\"o$\xcbа\xeb\xa7\xfe\x0f\xa2]\xba_\x17\t\xbfyf\xff\xcc\x04w\xeaV\xdd\xc0\xbf\xff\xe7\x05\xf8\xd0\xeeS\xf8q3\xf3\xe1\xff\x06\x00\x00\xff\xff?.\xf4\u007f\x8aU_\xbdٓ\xe9t:a\x8d|F\xeb\xa4\xd1s`\x8dğ=j\xfa媗\u07fbJ\x9a\xd9\xfe\xfbɋ\xd4b\x0e\xcb༩\xbf\xa23\xc1r\xfc\x84\x1b\xa9\xa5\x97FOj\xf4L0\xcf\xe6\x13\x00\xa6\xb5\xf1\x8c\xa6\x1d\xfd\x04\xe0F{k\x94B;ݢ\xae^\xc2\x1a\xd7A*\x816\x12/W\xef?T\xbf\xab>L\x00\xb8\xc5x\xfcI\xd6\xe8<\xab\x9b9\xe8\xa0\xd4\x04@\xb3\x1a\xe7\xb0f\xfc%4\xce\x1b˶\xa8\fOwU{ThM%\xcd\xc45\xc8\xe9\xea\xad5\xa1\x99C\xbb\x90(d\xb6\x92H\x1f#\xb1U\"v\x9f\x89\xc5u%\x9d\xff\xf3\xe5=\xf7\xd2\xf9\xb8\xafQ\xc12u\x89\xad\xb8\xc5\xed\x8c\xf5?\xb4WOa\xedTZ\x91z\x1b\x14\xb3\x17\x8eO\x00\x1c7\r\xce!\x9en\x18G1\x01ȘEjS`BD-0\xf5h\xa5\xf6h\x97F\x85Z\x9f\xee\x12踕\x8d\x8f('Y \v\x03E\x1ap\x9e\xf9\xe0\xc0\x05\xbe\x03\xe6`\xb1gR\xb1\xb5\xc2\xd9_4+\xffGz\x00?9\xa3\x1f\x99\xdf͡J\xa7\xaaf\xc7\\YM:z\xec\xcc\xf8#\t༕z;\xc6\xd2=s\xfe\x99))NZ\a\xe9\xc0\xef\x10\x14s\x1e\x8c\xb6e,\x1e?\x97ț\x1c(\xfb[ƪ\x82E\xf6\\\xb3\x81\x0f \xa4\xa3\x02\xc0E\xa2C\xb0\xa8<\xa3\xf59x\x1b\xde$>7z#\xb7C\xa1\xbb5\xcd%\x8b\xb9B\xba\x87\xdc2\xdeD\xa1\x89\xac\xa3\xb1f/\x05\xda)\xf9\x87\xdcH\x9e9\t6e\xae\x8dD%\xdcP\xd2\v^\x16E\xb1(ȫ\x99\xba\xa2\xc3\xe5ic,\x8d\x99\xd4ɂ[\x021\xd8\xd8:\xa7T\xedQ\x8bS5rƍ\x89Qˡ\x80\x83\xf4\xbb\x14\x0e\u0558\xdf\xc1\xab\xbeG\xe3\x05\x8fc\xd3=ޟvH;S\x02Ep\xc8-\xfahm\xa8\xc8|Ȕ*\x80/\xc1ŀڏ\x13e\xc4B\xad\x9c~\xc1\xe3\x10h\xb8\xa6\xdc\\\xc2\\g\xf9\x8eJ\xe7°\xc5\rZ\xd4~4\xa8Sgb5z\x8cq]\x18\xee(\xa4sl\xbc\x9b\x99=ڽ\xc4\xc3\xec`\xec\x8b\xd4\xdb)\x01>\xcd\x1e4\x8bm\xc5\xec\xbb\xf8\xe7\x82\xc8O\x0f\x9f\x1e\xe6\xb0\x10\x02\x8cߡ%\xadm\x82*\x86֩o\xde\xc7\x1c\xfb\x1e\x82\x14\u007f\xb8\xfb\x16\\L\x93<\xe7\x06lV\xd1\xfa\x8fT\xa8E\xa6\b\xa2UҊ\xb1@\x99\x92\x94]gm\xa6X3f\x88c\x15fwP`\xa2\f2\x16Q_p\x18L_q\xb3\\\xec^\xf1\xb1RHK-$\xa7B\xec\xdc7J\x83!\xce\xea\xed\x11\xc1\xfa\x15\xf8\xa5\x880.x\x12 \xe7\xc3+\x1c?t\xf7\xb6mY\nO9\xc79\xf4T@9\xd0H9\x90\xd9!r1(p\xa35y\xa37\xc0N\xa1\xee\xce\xf5c\xfc\x1b#\xc4:\xf0\x17\x1c\x01~ \xcaǸ\xb1`\x9c\x8e\x11/\xc1a\f\xbe\xd7\u0600\xeb6\xce\xd9\x12\xed-\xbc,\x17\xb4\xf1\x94&\x19,\x17\xb0\x0eZ(,\x1c\x1dv\xa8\xa9C\x90\x9b\xe3\xf8]4\x9e\xeeW\x05\xd5Xa\xe4\x1a\xbf`;.C\x8a\xe1sX\x1fGj\x82\x1b\x84l,n\xe4\xcf7\b\xf9\x187\x16\xc0\x1b\xe6w \xb5\x93\x02\x81\x8d\xc0\x9f\x8a\xb5\v\x82\x9e\xf2\xffC\x8e\"ߠ\x9e\u05fc=\xb1\xf3\x16\x87/\x18_\xf1\x9fǼ\xed\x84B\xf9\x9d#\xffy-xɏG%ڟ\x1e\f\xfe\x94*,>\x92*Ϙy\x1e\x9ex\xa5R+\xcf\x16c\xceLu\x81\xb1\x16]c\xb4\xa0\xe6\xe9\xb6:\xade\xf9\u007fW\xad\x8d\xabuz\x1e\xe5zkE\v7\xb5*\xf1\x89\xe6\xcd\xcdJz\xb8\xea\xb6\x02f\xed\xa8Sl\xfb\x95\x9e\x8c\xbfH\x9b\xf2\xaeӧP?\xac!\xe8X\xa9Ō_\xc1\xdf5|\xa2ޖ\xb2\x93\x98\x13\xdfv\xcc\x00\xa4\x03m\x0et\xbcC/\x92\x00\xa3S\xbe\xa6n\x8di\x91\x9b\xe1\xb8t\x90JQƶX\x9b\xfdhƦBӢ:\x02sd:\xfb\xdfT\x1f\xaaw\xbfZ\x17\xa4\x98\xf3\xd4Ԡ\xf8\x8a{9|\xe5\x19\xa2{?8Q\x1c\xff\xe4\x0e\xf4\xe3\xc7\xd2,\xcfl\xde\xf6\xe3\b\x18\x1b\xa9\xa8\x16\x1c\x89\x13m\xc50|\x8f\xfc\xb8\xba\xbfs\xb1\x84G\xed\xc7ʾ\x03Z\x8c\x1d\x13\n\xaa\xe2M~\x97\bΣ\x1d1\x80\x93\xf6\xa2\xceA\x19\xbd\xed9N\x1a\xf9\x95\x82*\xb4dPƂ@O\xa9Io\x81\xef\x98\xdeb\xfb\n\x95\xf9\u007f\x9dS2\x9f\x9eʹ\x16\"\xf5%\xf3\xb8I\xa3Or\xacL\x1f\xbc\x00\xb7\x9b\xc7_\u007f\v\xf7E\xb3\x17ۜ+\xb8\x0f\xf6\x97,M\xa0N}\xfb\"\u070eooo\x87\xcf\xcd7 \xf1ַ\xf0W\xde5\xe0\xc0\\\xfb*\xfe\xeb\xe1PS\xb5z\xb5\x04\xfe\x92v\xa5\xe7\xc3|\x04\xd8\xda\x04\xff\x9agލ\x19t~\xee\u007f\v\x8f\xf1#Ƶ\"\x83\xf6\x14\x8d\xf0`\xa9\x95l_\xc5bP\x18\xcb-\xb7?/-z\xdfZ\xbak\xc3/17\xc85\x9ak\a\x93)_v\xf4\x9aA\xee΄\xf5\xe9\xa5x\x0e\xff\xfeϤMה\x13\x1b\x8f\xe2\x87\xfeǵw)d\x94/d\xf1'\xa7:&}\x1d\x84\xbf\xfdc\x92\xaeB\xf1\\>i\xd1\xe4\u007f\x03\x00\x00\xff\xff\x1d\r\x93\v\x97\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\xc7\xef\xfa\x14\x98}\x0e{y$\xefN\x0f\xed\xe8\xd6\xcd\xee!\xd36\xe3I2\xb9tz\xa0I\xd8\xe2F\"Y\x00t\xeav\xfa\xdd;$%\xbf\xc8v6=\x947\x91 \xf0\xe7\x0f\x04Ī\xae\xebJ\x05\xfb\x84\xc4ֻ\x16T\xb0\xf8\x87\xa0K_\xdc<\xff\xc0\x8d\xf5\x8b\xed\xc7\xea\xd9:\xd3\xc2Md\xf1\xc3=\xb2\x8f\xa4\xf13\xae\xad\xb3b\xbd\xab\x06\x14e\x94\xa8\xb6\x02P\xceyQi\x9a\xd3'\x80\xf6N\xc8\xf7=R\xbdA\xd7<\xc7\x15\xae\xa2\xed\rRv>\x85\xde~h\xbeo>T\x00\x9a0o\u007f\xb4\x03\xb2\xa8!\xb4\xe0b\xdfW\x00N\r\u0602\xc1\x1e\x05WJ?\xc7@\xf8{D\x16n\xb6\xd8#\xf9\xc6\xfa\x8a\x03\xea\x14xC>\x86\x16\x0e\ve\xff(\xaa\x1c\xe8sv\xf5)\xbb\xba/\xae\xf2joY~\xbaf\xf1\xb3\x1d\xadB\x1fI\xf5\x97\x05e\x03\xb6n\x13{E\x17M*\x00\xd6>`\vwIVP\x1aM\x050\xf2\xc82kP\xc6dª_\x92u\x82t\xe3\xfb8Ldk0Țl\x90L\xf0\xb1\xc3|D\xf0k\x90\x0e\xa1\x84\x03\xf1\xb0\xc2Q\x81\xc9\xfb\x00\xbe\xb2wK%]\vM\xe2\xd5\x14\xd3$d4(\xa8?ͧe\x97\x04\xb3\x90u\x9bk\x12X\x94D\x9eD\xe4\xb8\xd6;\xa0#\xbe\xa7\x02\xb2}\x13:ŧ\xd1\x1f\xf2µ\xc8\xc5f\xfb\xb1\x90\xd6\x1d\x0e\xaa\x1dm}@\xf7\xe3\xf2\xf6黇\x93i8\xd5z!\xb5`\x19Ԥ4\x81+\xd4\xc0;\x04O0x\x9a\xa8r\xb3w\x1a\xc8\a$\xb1\xd3\xd5*㨪\x8efg\x12\xde'\x95\xc5\nL*'\xe4\fm\xbc\x04hƃ\x15\x98\x96\x810\x102\xbaR`'\x8e!\x19)\a~\xf5\x15\xb54\xf0\x80\x94\xdc\x00w>\xf6&U\xe1\x16I\x80P\xfb\x8d\xb3\u007f\xee}s:g\n\xda+9\xe4g\x1a\xf9\xd29\xd5\xc3V\xf5\x11\xff\x0f\xca\x19\x18\xd4\x0e\bS\x14\x88\xee\xc8_6\xe1\x06~I\x98\xac[\xfb\x16:\x91\xc0\xedb\xb1\xb12u\x13\xed\x87!:+\xbbEn\fv\x15\xc5\x13/\fn\xb1_\xb0\xddԊtg\x05\xb5D\u0085\n\xb6\xce\xd2]\xee(\xcd`\xfeGc\xff\xe1\xf7'Z\xcf.H\x19\xb9\xd0_\xc9@*\xf3\x92\xf6\xb2\xb5\x9c\xe2\x00:M%:\xf7_\x1e\x1ea\n\x9d\x931\xa7\x9f\xb9\x1f6\xf2!\x05\t\x98uk\xa4\x92\xc45\xf9!\xfbDg\x82\xb7N\xf2\x87\xee-\xba9~\x8e\xab\xc1\nOW2媁\x9b\xdcbSQ\xc7`\x94\xa0i\xe0\xd6\xc1\x8d\x1a\xb0\xbfQ\x8c\xffy\x02\x12i\xae\x13ط\xa5\xe0\xf8\xef07.Ԏ\x16\xa6\xf6}%_\x17\x8a\xf6!\xa0N\x19L\x10\xd3n\xbb\xb6:\x97\a\xac=\xc1Kgu7\x15\xed\x8c\xee\xbe\xc0\x9b\x93\x85\xcb\x05\x9dơM\xceW\xae\x1e\x1er\xee,\xe1\xec\x16\xd6p\xd6s_璛\xe1\xbf$S:\xf1\xc8FG\"trԟեMoe\x81D\x9e\xcefg\xa2\xbed\xa3\xfc\x04P\xd61(\xb7\x1b7\x82tJ\xe0\x05)\x95\x81\xf61\xf5\x194`\xe2\x19\xbf\x11\xcb\xf1\xbf$\x90\xd7\xc8ܜ\xd9Y\xc1ႦW\xb2\x93Fz^\xa8U\x8f-\bE\xbc\x92YE\xa4v\xb3\xb5\xfc\xcf\xfa\x06\x82e\xb2\xb9\x94\x83\xfd\u007f\xfa\x9bIȸ]\x1c\xce#\xd5p\x87/\x17foݒ\xfc\x86\x90\xe7W>-.\v\xbd\xfdc\xe0\r\x94.^ʳIN\xfd\xce\x1cQd\xf1\xa46\xc7\\9\xae\xf6\xfd\xbb\x85\xbf\xfe\xae\x0e\xf7Zi\x8dA\xd0\xdc\xcd_i\xefޝ<\xb7\xf2\xa7\xf6\xae\xbc\x8c\xb8\x85_\u007f\xabJ(4O\xd3\xeb)M\xfe\x13\x00\x00\xff\xff--\nM\xde\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x93۶\x0f\xbd\xfbS`\xf2;\xe4יHN\xa6\x87v|k79\xec4M3\xebt/\x9d\x1eh\n\x96إH\x96\x00\xbd\xd9~\xfa\x0eH\xc9\u007fdٻ9\x947\x81 \xf8\xf0\xf8\x00R\x8b\xaa\xaa\x16*\x98{\x8cd\xbc[\x81\n\x06\xbf2:\xf9\xa2\xfa\xe1G\xaa\x8d_\xee\xde-\x1e\x8ckVp\x93\x88}\u007f\x87\xe4S\xd4\xf8\x1e\xb7\xc6\x196\xde-zd\xd5(V\xab\x05\x80rγ\x123\xc9'\x80\xf6\x8e\xa3\xb7\x16cբ\xab\x1f\xd2\x067\xc9\xd8\x06c\x0e>n\xbd{[\xffP\xbf]\x00\xe8\x88y\xf9\x17\xd3#\xb1\xea\xc3\n\\\xb2v\x01\xe0T\x8f+h\xfc\xa3\xb3^5\x11\xffNHL\xf5\x0e-F_\x1b\xbf\xa0\x80Z6m\xa3Oa\x05\x87\x89\xb2v\x00T\x92y?\x84\xb9+a\xf2\x8c5Ŀ\xcc\xcd~4\x83G\xb0)*{\x0e\"O\x92qm\xb2*\x9eM/\x00H\xfb\x80+\xf8$0\x82\xd2\xd8,\x00\x86\xdc3\xacj\xc8n\xf7\xae\x84\xd2\x1d\xf6\xaa\xe0\x05\xf0\x01\xddO\x9fo\xef\xbf_\x9f\x98\x01\x1a$\x1dM\xe0\xcc\xe0\x043\x18\x02\x05\x03\x02`\xbf\a\x05ʁ\x8al\xb6J3l\xa3\xefa\xa3\xf4C\n\xfb\xa8\x00~\xf3\x17j\x06b\x1fU\x8bo\x80\x92\xee@I\xbc\xe2\nַ\xb05\x16\xeb\xfd\xa2\x10}\xc0\xc8fd\xb9\x8c#q\x1dY'\xc0_Kn\xc5\v\x1aQ\x15\x12p\x87#?\xd8\ft\x80\xdf\x02w\x86 b\x88H\xe8\x8a\xceN\x02\x838)7dP\xc3\x1a\xa3\x84\x01\xea|\xb2\x8d\x88q\x87\x91!\xa2\xf6\xad3\xff\xecc\x930$\x9bZţ\x1c\x0e\xc38\xc6蔅\x9d\xb2\t߀r\r\xf4\xea\t\"f\x9e\x92;\x8a\x97]\xa8\x86_}D0n\xebW\xd01\aZ-\x97\xadᱨ\xb4\xef\xfb\xe4\f?-s}\x98Mb\x1fi\xd9\xe0\x0e\xed\x92L[\xa9\xa8;è9E\\\xaa`\xaa\f\xdd\xe5ª\xfb\xe6\u007fq(Cz}\x82\x95\x9fDf\xc4Ѹ\xf6h\"k\xfe\xca\t\x88\xea\x8b`\xcaҒŁh1\t;w\x1f\xd6_`\xdc:\x1fƔ\xfd\xa2\x9c\xfdB:\x1c\x81\x10f\xdc\x16c9Ĭ<\x89\x89\xae\t\xde8\xce\x1f\xda\x1atS\xfa)mz\xc34\x8aYΪ\x86\x9b\xdci`\x83\x90B\xa3\x18\x9b\x1an\x1dܨ\x1e\xed\x8d\"\xfc\xcf\x0f@\x98\xa6J\x88}\xd9\x11\x1c7ɩsa\xedhb\xecd\x17\xcekR\xea\xeb\x80ZNO\b\x94\x95fkt.\r\xd8\xfa\b\xeaP\xf9\x03\x81\xf5I\xe4\xf9\xca\xcd\xe0Tl\x91\xa7\xd6\t\x96/\xd9I\xb6\u007f\xec\xd4i\xa3\xf9?\xd6m-\xbd\x82\x06 \xa5{|W\x9fE\xbc\x8c\x01f\xd5;\x8bd\x14\xb1\xd0 \xbcJ+\x90&u\x8c\xe9|k\x19\xe8R?\xbfA\x05?g\xcc\x1f}{u\xfe\xc6;\x16\xb9_u\xba\xf76\xf5\xb8v*P\xe7\x9f\xf1\xbde\xec\u007f\v\x18\xcbUz\xd5u\xbc\x91\xf7\xb7Թ\xe3\x1dJ/\xc7\xcbY\f\x0ewH\xc9^D68\xbd\b\xda\xcd\xfa\xf6[\x92\xbe\xe0~\x95\xd6\v\x856\x8e|\xa1>\xaf\x1a\xb9\x92G\xd5Ȓr\xcb \xc8C%:d\xa4C\xc3{4\xdc\xcdF\x04x\xec\x8c\xee\xf2\xc2,9\xe9\xa5D^\x9bܙ\xbe\x1d\xbeT\xaa\x898#\xfb*\x97ÌY\xc0\x9f\x99/\xf4\x97K\x1bTCͿ\xa8G\xb1\xe2D\xdfХ\xb2\xffH\xb5N1\xa2\xe3!J\xbe\xb5\xa7\v^ڦ\xc6\xda\xfe\xfd\xee\xe33\xbd\xea\xfd\xc13\xbfK\x95q\x05M\x88X\x91i\xe5\xad!sҭr\x179'\xa3\x8cӷ\xcf)Q\xb3'\x8a_\x83)\x05\xf3\f\xc4\x0f{\xc7\xd2Rѕ\xebr\xfa\xba\xcb\x01\x91\xf2SD\xab\xe9#H\xc6\x06\xa1A\x8b\x8c\rl\x9e\xca\xdd\xf0D\x8c\xfd9\ueb4f\xbd\xe2\x15\xc85Z\xb1\x99\x91\x91\xbc\xc0\xd5\xc6\xe2\n8\xa6K*\x9bM\xf38\xfa\x01\xed\xe4\xfa\x8b\x99\xdb\x1e\xd1~[aޞ\xc3\xf4\xee\xfb\xe0@\xcb\x1a[\xb6\x8e+\x95F\xf9\xf6\xf6\xfa\xfe\xcfw\xa3a\x00m\x94F\xe3Dr\xe8\xe1\x1bı\xc1(\x8cY}A\x04\xc3*\xe0\x14\xc0\xd0\x06\xab\bc\xc8#\x86 \x0eaIu\rZ\x94nȒ\xf4\xa9-0\tj\xf3\v\x96\xae\x80;4D&\t\xa6Tr\x87Ɓ\xc1RUR\xfc\xb7\xa7mI\xd7\xe8І9\x8cq\xe5\xf0y\xd7/Y\x03;\xd6t\xf8\x1a\x98\xe4в=\x18\xa4S\xa0\x93\x03z~\x89-\xe0ge\x10\x84ܪ5\xd4\xcei\xbb^\xad*\xe1R\xfc.U\xdbvR\xb8\xfdʇb\xb1\xe9\x9c2v\xc5q\x87\xcdʊj\xc9LY\v\x87\xa5\xeb\f\xae\x98\x16K\x0f]\xfa\x18^\xb4\xfc\x1b\x13#\xbe\xbd\x18a\x9d)F\xf8|x=!\x01\n\xb0 ,\xb0\xb85\xdc\xe2\xc0\xe8\xe4 \xdf\xff\xe3\xee\x03\xa4\xa3\xbd0\xa6\xdc\xf7|?l\xb4\a\x11\x10Ä\xdcbt0[\xa3ZO\x13%\xd7JH\xe7\u007f\x94\x8d@9e\xbf\xed6\xadp$\xf7\xffth\x1dɪ\x80K\x9fԐ\xc3\xec4i./\xe0Z\xc2%k\xb1\xb9d\x16\xbf\xba\x00\x88\xd3vI\x8c}\x9a\b\x86\xf9\xd8tq\xe0\xda`\"%MG\xe45Ʉ\xee4\x96$=b \xed\x14[\x11=\x14\xb9s6]^\x8c\b\xe7\r\x97\xbe\xacw\x9a.\x82\\p\x99\xecI\xd8\xe4\xc0\xa7&\x87\x19VΈ\x024S/\xdb\xef\x19F.\x1b\x1dl1\xa3pD\f\xf4I\xc5\xf1\xcc=n\x14\xc7\x1cl\xda\n\xaefA[)\xe3#\u007f\xd4I9?\x85>%\x9f\x05L+~\x06W<\x91\x81\xc1-\x1a\x94%&\xc7u*\x9d\xc9 \x1b&\x1as\x8cǕ\x02Nx\xf5,ⷷ\xd7ɓ'&F\xecn~\xee\x19\xfeз\x15\xd8p\x1f\xe8Ο}q\xbd\r\x87y\x9f\xe6\x140\xd0\x02Cb\xda\a\t\x10\xd2:d\x1c\xd46K\x91\xca' \xc37\x18w\xbc\x0e\x1e,\xba\xcaCh!\xde\x03#\xdf)8\xfc\xeb\xee\xdd\xcd\xea\x9f9\xd6\xf7\xb7\x00V\x96h}^\xee\xb0E\xe9^\xf7\xa5\x02G+\frJ\xfc\xb1h\x99\x14[\xb4\xae\x88g\xa0\xb1\x9f\xde|\xces\x0f\xe0Ge\x00\xbf\xb0V7\xf8\x1aD\xe0x\uf593\xd2\b\x1b\xd8\xd1S\x84G\xe1j1\r\xa6=\aH\xbd\xe2\xb5\x1f\xfdu\x1d{@P\xf1\xba\x1dB#\x1ep\r\xaf|Zs\x80\xf9+\xd9\xceo\xaf\x8eP\xfdS0\xedW\xb4\xe8U\x00\xd7\xc7\xe1\xa1\xd1\x1d@\x06\xcb3\xa2\xaa\xf0\x90UM?\x1fT\xc8U\u007f\v\xca\x10\a\xa4\x1a\x90\xf0\x84Iz\xc1Q\"\x9f\x81\xfe\xf4\xe6\xf3Q\xc4c~\x81\x90\x1c\xbf\xc0\x1b\x10\xb1\xd8Ҋ\u007f[\xc0\a\xaf\x1d{\xe9\xd8\x17:\xa9\xac\x95\xc5c\x9cU\xb2ه\xf6\x881\tʄyp\xcdL\ueffa*\x13C;C\x88\xf6\xcb\xd8\xf6[2\xc9\xe9\xffVXG\xe3/\xe2`'\x9ed\xbe\x1f\xaf\xaf~\x1f\x05\xefċl\xf5H\x02\x1etd\xd8\xe58\x93\x98\xbd\x1f-N\xa9c&c\xed\xd7<+3t\xacʤb\xc3\xf6䩄\xed$\aƭ\x18VY`\x06\x81A\xcb4I\xee\x01\xf7\xcb\x10\xe25\x13\x14\x9f)\x04\xf7}\x0e`Z7\"\x1b\x8ac \x8fIh\xe4\x04\x15ڬ\xb2\xc7\ue795ð\xafsF\n\x1f\aK\x93\f\xcet\x96\\\x9d\xb3\xd4Q\xbfi\x8e\x16e\xd7Ρ,\xe1Ai\xc12\xe3\x06\xad\x13ef\xe2\xd5<\xd38!\xac\xc0\xcb3<\x88-\xe8L\xf1\x12E\x112\xbd\xbe\x80\xf1]\xc7\\\x85p\xbc<8\n\x91*t\xca[\xc7\x10\x97\xf9Rr\xb2\x86J\xabɐV|1ed\xa6\xf3\x98&G\x9d\xd1!\xd2y}\xed\x1b\xdeϨ\xb0C#?\xf24\xf8S\x97\xda\xfbTL\xbc\xb4\xc6.\x15\xe5\xe9㧕\xd3⽜\xef\xf0\xed,ã\xba\x8b\x96\xacw\xd0\xf6\x8fg\xe4\x8ad\x18\x90\v;}\x04#j\xc8}\x12M9\xfe\x96\x89\x069\xa4\xb7\x9d\xe9\x9e\f\xd5!\x95\rn\xc9\xdd\a\xd3K\xa5i\x84\xd7'\xaa5\x82\xf5}\xa2\v{\x82fg\x91\xfb\x9eF\x86\t\xf3\xe4u\xabL\xcb\\\xe8k.\xb3De\xd74l\xd3\xe0\x1a\x9c\xe9\xe6\xd3',\xb1EkYu\xce\x14\u007f\x0e\xabB\xc5\x1e\xb7\x00ۨ\xce\xf5%\xfb\xc8=^بS\xcf\xeb\x1ad\x8b\xe1\xb1:3*VlLڛ\xc6\xef\x19:\x82Ã\xa0G\xb5\xc1|\xd0\u007f\x89O\x00\xf0\x0fZ\xe7\x10Қ\x9c\x81\xf5\xde뤅\xc1\t\xa7|\x83\x8f\x99\xd1\xd9C\xdcp\xf22\x99Lf\xeeGo\rϺ\u007f<\xe8\x1c\v\xe22\xa8U\x93\x8cY9ր\xec\xda\r\x1a\xe2\xc3f\xefЎ\xddy\xae?\xe3\xeb\xba\x03\x1b\a\xfb\x93\xfc\x02\xa5X\xaa\x96L\xfa>*Y\x97S\xc0\x85\xd5\r\xdbg\b\xa7\x8b\xf8܍\x8c\x8b\\\xc0A\x9f\x93Qk4~\xea\xb9}%\x8f\xe9J\xc9#\x95F\xb2g!\xdd_\xffr\"\xd3\x13\xd2a5\t\x0eq\x9e\xd8\xf9\x03\x9d\xf2uN8\x91\xc4Xɴ\xad\x95\xbb\xbe:\xa3\x05w\xfd\xc2d\r\xb3\xe79\xec\xa9EUȉ\xaa\xf7-\xcf2\xd5\xf1\x13\xf09\xa8\xa3\xc5g\xa2P||\xceŠ;\xd4̐\xa5\xfb7\x81\xcb\xe9\xa3\xd5k\xb0\xc27:)\xf3\f\xa9hhCX\nN\x94Z)\x83\x19\x97\t\xf3\xb02\n\"c\xf8\xbfg\xfc\xc8\xea\xc9l\xd0#\xe7\x03ڱY>\x1c\xe96\xfdC\xd0\x1a~\xfdmqHlXI\xc5\x13\xf2\x9b\xe9\x1fYĔ3\xfdՄ\xffY*\x19*\t\xbb\x86O\x9f\x17\xe9\xd9\xf2>\xfd1\x04\r\xfe/\x00\x00\xff\xff\xb0\xddǼ\x99\"\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\x1b\xb9\x11\xbe\xf3Wti\x0f\xcaV\x99\xc3]'\x95\xa4x\xb3\xa5lJɮ\xac2e]\\>\x80\x83\xe6\f\x963\x00\x02`H3[\xfb\xdfS\x8d\a9/\x92\xa2*Z\xcf\xc5\x16\xd0h|\xf8\xd0/4'\xd3\xe9t´xBc\x85\x92s`Z\xe0W\x87\x92\xfe\xb2\xd9\xfa\xef6\x13j\xb6\xf9q\xb2\x16\x92\xcfᦱN\xd5\x1fѪ\xc6\xe4x\x8b+!\x85\x13JNjt\x8c3\xc7\xe6\x13\x00&\xa5r\x8c\x86-\xfd\t\x90+錪*4\xd3\x02e\xb6n\x96\xb8lD\xc5\xd1x\xe5i\xeb\xcd\x0f\xd9߲\x1f&\x00\xb9A\xbf\xfcQ\xd4h\x1d\xab\xf5\x1cdSU\x13\x00\xc9j\x9c\x83V|\xa3\xaa\xa6F\x83\xd6)\x836\xdb`\x85FeBM\xacƜv-\x8cj\xf4\x1c\x0e\x13aqD\x14N\xf3\xa0\xf8\x93\xd7\xf31\xe8\xf1S\x95\xb0\xeeߣ\xd3?\v뼈\xae\x1aê\x11\x1c~\xd6\nY4\x153\xc3\xf9\t\x80͕\xc69\xdc\x13\x14\xcdr\xe4\x13\x80H\x80\x876\x05ƹ\xa7\x94U\x0fFH\x87\xe6\x86T$*\xa7\xc0\xd1\xe6Fh\xe7)\xdb\xeb\x01\xb5\x02W\"m\xe9\xe9fB\nY\xf8\xa1\x00\x01\x9c\x82%BD½2\x80_\xad\x92\x0f̕sȈ\xb8L+\x9eɤ3\xca\x04\xce\xef{\xa3nG\xe7\xb0\xce\bY\x1cC\xf6\u007f\x06\xd5\xc1\xf3\xa0\xf83\x91<\x96\xe8e\x12\x9aFW\x8aq4\xb4y\xc9$\xaf\x10\xc8r\xc1\x19&\xed\n\xcd\x11\x14i\xd9\xe3Nw\x91|J\xfaZ3\x97\xb0s\t\x15A\xb6\xb3\xfdS{\xe8ܾ\x0f\x8a\xc7\x05\x10\x8d\x1a\xacc\xae\xb1`\x9b\xbc\x04f\xe1\x1e\xb7\xb3;\xf9`Ta\xd0\xda\x11\x18^<\xd3%\xb3]\x1c\v?\xf1\xba8V\xca\xd4\xcc\xcdAH\xf7\u05ff\x1c\xc7\x16\x17eN9V\xbd\xdf9\xb4\x1d\xa4\x8f\xfdဖ\x9c\xad\x88\xd7\xffM\xe0.\tҭ\x92]^\xdf\xf7F\xc7\xc0\xb6\x94\xa6@\x9c\r\x82hG뻢\xab\x8f3\x17\x06\xc2\xf4\xe6\xc7\x10\xca\xf2\x12k6\x8f\x92J\xa3|\xf7p\xf7\xf4\xe7Eg\x18@\x1b\xa5\xd18\x91\xa2k\xf8ZY\xa55\n]f\xafIa\x90\x02N\xe9\x04mp\x8a0\x86\xa3f5\xff\xce\xc4\xfck\xaf;X\aN\x17>\x9f\xebN\xdc\x00%;\x10\x16X\\\x1aNq :\x85\xec\x8f\xffX\xdc\xcf\xfe9\xc6\xfc\xfe\x14\xc0\xf2\x1c\xad\xf5\xf9\x1ak\x94\xee\xcd>gs\xb4\xc2 \xa7\xc2\x05\xb3\x9aI\xb1B벸\a\x1a\xfb\xf9\xed\x97q\xf6\x00~R\x06\xf0+\xabu\x85o@\x04\xc6\xf7\xe1/ٌ\xb0\x81\x8e\xbdF\xd8\nW\x8a~\xd2\xda3@\xd6\x15\x8f\xbd\xf5\xc7ul\x8d\xa0\xe2q\x1b\x84J\xacq\x0eW\xbe\x12<\xc0\xfc\x8d\x1c\xeb\xf7\xab#Z\xff\x14\x1c芄\xae\x02\xb8}\xbek{\xe4\x01\xa4+\x99\x03gDQ\xe0\xa1\x10\xed\u007f>xSH\xfc\x1e\x94!\x06\xa4j\xa9\xf0\x8a\xe9\xf6B\x00\xfd\xf9헣\x88\xbb|\x81\x90\x1c\xbf\xc2[\x102p\xa3\x15\xff>\x83Go\x1d;\xe9\xd8W\xda)/\x95\xc5c\xcc*Y\xedB\xb5\xbfA\xb0\xaaF\xd8bUMC\xbd\xc1a\xcbv\xc4B\xba8\xb27\x06\x9a\x19w\xd2ZS\x95\xf1\xf8\xe1\xf6\xc3< #\x83*|\xbc\xa3\xec\xb4\x12T5P\xb9\x10r\x9e\xb7\xc6A\xd2L\x9fm\x82\xf98\x05y\xc9d\x81\xe1\xbc\b\xab\x86\xb2Pv\xfd\x12?\x1e\xa6\xfe\xf4\x8d\x94\x00\xfd\xc0\xf1͒\xe83\x0f\xe7+\xd5g\x1c\xae\xfd\xd6:y\xb8u\xb3D#ѡ?\x1fW\xb9\xa5\xa3娝\x9d\xa9\r\x9a\x8d\xc0\xedl\xab\xccZ\xc8bJ\xa69\r6`g\xfe\xc9<\xfb\xce\xff\xf3\xe2\xb3\xf8\xd7\xf5s\x0f\xd4y\xf4\xbf\xe6\xa9h\x1f;{ѡR\xad\xf8\xfc2q5\xacTN0\x11\f\xe0\f\a\xb1\x994\xf22\x8a\xf6\x13*E?B\xaf\x11oE\xe3!\xf6R\xbb\xa2\x874\x95\xbd]\x84\xd3\xf1\xf7^OF+>\xe9\x93\xd6v\xc9\xde\xe4\xc1\xa1\xfa\x13][\xed\xcdv\x9a\x9c\xed\xd3\f\x9fʾ\x83v\xc9c9t\xed\"\xef!f\xbb\xd4ˣ\aˋ\x9f˹\xa2\xc7@\xf7W\x8b\xd36p3\\\xe1{S\x86G\x9f\x105\xfa7hh8n\x99M\x9b\x8c\xdd7\xb4\xf4\x85\xa5>O\x92:\xe4\xbeT\xa7\x97Ċ\x89\n9\xec\u007f7\xf1\xcdq\xeb\x9b4\xd7c\x95iR\xd4X\xe4>n\x8c\x80\x1e\xaeK}O\xce\x1cNI\xc5@B6UŖ\x15\xce\xc1\x99f8}½j\xb4\x96\x15\xe7\xfc\xeb\x97 \x15^\xf1q\t\xb0\xa5j\xdc\xfe\x19\x1f\x1d-Rqm\xa3\x15\\\xd6J(\x99=\a\xe5\x81d\xc6,n\xef\xf2\xa7M\x0eN\x84\xb2{\u070e\x8c\x0e\xfa\xd0\xedɛdB#s?y븈\x80\xb8\xd19\x0e\xa2\x18\x94\xaaJ֭\x1c%\xa5\xa6^\xa2!\"|\xf3;1\x92\x02\xc7X_Ŀ\xa7\x0eL\x1e4\xa4X\x18T\xc5\x17bΤo\x13\x92\xfd:\x05\\X]\xb1݈\xdet\x12_2\x91\xf9\x92\x1f\x1d,&y!\xb9\xbf\x9f\xbb\xb4\x9f\xb3o\xee\x8f\x17tc?\x15\x8c\xddB\xbb\xefߛ\xdf\xff\xaa\xf1:;\x9c(\xe2\xacc\xc6=7\xec-:\xc2\xe7\"\x9eW=\x1e\xefڡk\x18\xa8\xba\xdb\xfc\x911j\x94\xa8\xc1\xa0G\xce[\xbac/\xb4=\xd2,\xf7\x9d\xfe9\xfc\xf6\xfb\xe4\x90\xeeXNU;\xf2\xfb\xfeOڱVI\xbfP\xfb?s%\xc3O\xcav\x0e\x9f\xbfL 6M\x9f\xd2\xcf\xce4\xf8\xbf\x00\x00\x00\xff\xffe\xe5\xd5&\b \x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xdc<\xcbr۸\x96{\u007f\xc5)\xcf\"3U\x96ܩY̔w\x19wR\xed\xea\xee\xc4e\xa7ҋ\xa9\xbb\x80\xc8#\tm\x10`\xe3!G\xf7\xd6\xfd\xf7[x\xf1!\x82$$[\xe9\xf4\xe5N\x14p\x80\xf3\xc0y\x13\x17\x8b\xc5\xe2\x82\xd4\xf4\vJE\x05\xbf\x01RS\xfc\xaa\x91\xdb_j\xf9\xf4\xbfjI\xc5\xf5\xee\xed\xc5\x13\xe5\xe5\r\xdc\x1a\xa5E\xf5\x80J\x18Y\xe0\x8f\xb8\xa6\x9cj*\xf8E\x85\x9a\x94D\x93\x9b\v\x00¹\xd0ľV\xf6'@!\xb8\x96\x821\x94\x8b\r\xf2\xe5\x93Y\xe1\xcaPV\xa2t\xc0\xe3һ\x1f\x96\xff\xb3\xfc\xe1\x02\xa0\x90\xe8\xa6\u007f\xa6\x15*M\xaa\xfa\x06\xb8a\xec\x02\x80\x93\no@\xa2\xd2B\xa2Z\ue421\x14K*.T\x8d\x85]l#\x85\xa9o\xa0\xfd\xc3\xcf\t\x1b\xf1H<\xf8\xe9\xee\r\xa3J\xff\xdc}\xfb\vU\xda\xfdS3#\tk\x17s/\x15\xe5\x1bÈl^_\x00\xa8B\xd4x\x03\x1f\xed25)\xb0\xbc\x00\b8\xb9e\x17a\u05fb\xb7\x1eD\xb1Ŋ\xf8\xfd\x00\x88\x1a\xf9\xbb\xfb\xbb/\xff\xfd\xd8{\rP\xa2*$\xad\xb5\xa3L\xd8\x1bP\x05\x04\xbe8\xdc\xec\x06\x1c\x13@o\x89\x06\x89\xb5D\x85\\+\xd0[\x04R\u05cc\x16\x8e\x88\rD\x00\xb1nf)XKQ\xb5\xd0V\xa4x25h\x01\x044\x91\x1b\xd4\xf0\xb3Y\xa1\xe4\xa8QA\xc1\x8c\xd2(\x97\r\xacZ\x8a\x1a\xa5\xa6\x91\xb0\xfe\xe9\xc8Q\xe7\xed\x01.o,\xba~\x14\x94V\x80\xd0o9\x90\f\xcb@!\xbb[\xbd\xa5\xaaE\xed\x10\x9d\x80\x12\xe1 V\xbfc\xa1\x97\xf0\x88҂\x01\xb5\x15\x86\x95V\xeev(-q\n\xb1\xe1\xf4\xef\rle\x11\xb5\x8b2\xa21\xf0\xbb}(\xd7(9a\xb0#\xcc\xe0\x15\x10^BE\xf6 Ѯ\x02\x86w\xe0\xb9!j\t\xbf:\xf6\U00035e01\xadֵ\xba\xb9\xbe\xdeP\x1d\xcfO!\xaa\xcap\xaa\xf7\xd7\xee(Е\xd1B\xaa\xeb\x12wȮ\x15\xdd,\x88,\xb6Tc\xa1\x8d\xc4kRӅ\xdb:wghY\x95\xffѰ\xedMo\xafzo%OiI\xf9\xa6\xf3\x87\x13\xf3\t\x0eX\x81\xf7\xb2\xe4\xa7z,ZB\xdbW\x96:\x0f\xef\x1f?w匪C\xea;\xbaw\x84\xafe\x81%\x18\xe5k\x94\x9e\x89N\xda,L\xe4e-(\xd7\xeeG\xc1(\xf2C\xf2+\xb3\xaa\xa8\xb6|\xffà\xb2\x02-\x96p\xeb\x94\n\xac\x10L]\x12\x8d\xe5\x12\xee8ܒ\n\xd9-Qxv\x06XJ\xab\x85%l\x1e\v\xba\xfa\xf0p\xb0\xa7Z珨\xbcF\xf8\x15N\xffc\x8dE\xef\xc4\xd8it\x1d\x8e9\xac\x85\xec)\a;e\xd9\x03\x9a>\xb4\xf6\xf1\xa7\xdfj\xb0\xc3\u007f\x0e\xb6\xf2\u007f\xcd@+?v\x13\x86\xd3?\f:\x15\xe7O,\x0eT\xca\x00$\xc4\xfd9\xb1X\x0e\xfe\x1f\xa1\xa9}\xf0k\xc1L\x89e\xa3m\a\xb8\x1c\xec\xf8\xfd`\x823G\x84r+\xffV\xfd\xdbm\xf3\xf6_\xabN\x13;&\x12\xc1J \xe5\x1e\x1eP\xee\x90MR\xda>Tc\x95\xd8\xdc$v\xe0\xec\x1cY1\xbc\x01-\r\x8eP\x86HI\xf6#\x84\x89\xb69\x97.\xcd\xf8\xa0\x10\x18-\xb0k(q\f\x12\xfa\x86y\x06.\xb8\xf8\x95J\xac|\\\xfc\xd9Q\xb3}\xe3\x02\x8aw\x1f\u007f\xc4r\x8a<\x90'y\x03D\xde\x1dl\xb6\xbbtp\xf4s\xd1\b\xaeO\x134\xf9\x8c\xc7\x15\x10x½\xf7X\b\a\xcb\x1c\xa2\x9d\xbf\x9b\f\x9f\x86\xc4q\xa9\x17\xef\x1e\xe3ށ\t\xb9\x94\xd9ٹ\xa2\xe0\x9f'L\xf8\xfb\xa9\xa7G@\xbb\xa7\x10\xe1zJ\xda\x17\x8e\x10.\xf2\xce'\x1e\xb8\xbcX\xd4E\xf3\xc8A\xbe\"\x89O\xa4\xfd\th6l\xeb\xe4\x0f\x1dc\xdf(\xcf\"{\n\xb6\xb4\xceDԥ\x0f\x15\xba\xd3\x123c_\b\xa3e\xb3\x90\x97\xfb;>\xee\r\xf7\x9f\x8fB\xdf\xf1+\x1f\x92)'%?\nT\x1f\x85vo\xceBN\xbf\xf1\x13\x88\xe9'\xba\xe3Žڶt\xe8\xa6\xd82\x84\xdb?w>\x93Ұ\x87*\xb8\xe36p\t\xf4p\tS\xbfܴ}\xe8?\x95Q.\x87\xc6\x05_8S\xb9L\xad䉝\tR\xc8\x1eG\x86[k\x16\xf5\vf\x82\xfdl-\x89\x9f\xefS\xc0\x8c\x14X\xc6h\xd3%.\x89\xc6\r-\xa0B\xb9\x992\x1cݧ\xb6\xfa=o\v\x99Z\xd7?GJX\x9ei\x8fOP\xdd\xe5\xfcf\x16\xf6\xe4f\x8c\x8a̞\x1d:\x92\xaf\x1c\x1f:\x8f\x913\xb1\xce\xff\x98\xa5.)KW\\\"\xec\xfe\b\x8d\u007f\x04/\x86\xb6\xdfo\xcc[Ȋ\xd4\xf6\xfc\xfeÚ9'\xd0\xff\x84\x9aP\x99q\x86߹:\x11\xc3\xdeܐ\x19\xeb.cW\xa0\n,\u007fw\x84\r3\xe1\t\xe4\x84\xd5-ȼ!\x17\xeb\x81\xc7r\x05\xcf[\xa1\xbcM]Sd\xa9\x94M\xff\xa1\n.\x9fp\u007fy5\xd0\x03\x97w\xfc\xd2\x1b\xf8\xa3\xd5M\xe3-\b\xce\xf6p\xe9\xe6^\xbe\xc4\tʔĬa<\x99\xe7n\x9f\x9eXts\xddm\x92;\xb8\xb9S\xbbΒ\xc3Z(\xfdS:a7\xb2\x9f\xfb8\xa3\xef\x9b&\xf2^\xb3>{\xc8a5J\xd5zrk\x8d2$\U0007c88d\x11\xc0\vc\xa3\xb9$]\x93\xa0#Mf\xd5\x12xF*|\xcd#g\x8b\xc7x\x8d\x96.G\xfa\xdb\xef\xbfvr\x8c\xf6\x84\xda\xdf]D^۫-DU\x91\xc3*_\xd6Vo\xfd\xcc(\xd3\x01\x90\xe7\xbe\xdc\x18w.\xf3ݽ(C\xae\xbe\xf7L\xf5\x96r \xf1\xf8\xa3\f\x02E\xa0\x16\xf3\x9a\xc8?[\xa2`\x85ț\xdc\xf8\xf7`\xaf+\xca\xef\xdc\x02\xf0\xf6\xd5\xed;\xb4\xe4:\x89\x9d\x91\xd4\rC\x9b\x17\xce\xe2\xe4\xbaF\xa2\x84\xe7-J\xecI\xc50\xe1m=\xc6L\x90\\\xe8n^\xc1\u00adE\xf9F\xc1\x9aJ\xa5\xbb\x1b\xcd\x158\xa3r\xc5\xe1H\x0e[\xec>\xd3\n\x85\xd1'\xf0\xe0};\xbbW\xa0\xad\xc8WZ\x99\nH%L\x86q\xf7\x8f\xb5/\xb4j\xaa\xa8\x81\x03τꦞ\xe42,ZX.\xd5\fu.\x8bW\xb8\xb6\xea\xa8\x10\\\xd1\x12e\xac\xf2{\xceRa\x0f\xee\x9aPfR\xe5\x9b\xd4sl\x98\xca\xdfKyR\x94\xfa\xc9\xcf\xecd\r\xb7\xe2\xb9O\xa0l\x12l\xc9\x0e\x81\xae\x81j@^X\xbe\xa0\xf4*\xdb-\x11\x88\xe1H\x93-\x96y\n\xde>\xc8M\x95G\x80\x85;ٔO&ź\xc3?\x10\xca\xce\xc16+y\xa7\x1f\x8d\xdf\xda\xd9\xdf\xe4h4J%߄\xad\x10\x1e\x90\x94\xfbx>\x88\xd66Tu2 @\x1a\xdeՈg8\x19\xc7\xc4wa\x17\xaf\x19\xb8QN3\x18{\x90ϧ\xba\xeb\xedX\x10g\xf5v\xec\x02\x8d\xa1;%5s\xd7\x03`Met\x9c\xdd\xde\x1b\xa99\xc2\xf3Y\xa1\rP\xb1\xf4I/k>\x83\x1f\xed{\x97F\xca\xe0I\xec\x8ew]\xb28\u06dd\x90\x9f%\xfa\xbah\xdb\x15\x16.)(w\xb80\xfc\x89\x8bg\xbep1\xa5\x9a\xcd\xd67\x8b\x9f\xac8\xbe\xa5\xd2\xe8\x8bW\xbe\bD\xfb{\x06\xa5\x90\xcd\xe6\xa3\x02\xe3))\x98SC\xbe\x8du\xe4\xcf\xd9]L\xad?19\xd4\x1co}\xffino\xd3]zV\xc7\u007fxޢޢ\x8c\x8d\xad\v\xd7ÛR\xabmi\xb2u\x85\x9bf'+?ћ\xf2Mx\a\xedOi_\x99\x1bƮ\xac`\x13ôoE\x95&!DY=@+!\x18\x92ö\u061c\"\xfa\\\xe9\xbc\xdf\x0f֔\xaecC\x98\x88\x8b$0\xf4\xbc\xf4]\x9fݺl\xbf\x06\xee\xb2?q\xa7\u007fz\xabXFy{\xa6\xa8=\xdd@7E\xaf\xa1\xd8t)\xd6\xca`\x18\x17:+\xbf+\xf2\xcd\x14\xa2\xc7\xcb\xcf!ي\x9a\xec\xde.\xfb\xffh\x11\x8a\xd1.\xb3\x90@\xe5y\xdb\xe4\t\x9c\xe5\xe5%\xdd\xd1\xd2\x10֓\xc0\x0e\xcdZ҂\x90\xc0)Kա,\xcd\xe3\xfc\x1e\x8d\xe1S\xed\xf3\xd1G\x9f\xd5iw'\xaff}r\xa5\xba_\x89\x1e\xd1\xe0Ǧf\xf3[\xf2\xf2k\xd1\xd3\xc5\xe3c*Ї\xf5\xe5Q\xa0\xf3u\xe7\x1cOu\xa6\xc6|Be9\xb3\xab\xe8\xc5\t\xe8\x9c\xda\xf1I\x15\xe3\xd9ƛ\xcc:q\xbf\x02<\r\xf2\x88\xeap\x16q\xe6+\xc1G\xd7\u007fC\xbdu\x12\x8f\xec\xaao\xa2\x9e;\tx\xb4\xd6;Uŝ&y\xa2\u009b_\xbb\x9d\x04\xed\xea\xba\xf3\x15\xdb\xd7\xeb\xcbz\r\x17y\\\xd5\xccV]_\xe4Bg\xd4U\x8f\xa9\xa6\xceR\xec\xc4\xcaiS\x19\x1dY\xf7\xd8zi\xbf\x1e:\x024\xa7J:R\x05\x1d\x818Y\x1bͭ}\x8e\xc0\x9e1\xbb\x93R2\xf1g\xe3u\xffJ\xea\x9a\xf2͐\xf3\xb9\xf21)\x1b\x83\xd2iw͞pt\x9d\xe3^X\x91Z\xd2\u007f\x90\x98\bAb҉r-\x96\xf0\x8e\xef\ap]3t\xd2\xe5\xee\u007f\xb1b\xb7\xf5L\x19\xeb~\x95\xe1\xc0vA\x85\x0f\x9cT:\x10\xb6\x03\xc7>fJ2EȞ\xbf;\x17q|:\x18\xdeMcM\xfb\xcf)י\xea\xed\x89\xfese\x98\xa6u\xf2\x10\xd7R\xec\xa8K\x8amq\xdf\xd0\xf3wᾇX\xed\x1d\xa4O\x0f\xcd\xf9Z\x1e\x84\x02$u*\x9e\x911 j\x88~\xe1\xbf\t,\xc4\xc2}\xe6c9\x19\xe5!|;x\xe5\xce`*@\xe5\xf1k\xb5ʂq\xdf\x15\xaaD\x02`ԺL{\xb8\xde\x19w\xef\xfe0(\xf7 v\xae\n\x1a\\\x9e\x99\x86c\xaf)\x94am\x87GP\x80\xfeK\xd4\x03Ͽ\xd5\x18\xf0\x8e{\x1b\x9c\x04{\xb0G\a\xc7*\xad6ڱ\xfa\xd9\x062#C\x93P\xb9hf\xa7\xe5a\xd2\xd4\xe4v\xeb\x9e7\xf69>\xfa\x99\xf5;\xce\x12\x01\x9d\x1e\x03M\x80\xcc\xed\xbe\xcd\xcb\xd8\xcfv۞+\x16\x9a\x8b\x86\xb2\xdd\xc0\xbcn\xdast\xd1\x1e\xd1={DTt\\\\\x94M\xa6\x9c.ٳDGg\x8c\x8f\xce\x11!\x9d\x16#̀<\xe8~\xcd\xe9k\xcd*2e\x97(r\x8aJ\xf3u\xcd\xe9~Ռ>Ռ\xe2\xc7\xdcN3\xfaQ\x8f\xebC͠ᙢ\xa73\xc5O爠\xce\x1bC\xcdFQ\xb3\x923\xf9\xf7\xc99\xf2XM\xfd(J\xbc\x17R\xcf9\xfc\xf7\x87\xe3\x13\x15\xacN\x10$X\t<\x0eM \xe5|\xf9\xe0ǟ\x86T\xba\xd8\x14ֿ\xff2\x87\xcfC3p\x1a\x11\xeb\x92\xc6\xf8,\x81\x87\x9d\xefpQ\x9c\xd4j\x9b\b\xef^\x8ẹ&\xdad\xe2\xe3\xc7\xf6P\xa2ŶS\xb6y\xc6X=\x94\x9d\xab\x87\x0e\xf6d\x1d\x1e\x0fȝ'g\v9eW\x1d\x0f\xfb\xdbT#2\xbfl>\xf9\x9bfO\x9e\x91s\xe9BF\x1b\xd5EYh\xe9rB\x1dbV\x17g|~8m@2?\x8a=\xf9s\xd8yb%\b5\xf6%l\xce\u05ee\u007f*='Ԯ*\xb6X\x1a\x86\x19\x97\xd4\xcdB\xa6o`\x92r>U\xe9\xfau\x83?\xeb\x9al\x1d{\x19\v]\xb6\x15*E6\xf1Z\xa1g\x94\b\x1b\xe4\x96\xc4Sw̴\x8d\xca\xe1\x047\xfd\x12\x96Z\xa4І\x84\x05\xbc\xa5l\x92\xb8\xa9\x04\xa0\xbf\xb9\xcc\x0e!\x9b\xd1sC\xb9\xc6\xcd }\x1a\x9a\xa4\x1f\x90\xa8Û\xee\x06\x84\xf8\xd0\x1d\x1b\x82_O\x03\xffU5q<\xf5\x17\xa3i*q\xcaC\x10n壮˪\xb7Dͩ\xcb{;\xa6\xf9z\xa0s(\x1bM\xf90\xb2\xa7t7\xf3\x02>\xe2s\xe2\xed\a'\xf4.\xa1\x91>J\v\xb8\xe3\xf7Rll\x8c\x91\xf8\xf37B5\xe5\x9b\x0fB\xde3\xb3\xa1\xfcS<\x93\xc7\r\xbe'RS\xc2\xd8\xde\xef'1\xf76\x1e\xe6\xc4\u007f\xf3\xb3G\xfe\x98bR\xc0y6\x1a\xf0\xc3\xda\xe0\x88r\u007f\xd0]\xef\xfeJ\x18\xdd=\x15oT{`ҙY\am\t\x1f\x85Ƙt\xa3}\xa0T\xc1\n\x95^\xe0z-\xa4\xf6\xc1\xd8b\x01t\x1d\x14u*\xc8 \x949_\xc3\xdf\xd3g\x1d\x90\xa6\xf0\xdbX>!\x81\xf0=Hw*\x9c\x93R\x91\xbdo\xad#Ea\xac\x1e\xb8V\x9a\xa4\fڋ\\[\xe7\xdc\x04i\x1e\xc9K\xf4=\xb5\xee\xf8\xe6+@S\xadP\xba\xdea\xfb\xb7'\x9d\xfb\xac\xc0\xab\xa0d\xc1\x01\xdc\xc7\a\x9d\xaf\x9a@\xd9㜎\x8f\xa7\x94\x8f\xfb_h\xc2\xee\xc6\x1d\xb5~#m38\"\xe0\xa6\x0f\xd1\xe8]H6\xde&DU\x9cjyVl\t\xdfX\xf1\x91\xc2l\xb6Q\x04\xc74\xf5X>ĸ\xfb\xd0jwRUL^k#y'\xf7\x12\xd2\xd9e\xbb\xdd)\xa0\xd3$\x9c\xf23{\xa6u\xce\xd3\xec\r~\x99\v\xe1\x16\xb6\x0e\xc4\xf7k\xfaw\x8d\xee~\x9f\xe3\x04|9\x18~\xd0 i݁\x16b0\xdc\t\xe2\xfc']\xc7\xfblW\f\xffk0\xe2\x1b7:>\x13\xc9)\xdf\xcc!\xff[\x18\x96\xf0\x81\x02\x84\x84\x17\x94@\xa2\xf1\x8b\x8e\xf2\x82\xe2&G\xaell<\xa3\x17\xf8A\xc934x\xe9\x04\xb9\xec\x109\xac\x14\u07b4\xf1\x03)\n\xacuh@\xee\xde\xd6|y\xe9~\xc4\xeb\x98\xdd\xcfBp\xaf\x16\xd4\r\xfc\xff\xdf.\"B_\xe2\xad\xcb\xf6\xe5\xbf\x02\x00\x00\xff\xff鐱=\xdaZ\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c7l\xef\xfa\x15\x18\xf5\xc1\xed\x8c\xee\x14O\x1f\xdaћ++SM\\\xdbc)zi\xfb\xc0\xdb\xc5\xe9\x18\xed\x92\x1b\x92{\xf25\x93\xff\xde\x01\xc8\xfd\xbc\xfd\xe0\x9e\xa5i\x9a\x11_\x12\xed\x91 \b\x80\xf8 A\xf8l\xb5Z\x9d\x89B>\xa0\xb1R\xab+\x10\x85\xc4\xef\x0e\x15\xfde\xd7O\xffj\xd7R_\xeeߟ=I\x95^\xc1ui\x9dο\xa1եI\xf0#n\xa5\x92Nju\x96\xa3\x13\xa9p\xe2\xea\f@(\xa5\x9d\xa0ϖ\xfe\x04H\xb4rFg\x19\x9a\xd5#\xaa\xf5S\xb9\xc1M)\xb3\x14\r\x03\xaf\xa6\xde\xff\xb4\xfe\x97\xf5Og\x00\x89A\x1e~/s\xb4N\xe4\xc5\x15\xa82\xcb\xce\x00\x94\xc8\xf1\nl\xb2ô\xccЮ\xf7\x98\xa1\xd1k\xa9\xcfl\x81\t\xcd\xf6htY\\A\xf3\x83\x1f\x140\xf1\xab\xb8\v\xe3\xf9S&\xad\xfb\xa5\xf3\xf9\x93\xb4\x8e\u007f*\xb2҈\xac5\x1f\u007f\xb5R=\x96\x990\xcd\xf73\x00\x9b\xe8\x02\xaf\xe03MU\x88\x04\xd33\x80\xb00\x9ez\x05\"M\x99T\"\xfbj\xa4rh\xaeuV\xe6\x15\x89V\x90\xa2M\x8c,\x1c\x93\xe2\xce\tWZ\xd0[p;l\xcfC\xed7\xab\xd5W\xe1vW\xb0\xb6\xdco]섭~\xf5$\xf2\x00\xc2'w ܬ3R=\x0e\xcd\xf6\x01\xae\x8dV\x80\xdf\v\x83\x96P\x86\x949\xab\x1e\xe1y\x87\n\x9c\x06S*F\xe5\xdfD\xf2T\x16\x03\x88\x14\x98\xac{x\x06L\xba\x1f\xe7p\xb9\xdf!d\xc2:p2G\x10aBx\x16\x96q\xd8j\x03n'\xedh\x1ao`%2؋\xac\xc4\v\x10*\x85\\\x1c\xc0 \xcd\x02\xa5j\xc1\xe3.v\r\xff\xa1\r\x82T[}\x05;\xe7\n{uy\xf9(]\xa5b\x13\x9d祒\xeep\xc9\xdaRnJ\xa7\x8d\xbdLq\x8f٥\x95\x8f+a\x92\x9dt\x98\xb8\xd2\xe0\xa5(\xe4\x8aQW\xacf\xd7y\xfa\x0f\x15G\xed\xbb\x0e\xaeG\xfb\xcd7V\x84\x13\x1c \x8d\xe8\x05\xc6\x0f\xf5\xabh\bM\x9f\x88:\xdfn\xee\xee\xdb\xc2$m\x9f\xfaL\xf7\x96\x845, \x82I\xb5Ű\xa3\xb7F\xe7\f\x13UZh\xa9\x1c\xff\x91d\x12U\x9f\xfc\xb6\xdc\xe4\xd2\x11\xdf\u007f/\xd1:\xe2\xd5\x1a\xae\xd9\xee\x90\x1c\x96\x05\xed\xc0t\r\xb7\n\xaeE\x8eٵ\xb0\xf8\xea\f J\xdb\x15\x116\x8e\x05m\x93\xd9\xef\xec\xa9\xd6\xfa\xa12o#\xfc\xaa\xf6\xf8]\x81Ig\xcb\xd08\xb9\x95\to\f֞\xb5\n\xe8iP߆w-\xff\xc2j\xaa\xff\xb5\x87\x87\xd7eլh\xc9~\xb8\x1ds\xb81c$W\x1e\x1a\xe9\x14\xa5\xfb\xdc\x1d҂-J\x04(3\x98t\xb5^\xac};\x82\tAխGp<\xe2*\xff\x84yAjc\x06\xc5\xfbЍP$\xfa\xa4\xb5;U\x19\xfeJ\xcd\xea\xa0]\xe1H\xb9\xf1t;$\xbe\xede\x1a\xb4\xd7\x11Wa\x92\xb3\xd4\x12+\xef\x94(\xecN;\xb2q\xbatC\xbdz\v\xb8\xbe\xbb\xed\rjq\x9e\xb0b\x1bΌv\x1a\x9e\x85<\xe6\xb4o$\x97\xd7w\xb7\xf0@.\x11V0\xc1[rp\xa5Q\xac\x8e\xbf\xa1H\x0f\xf7\xfaW\x8b\x90\x96\xac\x95*\xbb|1\x02x\x83[\xda\xf4\x06\t\x06\r@ch\x0fXFM\x97n\xcd\x0eG\x8a[Qf.(9i\xe1\xfdO\x90KU:<\xe6;L\xf3\xde\x13\x89\xc1\xf9\xd5\xd8{\xfd\xb3\xf5\x8c\x8c \xe9Ǒ\xa1\x03[\xaa\xd0)\xec\xb9\xdf\x18Ue\x86`\x0f\xd6a\x0e\x9b\x00\xa5\xb6\xd5\xcc\x15\xd6\aY\x16\xc0X\xd8\x1c*܇\xd7M^\xb8\xd8dx\x05Δ\xc3\xd3Nm\xdd!\xda|C\xebd\x12A\x99\xf3>i\xfc\xc8\x01\xc2\x18\xfea\x84(=\n\x90\x91\x17O\xe4h\x06\n\x91\xb7\x90e-\xe2\xceS\x05\xe0\xbf\x14|$\x03\x97\x90ٹ\n\xe6Lb\xc6&TiȴzD\xe3g$W\xe1Yf\x19oi\xcc\xf5\xbe\xe3d\xb5\x1b\xd9\x16\x83\x19\x19Iؖdv\xd6@\xb2?*#RY\x87\"]\x9f\xbf\x16\xf3\xf0{\x92\x95)\xa6u\x983\xa8Kz\x8c\xbb9\x1a\xc4\x01\xa1\x90\x8a43\x85_DtU\xff:B\r\xf65\x85A2\x18 \x95\x87I\xa4!E\xb3\x19Q\xd2Ԥ\xc3|\x04\xcfٝ\xbc\x80j\xc2\x18q\x98\xa0Y\x154/!Y=&\xb8b\x99L\x90\x88U;\\L5&\xcd\xc8\xfa\xfe\x1f\x12l\xa7\xf5S\f\x91\xfe\x9d\xfa5\x8e%$|6\x01\x1b܉\xbd\xd4\xc6\xf6\xa3\x13\xfc\x8eI\xe9Fw\x9bp\x90\xca\xed\x16\r\xc1›\x8e\xbf\xa7\x885mV\xa9\x99i\xc6\x1f\xad\xaba:1\x8f\xa91\xb6\x14v_F\xa1\x02#NV\x8fuC*\xf72-E\xc6jB\xa8įO\xd4\xf8\x8d)\xb7\x19\x818\xc2\xdf+\xa3j\x15ĥ\x8eW\xaa\x15\x92ۗk3f\xb7|;\x063N\x86\x8d`gr̅k\x9a)3\xb4\x01\x15o\xfe\x1a\xbds\xd1p\xca\at\x99\xd8`\x06\x163L\x9c6\xe3\xe4\x89\x11\x02\xdfb\xf5\xe7\be\a4i\xd7ߚU\xa2M#\x87l'\x93\x9d7V$e\f\vR\x8d\x965\x86(\x8a\xec0\xb5h\x88\x91\x8c0ٜ\xd2hZ\x84\xfa\xe8\xc3\x1dS$M\x8b\xd4\xc1M\x9b\xd1\xc6]\xaa\xd7b\xf3F\xf4\x0e\x9aꇄ\xfd\xf6h\xf8\xcb\v;\x91[\xa2]\xc3\xed\x160/\xdc\xe1\x02\xa4\xab\xbe\xc6@%W\xb1\xc1\xe3oƸ\xd3v\xcbm\u007f\xf4\x8b\xef\x96\x17\xe1Z\x8d\xc6߄il\xac\ue0adZİO\xed\x91\x17 \xb75\xc3\xd2\v\x8a!\x1d\xb2/5\x87h\xcbљ\xe5\xdcK\x12(\xd6\xf6R˅Kv7\xf51PĈ\x1e\xad\xfa\x00\xbc_^\xc50̃\b\x90P;\x15|\x82)\r\xe6\xfed\xf4\x9e\xf7G\xf3\x85=\xc0\x0f\x9f?b:G2\x88\x97ԣE}\xe8y:m\x14x\x81Q [\x8bb7\xad\x8e\xf1\xfc\xf9\xf7\x05\bx\u0083\xf7\xac\x06\x83ˡF\xac\x155H\x83|\x18\xcfj\xe4\t\x0f\f*\x9c\xaeG\xc1[\"*\xbe=\xe1!\xb6k\x8f\xa8\x84_8\xd7\xf3ԥ\x0f\xbc\x8a\x98\xadԴ\x9a\xa8a\xef\x80\xd3q\x8b\x85eJ\xa9j\x15\xc5O\\vͰΕ\xd2\x13\x1e\xdeY\xcf>\xda5;Y,\xa0\x00)l\xb0\xc8;\xac\xbaKy\x10\x99L\xeb\xc9x\x9f,\x80x\xab.\xe0\xb3v\xf4\x9f\x9b\xef\xd2\x12\x8a*\x85\x8f\x1a\xedg\xed\xf8˫\x92\xd8/\xe2D\x02\xfb\xc1\xbc-\x957\vD\x97E\xf378\xb0\t%\x11\xad\xd9&-\xdc*\x8a\xcf<}\x96\xb0i\x87\x15r\x1e\xad\xbc\xb4|\x1b\xa3\xb4Z\xb1\x99\xaef[\x00\xb4\x8dW`\x956\x1dN],\x848\x88b@\uf7ac\x95\xff\xe5\xe8\x1ek\xaa\x19,2\x91`Z\x9dJ\xf3\xa5\x99p\xf8(\x13\xc8\xd1<\"\x14d7\xe2\x85j\x81&\xf7\xed\x04)\x8cw-\xaa\x16\xcc\xc2\xc0\x1d\xd0P[Ѯ\x8f\xecY\xb19\xaa\xfb\xc8\r\xd9t\xf7\xb8U\xb2yg\u007f(\x8a\xfa픎e\x96e!\xbf\x8e}\x10\x8f\xa4w?r\xc1\xc7\xd6\u007f\x90ye\xf1\xfe3\xce\x1a\ni\xec\x1a>pBK\x86\xed\xf1\xd5)ak\xaa(\x90\x84\x89\xb4@r\xb2\x17\x19\xb9\x0f\xa4\xbc\x15`\xe6\x9d\t\xbd=\xf2\xa0\xe2T\xcc\xf3N[o\xf3\xebc\xf5\xf3'<\x9c_\x1ci\xaf\xf3[u\x1e\a\x93t\xfe\x91Ҫ\xbd\x16\xad\xb2\x03\x9c\xf3o\xe7\xec\x98-\xd9\"'8o\v\xa4:\xba+\xa7\x8e,\t\x05(֮\xbc\x16\x1a\\'X\x90\v?\xb7\x8ah\x99.\xb4\x1d\xb9]\x1cA뫶\xce\x1f\x00v\xdc\xed\x81\x13\u0098\xe8/\x9c\x1a\x82\xd8:4`\x9d6U2\x03\xa9\xdd\xde\x019q\xde\xce\xf3\x9eX]\x9fFz\xc0\x14d\x9e7\x1a\xc2\xeb\xf4s\x9f\xe5@\xff?\x0f3ag\x89a\x17F'h\xed\xbc(EZ\x8e\x99\x03\xdb\xfa\xb0V\xf8\xe0m\x1b\xa5\x9ac\x8e\x92\xab\xb6\xcc\x15'Ҟ\x10\xd8\xdc|o\x9d;\x93\x1a\xa2\xbfcD\xf9\x14\x1c\x81\x13\x1d\xf3\\\xf4\x13k\xa2ѽ\xf6\xa3\xab\r\x18\x80\xf9\x80\xc9<\x96\xacT\x96\xf9\xcdA$\xffj\x8eG.\xd5-O\x04\xef_\xcdY\x81J\x95㩡\xccu5\xbeaH\xfd!6~\x85*=C\xf3]\x8d\xc1\x0eg\x8fo2\xe29\x05\xe4L+\xedڇ5a\xa6w\x16\xb6\xd2X\xd7 \xbc\x00\xaa\xb4|M\xfd\xba1\xa6\xba1\xe6\xe4\x10\xf3\x8b\x1f\xdd:V\xdc\xe9\xe7\x90Դ$\xb0\xae\x88\xbf\x13{\x04\xb9\x05\xe9\x00U\xa2K\xc5\a^\xa4.h\x9a\x05\x10=\x13\xbd1\x89\xb4\x99\xad\xc1\xaa\xcc\xe3\t\xb2b\xe9\x94j\xf6t\xac=\xe4g!\xe3N\xa7\xe04\xb6\xba\xa9ġ\xa1\xd6͆\n\x19D\xed\xec\xb5\\|\x97y\x99\x83ȉ-K\xe2ƭ\xcf=\xaaR\xdd<\xaf\x9f\x85t!\x83\xd8_\xac.Ӧ\x89\u038b\f\x1dVYE\x89VV\xa6X\xbb\x0f\x81\xff\x839ZcM\xc0VȬ4\vt\xf4b\xce,\x8dۂzz\xf9`,\x1e\x91\x15\x133\xf2\xd0}\x81\xd3\x1f\xa8\xc5\xe7\xa5-\xcdF\xab\xf3\x87\xe6\xad\xca\x0f\xe4\xa0-{ 0\x9bo\x16\x834\xc4d\x99\r\xe7\x8f\xcd@]\x92[\x16\x1b\x83G\xe4\x91\xc5g\x8fő\a\xf8\xb5}l\xceX\xb4\xd7\x16\x9b\x1f\xf6:Ya\x91\xb9`\xad\f\xafY\x90'f\x80E\x13,.\xdb+:ǫ\x95\xb95O\xad\x89̮\xe1|\xadY\x90C\xf9\\1YZQ\xb8F\xe7f\xd5\x19W\xf3'\x89?\x94\x91\xf5\xf2\xb9\xdf/\xe9\xe7O\xe7WEeUE\xc5\x02\xf38G\xe5M-͖\x8a\xa2\xea\xd2̨:\xebib\xe2\xa8|\xa8\xe3\\\xa7\xa9\xa5\xccfA\x8dg8M\x81\x1d\xca}\x8a\xc8k\x9a\x00\xd9\xcexZ\xec\x06\xccJ\xd3L\x87\xe1\x8a\x18U\x9b\xb7\xb5\xd9\xff\x85\x04\xfe袵\xe9\xb8\xc01Qɗ\xde\x10\xe2}\xe5\xf5\r\xb9\xd5\xe31\x9ew\xb6Op\xabG@\xden!/3'\x8b\xacU\x92\xc2\xed\xf0P?y\xffM\xf3\xd3\xcb́\xa1}\xf9V\v\xf0\x18\xc8n\x80 ,\xe8\a]?\xfc\xcc%\xfa\x1a\u007f\xd1]R\xec\x15|\xdc5\xcf\xfc\xf3\x95\xc8g+\x91\x97@1\xd8G>OY\xfe,%\x92\xce'\x06[\x93SG>?Y\x14n\x9d\x18pMB\x9czn2\x1drM\x1f\xa7\xf5\x9f\x99\x9c\xe0NDH\xd8l\x97\x1f\xbe\x11\xd0&E3{\xb9\xb2D4g\x85\xb2\x17\x13u\xe7\xefU\x1d\xa8\xaa{Q\xaf\xf6\xc5\xcd\x18wt\xfd\n>\x81_\xa4J=oH\b[\xfeE\xe7\xf6\xa7q~\xc6\xee\x80\xda\x1eg(\x8b鯍,\x16\x82\x14)\x87E|\xbdm\xd7p#\x92]=\xc3\bH\x9ew',l\xb5Ʌ\x83\xf3\xfa>\xee\xd2O@\u007f\x9f\xaf\x01~\xd6\xf5Eh\xab\xca\xcd\bT+\xf3\";P\xf4\x03\xe7m0?&8\xa3\xc2gC\xb9\xbfP\xcf,\"\xfe\xbd\xeb\x8e\x18*7\x19ʺU\xb0'\xd8,\xd4\x01\xbe>\xb0\x17\xc5ş\x92\xa6HV\xf0\x91\xaa\b\xb8WCk\x04\xe4X\xbd\xc8E\xc4\x1a\xbf\x14\xb6N\x1b\U000487f4/\xe9\x19C\xad\xee\x88NUנ\xa3\xaa\x14\x91\xf0\xe6kdeU1\xe6>\xc0&s\xec\xa8\xca a;\xa6\xbcf\xf6\xb7sY\xc4\xe2\xee\xef?\xf9\x059\x99\xe3\xfac\xe9/\xe8W\x850\x16\x89\xd2\xd5B\xfd\xa0\u0378}\xdb\xe9g.\xd3\u05ee\xbb٪l\x8c\x9c\xa9ƹ\x00'\xadfߩlY\x91.F\xd8\x1f\x86G\xb6\xd4I\x8b\x89SW\xfaz;\nKX\xab\x13\xc9\x1a\x88\x8f\x828A\xec\xf5J\xc4MY\x92\teQZ\xfc\xf2\xac\xd0|\xab6\xaa\xbdUc\x855;$\xfc\xf5h\xe0hQM\xa7Y\xef\xf5\xba\x0f\xd9;\x15\bd}\x11\xd2\xeaLKں\xf4\xec1\xe9f\xf6\xff\xf8\xde\x1f\xf6YW\xc3\xd5^Wu\x01ڳ\b\xca\xfa\"\xab1\xb5\x85}5\xd6D\x14\xae4\xc1\xac&\xa5\xe1zy\x04\x04}9\xb9Ӫ\v7\xd5\xdagx\xd9\xd4oo\xa2\xfc\xd9j\xf1\x03\xfc\xab\xeb\x03\x8f\x16\xcc\xf56\xd5Ws_\x11\xfc\xd3\xd89\xb8\x0f\xb8\xbe\xe0\\-e\xeaS'\xf7\x06B\xf3\xc0\xaa.\xe1\xdd\x18\xea\xc3ٚ+\xf8\x8c\xcf\x03_o\x14-\xe2\xf8.ͧdb\xcag\x03C\x95\xd5'\x97\xb8\xafGq>쀶誹^\xf7^\xa2\rW\xa9\xad\xbb\xf8\xdc\xd7!\xb6\xfe\xa3\xdc\xfa\x83\x9b\x84\xd6\xf4OG=F\x15פ\xd2\x1aSX\x83[\xea\xe8\xa3E\xb3粰\x95\x90\x04\x1b\xde\xfeRn\x9a2\x91\xf0ǟgͮ\x14I\x82\x85\v\t]\xed\u007f\xc5\xe2ܗy\xad\xfe\x91\n\xfe3\xd1\xca;\xd8\xf6\n\xfe\xf3\xbf\xcf \x18\xe0\x87\xea_\xa2\xa0\x8f\xff\x1b\x00\x00\xff\xff\t\xb7x\x1e\xf3c\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c\xabr\xef\xfa\x15]\x9b\a'\xb7\xb4\xeb\xe3\xcaCRzsd\x9f\x8a\xea:\xb6\xcb\xd2\xd5K\x92\av\xa6W\xcb\xd1\f\xcc\x05f\xe5ͭ\xf3\xdfS40_;\xcc0k\xa9\xceG\x89\x17[,4Mw\xd3\x1f\xd00\x17\xeb\xf5\xfa\x82U\xfc\x1e\x95\xe6R\\\x01\xab8~7(\xec_z\xf3\xf8\xefz\xc3\xe5\xdbû\x8bG.\xf2+\xb8\xae\xb5\x91\xe57ԲV\x19~\xc0\x1d\x17\xdcp).J4,g\x86]]\x000!\xa4a\xb6Z\xdb?\x012)\x8c\x92E\x81j\xfd\x80b\xf3Xoq[\xf3\"GE\xc0\xc3Ї\x9f6\xff\xb6\xf9\xe9\x02 SH\xdd\xefx\x89ڰ\xb2\xba\x02Q\x17\xc5\x05\x80`%^\x81\xce\xf6\x98\xd7\x05\xea\xcd\x01\vTr\xc3兮0\xb3\xa3=(YWW\xd0\xfe\xe0:yL\xdc,n}\u007f\xaa*\xb86\u007f\xedU\u007f\xe2\xda\xd0OUQ+VtƣZ\xcd\xc5C]0\xd5\xd6_\x00\xe8LVx\x05\x9f\xedP\x15\xcb0\xbf\x00\xf0\x13\xa3\xa1\xd7\xc0\xf2\x9cHŊ\xaf\x8a\v\x83\xeaZ\x16u\x19H\xb4\x86\x1cu\xa6xe\x88\x14\xb7\x86\x99Z\x83܁\xd9cw\x1c[~\xd1R|ef\u007f\x05\x1bM\xed6՞\xe9\xf0\xab#\x91\x03\xe0\xab\xcc\xd1⦍\xe2\xe2al\xb4\xf7p\xad\xa4\x00\xfc^)\xd4\x16eȉ\xb3\xe2\x01\x9e\xf6(\xc0HP\xb5 T\xfe\x83e\x8fu5\x82H\x85\xd9f\x80\xa7Ǥ_9\x87\xcb\xdd\x1e\xa1`ڀ\xe1%\x02\xf3\x03\xc2\x13ӄ\xc3N*0{\xae\xe7ib\x81\xf4\xb0u\xe8|\x1aV;\x84rfУ\xd3\x01\x15\xa4zs\"\x91=\x98\xef\x1f0\x01\x18\x91\xa8b\xb5&\xe1h{\u007f\xedV9\x00[)\vd\xe2\xa2mtx\xe7d/\xdbcɮ|cY\xa1x\xff\xf5\xe6\xfe_o{\xd50\x90%O)\xe0\x1a\x18\xdc\xd3\xc2\x00\xe5\x970\x98=3\xa0\xd0r\x1e\x85\xb1-*\x85\xeb@ݼ\x01\t \x15T\xa8\xb8\xccy\x16\xb8B\x9d\xf5^\xd6E\x0e[\xb4\f\xda4\x1d*%+T\x86\x87\xa5\xe7JG\xd5tj\a\x18\xbf\xb1\x93r\xad\x9c$\xa2&\xe1\xf3\v\nsO\a\xb7>\xb8n\xf1'&\xf5\x00\x83m\xc4\x04\xc8\xed/\x98\x99\rܢ\xb2`\x02֙\x14\aT\x96\x02\x99|\x10\xfc\xff\x1a\xd8\xdaJ\xbd!a4\xe8\xf5A[h\x01\vV\xc0\x81\x155^\x02\x139\x94\xec\b\n\xed(P\x8b\x0ep\x13Tl&˲\x16\xdc\x1cߒ\xb6\xe4\xdb\xdaH\xa5\xdf\xe6x\xc0\xe2\xad\xe6\x0fk\xa6\xb2=7\x98\x99Z\xe1[V\xf15\xa1.H\xcdn\xca\xfc\x9f\x02G\xf5\x9b\x1e\xae'\xeb\xcd\x15R\x84\x13\x1c\xb0\x1a\xd1\t\x8c\xeb\xeaf\xd1\x12\xdaVY\xea|\xfbx{\xd7\x15&\xae\x87\xd4'\xbaw$\xace\x81%\x18\x17;\xf4+z\xa7dI0Q\xe4\x95\xe4\xc2\xd0\x1fY\xc1Q\fɯ\xebmɍ\xe5\xfb\xdfk\xd4\xc6\xf2j\x03\xd7dw\xac\x1c֕]\x81\xf9\x06n\x04\\\xb3\x12\x8bk\xa6\xf1\xc5\x19`)\xadז\xb0i,\xe8\x9a\xccacG\xb5\xce\x0f\xc1\xbcE\xf8\x15\xd6\xf8m\x85Yo\xc9\xd8~|\xc73Z\x18\xa4=\x1b\x150Р\xae\x8c\xafZ\xfa\x85\xd4\u0530v\x80\x87\xd3eaT\xd4\xd6~\x98=q\xb85cV\xae\x1c4\xabS\x84\x1crwL\vv(\xe1\xa1\xcc`\xd2\xd7z\xa9\xf6\xed\x04&xU\xb7\x89\xe0x\xc2U\xfa\t\xcbʪ\x8d\x19\x14\xef|3\x8b\xa2\xa5O\u07b8S\xc1\xf0\a5+\xbdv\x85\x13\xe5F\xc3\xed\xd1\xf2\xed\xc0s\xaf\xbdN\xb8\n\x93\x9c\xb5%\xd3\xfcV\xb0J辰6N\xd6f\xac\xd5`\x02\u05f77\x83N\x1d\xce[\xacȆ\x13\xa3\x8d\x84'\xc6O9튕\xcb\xeb\xdb\x1b\xb8\xb7.\x11\x06\x98\xe0,9\x98Z\tR\xc7ߐ\xe5\xc7;\xf97\x8d\x90פ\x95\x82]\xbe\x8c\x00\xde\xe2\xce.z\x85\x16\x86\xed\x80J\xd95\xa0\t5Y\x9b\r9\x1c9\xeeX]\x18\xaf丆w?A\xc9Em\xf0\x94\xef0\xcd{G$\x02\xe7f\xa3\xef\xe4\xcf\xda12\x81\xa4\x1f\"]G\x96T%s8P\xbb\x18Uy\x81\xa0\x8f\xda`\t[\x0f\xa5\xb1\xd5\xc4\x15\xd2\aE\xe1\xc1h\xd8\x1e\x03\xee\xe3\xf3\xb6^8\xdb\x16x\x05F\xd5\xe3\xc3N-\xdd1\xda|Cmx\x96@\x99Ր4\xae\xe7\ba\x14\xfd\x10!ʀ\x02\xd6ȳG\xebhz\nYo\xa1(:ĝ\xa7\n\xc0\xff\b\xf8`\r\\f\xcdΕ7g\x1c\v2\xa1BB!\xc5\x03*7\xa2u\x15\x9exQВ\xc6R\x1ezNV\xb7Xۢ\xb0\xb0F\x12v\xb55;\x1b\xb0\xb2\x1f\x95\x11.\xb4A\x96oV/\xc5<\xfc\x9e\x15u\x8e\xf9uQk\x83\xea\xd6\x06=!\x18\x1cU+\x03\x1e~\x9c\xea\xefݍ\x82gh\xb9\x90\xb9616ڮ\x1d\xc7\xe3X\xa1\x8b\xf3,G=\x9a\xadG\xe1x\xbb\x81\x9b\x1dh\x8c)!#a\xf5\x97\xd5%\xb1\xdf\x0f>6\x8c\x06\xa6\xb0\xa1ĸDp\x83e\x84\x1e\xb3\xcac\x01\xa3\x98R\xec8\xc1\xa66\x1a=\x87I#\xbd\a,\x12\xa1\xc5oäf\xf8?\v\x9b\xce⎦\xed\x15ƅeM\xc1\xb5\xe9qf\xe8\x8e7\x98\xd9\xc8\xcdRȺ\xcc\\8\x98V\xd1t8\xf1{\xa6\xd99\x02\x1d\x13\xe3Fn\xbcl\xeeYL\xfa\xfe\x80\x04\xdbK\xf9\x98B\xa4\xff\xb4\xed\xda0\r2\xda\xe9\x83-\xeeفK\xa5\x87\xb1>~Ǭ6Q\xdb\xc5\f\xe4|\xb7Cea\xd1\xf6T\xb3\x9b5E\xaci'\xd5\x165\xcd\xf8\x93y\xb5L\xb7\xcc#jĦB\xc1@\x14*\x10\xe2և$K\x9b\xf3\x03\xcfkV\x90\xd1e\"s\xf3c\r~1WaF N\xf0w\xa6=\xcc\xc2r\xa9\x17\xe3I\x816\x88*\xa5\x8a)aWN\xc1\xc4ɰe\x14\x9a\xc5\x02\xa2\xb6\xa8\xba@\xedQq\xced\xabw.[N\xb9푂m\xb1\x00\x8d\x05fF\xaa8yR\x84\xc0\x95T\xfd\x19\xa1\xec\x88&\xedG/\xb3J\xb4-6\xbc\xd9\xf3l\xef\\?+e\x04\vr\x89\x9a4\x06\xab\xaa\xe285iH\x91\f?\u061c\xd2hK\x82\xfa\x18\u008d)\x92\xb6$\xea\xe0\xb6\xcch\xe3>\xd5\x1b\xb1y%z\x0fM\xf1C\xc2~s\xd2\xfd\xf9\x85ݒ\x9b\xa3&\x0f\x0e\xcb\xca\x1c/\x81\x9bP\x9b\x02\xb5\xe7\xd4\xe9?\x19\xe3\xce[-7\xc3\xdeϾZ\x9e\x85k\r\x1a\u007f\x12\xa6\x91\xb1\xba\xf5\xb6j\x11\xc3>u{^\x02\xdf5\f\xcb/a\xc7\v\x83\xe4K\xcd!\xdaqtf9\xf7\x9c\x04J\xb5\xbd\xb6\x94\xccd\xfb\x8fͦjB\x8f\x01\xad\x86\x00\x9c_\x1eb\x18\xe2A\x02Hh\x9c\n:\x0f\xe0\nKw\xcepG룭!\x0f\xf0\xfd\xe7\x0f\xb1p\xb0_\x12%\xf5dR\xef\a\x9eN\x17\x05\x9a`\x12\xc8Τ\xc8Mkb\xbbj\xf6\xbcZ@\x01\xab\xb0A#\xad\xb0p2y\xcf\n\x9e7\x83\xd1:Y\x00\xf1F\\\xc2gi\xec?\x1f\xbfsmQ\x149|\x90\xa8?KC5/Jb7\x893\t\xec:Ӳ\x14\xce,X\xba,\x1a\xbfŁL\xa8\x15цm\\Í\xb0\xf1\x99\xa3\xcf\x126\xed1 \xe7\xd0*kMg\x9bB\x8a5\x99\xe90\xda\x02\xa0]\xbc<\xab\xa4\xeaq\xear!\xc4Q\x14=zw\xd6Z\xb9_NN\x85\xa7\x8aª`\x19\xe6ጇ\x8e\xa0\x99\xc1\a\x9eA\x89\xea\x01\xa1\xb2v#]\xa8\x16hrWΐ\xc2t\xd7\"\x14o\x16FNT\xc7\xcaڮ\xfaĖ\x81\xcdI\xcd#\xe7\xcd\xd3\xcd\xd3fI\xe6\x9d\xfc\xa1$\xeaw\x13\xa4\x96Y\x96\x85\xfc:\xf5A\x1c\x92\xce\xfd(\x19\x1d\x02\xfdÚW\x12\xef_Ӭ!\xe3Jo\xe0=\xa5\x87\x15\xd8\xed\x1fv\t;C%\x81\xb4\x98p\rVN\x0e\xac\xb0\xee\x83U\xde\x02\xb0p΄ܝxPi*\xe6i/\xb5\xb3\xf9\xcd!\xd5\xea\x11\x8f\xab\xcb\x13\xed\xb5\xba\x11\xab4\x98V\xe7\x9f(\xad\xc6k\x91\xa28\u008a~[\x91c\xb6d\x89\x9c\xe1\xbc-\x90\xea䦔\x88\xb5$\x14\xb0\xb1v\xf0Zl\xe7&]ɺ\xf0s\xb3H\x96\xe9J\xea\xc8Y}\x04\xad\xafR\x1b\xb7\x01\xd8s\xb7Gv\bS\xa2?\xbfk\blG\xe7eF\xaa\x90\x1ad\xd5\xee`\x83\xdcr^\xcf\xf3ޝ\xdc\xf8\xddH\a\xd8\x06\x99\xabVC8\x9d\xber\x87G\xf6\xff\xf303r\x96\bv\xa5d\x86ZϋR\xa2\xe5\x98ٰm6k\x99\v\xdevI\xaa9e+9\x94e\xae\xb8%\xed\x19\x81\xcd\xc7\xef\x9d}g\xab\x86\xec\xdf)\xa2|\x0e\x8e@i\xc3eɆij\xc9\xe8^\xbb\xdea\x01z`.`R\x0f5)\x95e~\xb3\x17\xc9ߛ\xe3QrqC\x03\xc1\xbb\x17sV \xa8r<7\x94\xb9\x0e\xfd[\x864\x15\xa9\xf1+\x84d'Ig5\n{\x9c==\xc9H\xe7\x14XgZH\xd3ݬ\xf1#\xbdѰ\xe3J\x9b\x16\xe1\x05P\xb9\xa6\xa4\x8f\x97\x8d1\xc5G\xa5\xce\x0e1\xbf\xb8ޝmŽ|\xf2)\x82K\x02\xeb@\xfc=; \xf0\x1dp\x03(2Y\v\xda\xf0\xb2\xea\xc2\x0e\xb3\x00\xa2c\xa23&\x896\xb3\xd3Y\xd4e:A\xd6$\x9d\\\xcc\xee\x8eu\xbb\xfc\xccx\xda\xee\x14\x9c\xc7V3\x95\x867V\xfa\xb9\x85>\x1f\xaf\x9b\vZ\xb2LK`\xa5e˒\xb8q\xe72\xf9B\xe2\xa8\xe3\xf5\x13\xe3\xc6\xe7㻃\xd5e\xda4\x93eU\xa0\xc1\x90\xa3\x97I\xa1y\x8e\x8d\xfb\xe0\xf9?\x9a\xf1\x18+\fv\x8c\x17\xb5Z\xa0\xa3\x17sfi\xdc\xe6\xd5\xd3\xf3\ac鈬\x89\x98\x89\x9b\xee\v\x9c\xe6y\xfbQ\xa9e.\xf3W\x85\xcf\xef\x9aV\x8a[)\x95s\xde\xe9,L\xf2^\xfbީ\x17^&\x8e1\xf7t\x16*a\xf2\xea\x9e6\xe5\xd5=}uO_\xdd\xd3AyuO_\xdd\xd3W\xf7t\xbc\xbc\xba\xa7\x9d\xf2\xea\x9e&ۏ\x14\f״s;\xd1 \t\xab\xc4\x14\x8c9\xb4g\xc6\xf2\x99F\xferŒ\f\xe9\x9b\xf1\x9e#wk\xfcň5e\xdcǤ\xa6M]i\x8d^\x932m\x97dXL\xeeVb\x82\x17\xfe\fwW\x02\x02\xe7\xde]\xb9\x99\xea\xff|wW<\x9aÍsw)\x02L\xcc\xe8\xa5\xdf\\\tt\xe8\xdd|\x8b@m\xee\xc3\xf9\xc40\x97\x86T\"\vG:.\x87$\x8f\x8c\x1as`{h\x8c\xb6\xf9\x8d\x93\xe6O\x92-\x17\xdd78ɵ|\x99\v4\x11IY\xc0\xd5\xd5_V\u007f\f\xf2\x9fE\xf5(\xb1\xdd\xffb\xb3k\t\xeb\x14\xad\xbb\x98\xdeM\x8f짩\xfeq\xa4\xf9\x1c\x19N\xbd3\x13\xd3Kqu\xd6!\xa6\xea\xdd\xd1\xf8]\xd32!14\x9e\x0e\xea\x13\fаûM\xff\x17#}rhdfO\xdc\xec\xddeu\x96\xe7\xd6\a\xed\xdc@\tr\xea\x1f\xac\x18\xd28\x02Q*\x10\xbcp\f\b\x10\xfa\xf6\xe1K\xe5v\xb3\xce6\xc9\xf3{*\xe9)\xa4K\x13G\x9bT\xbfy\a\xf0\a\xd2E\x97\xdd\xe5\x99M\rMA\x1aR\x12B\xc7S=g\xa0.I\x03M\xdd.KH\xf9LO\xf4L#\x0f\xd033\xa9\xe9\x9d\xc9\x01Vj*\xe7\xcb$p&\xa6mv\x921gA\x9e\x99\xac\x99L\xb0\xb4\xc4\xcc\xe4t\xccN\x92\xe5<\xb5&\x920\xc7S+gA\x8e\xa5^\xa6$T&᚜F\xd9$G\xceo\xfa\xffP\xf2\xe4\xf3_\xd3xΐ|:\x152)\x012)l\x9f\xc79)\xc5qibc\x12U\x97&16\t\x8a\x13\x03'\xa5.\x9e\xa6%NMe6a1\x9e\x8c8\x05v,M1!\x05q\x02d79q\xb1\x1b0+M3\rƟ\x82\ne\xde\xd6\x16\xbf\x85\x04\xfe褥\xea\xb9\xc0)QɗA\x17\xcb\xfb\xe0\xf5\x8d\xb9\xd5\xf1\x18\xcf9\xdbg\xb8\xd5\x11\x907;(\xeb\xc2\xf0\xaa\xe8\xbc\xc5d\xf6xl\xdez\xf9E\xd2-\xe9푠}\xf9\xd6\bp\fd?@`\x1a\x9e\xb0(\xec\xbf'T\xc8\xdc\xcbg\x99\\\xa3\xb59\xf1\x13+\xffƍ\u007f6\xed\xd2\xed\xe1\xd0\x15r\xb2g\xa5\x85\x14\x9e\xc69#\xfc\x9avv\x9d\x8fNu\u007f\xafQ\x1dA\x1eP5^MT\xccګ\x85~i\xea\xbahU\x89\xd7I\xee\xfd\xbd\xbej\x89\xaf\x86fA\xc3{\xe1\xcc\xec\x10W\x82euH\x1b\x1cM\xa9N\x1b\v\xc5@\b\xd9@\x88\xf4O\xf1\xa5\x97ܵ{\x89P\xe99\x82\xa5$\xb7\xe2%\x02\xa6\x97\n\x99\x96\x06MK\xb2\f\x92\xeeʽD\xe8\xb4$xZ\xe4\x01\xa6߅{\xa9;p/\x10D\x9d\x1dF-\"]\xea\x1d\xb7\xc5\xc1T\xc2\xfcf\ued1dx\\\t \xa3w\xd9\xc6\x03\xaa\x04\x88'w\xd8fC\xaa\x94u0\f\xba~\xf8FZr\xc6͢c\xdf\xd4l\x99\xb4\x13\xd9\xf9\x9bf\x897\xcc\x12\xcfkS\xb0O\xbcI\xb6\xfc\x06Y\"\x9d\xcf\f\xb6&\x87N\xbc)\xb6(\xdc:3\xe0\x9a\x848u3l:\xe4\x9a\xdeN\x1b\xde\b;ÝH\x90\xb0\xd9&?|\" U\x8ej\xf6pe\x89h\xce\n\xe5 &\xea\x8f?x $R\x88\x80\a\xcf\xddE@\xc6\x1eJ^D\xacx\xfe\x866R\xb1\a\xfc$\xdd[\xd6)\xd4\xea\xf7\xe8=g\xeeuT\xc8\xe6\xf2\xd73#3\v_!\x18\x02l\x93c\xb2\xb6\xf0\xcfc\xe7\xe8:\xa0\xa7@\xe7>\"`\xdb4y\xf8\x9e\xd0\xd41|\x82\xc9V\xfe\u007f\x00\x00\x00\xff\xff!dء\xecj\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4VO\x8f\xeb4\x10\xbf\xe7S\x8c\x1e\x87w!\xe9{\xe2\x00\xca\r\x15\x0e+`\xb5\xda>\xed\x05qp\x9di;\xacc\x9b\xf1\xb8K\xf9\xf4\xc8v\xb2m\x93\x94]\x90\xf0-\xf6\xfc\xf9\xcdo\xfed\xaa\xba\xae+\xe5\xe9\t9\x90\xb3-(O\xf8\xa7\xa0M_\xa1y\xfe.4\xe4V\xc7\xcf\xd53ٮ\x85u\f\xe2\xfaG\f.\xb2\xc6\x1fpG\x96\x84\x9c\xadz\x14\xd5)Qm\x05\xa0\xacu\xa2\xd2uH\x9f\x00\xdaYag\fr\xbdG\xdb<\xc7-n#\x99\x0e9\x1b\x1f]\x1f?5\xdf6\x9f*\x00͘տP\x8fAT\xef[\xb0ј\n\xc0\xaa\x1e[\b\xc8II\x94\xc4\xc0\xf8G\xc4 \xa19\xa2Av\r\xb9*x\xd4\xc9\xf1\x9e]\xf4-\x9c\x1f\x8a\xfe\x00\xaa\x04\xb4ɦ6\xd9\xd4c1\x95_\r\x05\xf9\xe9\x96\xc4\xcf4Hy\x13Y\x99e@Y \x1c\x1c\xcb\xfd\xd9i\r!py!\xbb\x8fF\xf1\xa2r\x05\x10\xb4\xf3\xd8B\xd6\xf5JcW\x01\fLe[\xf5\xc0\xc5\xf1s1\xa7\x0fث\xe2\x04\xc0y\xb4\xdf?\xdc=}\xb3\xb9\xba\x06\xe80h&/\x99\xef\x85Ȁ\x02(\x18P\x808PZc\b\xa0#3Z\x81\x82\x12\xc8\xee\x1c\xf79G\xaf\xa6\x01\xd4\xd6E\x019 d\x05\xd9*\x03Ge\"~\r\xcavЫ\x130&/\x10텽,\x12\x1a\xf8\xc51f2[8\x88\xf8ЮV{\x92\xb1\xeb\xb4\xeb\xfbhIN\xab\xdc@\xb4\x8d\xe28\xac:<\xa2Y\x05\xda\u05ca\xf5\x81\x04\xb5Dƕ\xf2Tg\xe86w^\xd3w_\xf1Ч\xe1\xe3\x15V9\xa5\xca\n\xc2d\xf7\x17\x0f\xb9!\xfe!\x03\xa9\x1dJ}\x14\xd5\x12ř\xe8t\x95\xd8y\xfcq\xf3\x05F\xd79\x19S\xf63\xefg\xc5pNA\"\x8c\xec\x0e\xb9$qǮ\xcf6\xd1vޑ-ե\r\xa1\x9d\xd2\x1f\xe2\xb6'\tc\xed\xa6\\5\xb0Σ\b\xb6\b\xd1wJ\xb0k\xe0\xce\xc2Z\xf5h\xd6*\xe0\xff\x9e\x80\xc4t\xa8\x13\xb1\xefK\xc1\xe5\x14\x9d\n\x17\xd6.\x1e\xc61w#_\vݽ\xf1\xa8S\x06\x13\x89I\x9bv\xa4s{\xc0\xce1\xa8%\x95\xe6]H\xb2ƿ\xc42L\x92\x82f2_R\u007f\xbe\x8dfy\x9c䗃\n8\xbd\x9c`zH2S\xff\x86v\xa8O\xda`1Q\xa6\t\xbe\r%\x1d\xb4\xb1\x9f\xfb\xac\xe1\x1e_\x16n\x1fإɚ\xe7\xfa\xf5\xb9Q\x1bP\xfe7{\xb2\xb3p\xa7\x91\x15\xa9\xfc\x0f\xbb\x1c\xd5\x17\x03z0\x04\x1c\xadM};\x9b\x90\x19\xc8t\x92\xcfdH\xb0_@\xb3\x88\xe7\xce\xee\\\xde\x04Tr\xac\xa4\xf4\x13\x0e\xc9\x1e\xfc\x14\\\v\x06o纜\xf9\xf0z\x17\xa1\xe5\xe4?\xe9\u007fSN\xe3\x86\x18\x17}\xd7\x19\xd5\xe2C\xf2\xb8\xc4\xf8r\u007f\r(\xa31jk\xb0\x05\xe18\xd7.\xba\x8aY\x9d\xa6U3\x96\xday\x9fz\xa3\x80f\n\xa9O^\x0ehou\x03\xbc\xa8锿\xf2\f\xdb\xd3-\xd5\xf5\xebr8o\xa9R\xba-\xa4\xd9]\v-p\xf6.R\x16\xb3WJzq\xf3\x98\x11\xb2\xb9\x94\x1dg\xc6Uk\x8c\x8b\xc8<\x86\x9b\x10\x16\x93=\xbb\xcc滋\xf0\x828V\xfb1\xe0\xf3\xe8M\x9b\x9a\x17\xec\xee\xa7+\xee\x87\x0fW\xbbj\xfe\xd4\xcevT6t\xf8\xf5\xb7\xaaX\xc5\xeei\\0\xd3\xe5\xdf\x01\x00\x00\xff\xff\xfb\xb1p\x12\x1b\f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x8f۶\x13\xbd\xfbS\f\x92\xc3^\"9\xc1\xef\xf0+t)\x82M\x0fA\xf3g\x11o\xf7R\xf4@\x93#\x8b]\x8aTgHm\xddO_\f)\xad\xbd\xb6\x9cl\x8aV\x17C\x149|\xf3\u07bc!\xbd\xaa\xaaj\xa5\x06{\x87\xc46\xf8\x06\xd4`\xf1ψ^\u07b8\xbe\xff\x81k\x1b\xd6\xe3\x9bս\xf5\xa6\x81\xeb\xc41\xf4_\x90C\"\x8dﰵ\xdeF\x1b\xfc\xaaǨ\x8c\x8a\xaaY\x01(\xefCT2\xcc\xf2\n\xa0\x83\x8f\x14\x9cC\xaav\xe8\xeb\xfb\xb4\xc5m\xb2\xce \xe5\xe0\xf3\xd6\xe3\xeb\xfa\xff\xf5\xeb\x15\x80&\xcc\xcbom\x8f\x1cU?4\xe0\x93s+\x00\xafzl`\f.\xf5\xc8^\r܅\xe8\x82.\x9b\xd5#:\xa4P۰\xe2\x01\xb5콣\x90\x86\x06\x0e\x1fJ\x88\tW\xc9\xe9.G\xdbL\xd1>L\xd1\xf2\x04g9\xfe\xfc\x95I\x1f,\xc7\xe8\xcdK\x9a,\xcaWO\xb0ƽT\x11G\xb2~w\xf4!\x1b\xe1+\n\x88\aJ!\x94\xa5%\x8b\x03\xd12$\xec|\xf9is\v\xf3\xd6Y\x8cS\xf63\uf1c5|\x90@\b\xb3\xbeE*\"\xb6\x14\xfa\x1c\x13\xbd\x19\x82\xf51\xbfhgџ\xd2\xcfi\xdb\xdb(\xba\xff\x91\x90\xa3hU\xc3u\xeeB\xb0EH\x83Q\x11M\r\xef=\\\xab\x1eݵb\xfc\xcf\x05\x10\xa6\xb9\x12b\x9f'\xc1q\x03=\x9d\\X;6\xd8\xd4\xde.\xe8\xb5\xec\xe4̀\xfa\x89\x81$\x8am\xed\xe4\xec6\xd0\t\xafj\xf6\xf9r\xbc\xfa\xc9\xf4e\x83C\xe9\xfe\xadݝ\x8e\x02(c\xf2١\xdc\xcdŵ_!l!\xef뼓\x14j\x1bH\x10\x8d\xd6 Us\x9e\x13\x92DS\xc2\x16\x9d\xe1\xfa,\xe4\x05\xces*\x84F4V\xee\x1c\xe8S$\x8f\x13\xf3᧬/\x94\x1f\x02\xe4ң~\xea\xb1>\xa27\xb9\xa9\x9f\xa1\t\xb9\x86\x19\r<\xd8\xd8\x15s\xb8\xe3C\xeay*\xc8s\x8f\xfb\xa5\xe1\x13\xec\xb7\x1d\xca\xcc\xd2N\x11\x185a\x14\x1c\x8cN\xcc+ά\x01>&\xce\xf6R\x8b\x11AZ\x845\xf3\xea{ܟ\x13\r\xdf\x12w:\xef\xbf\r\xf9J\xce\xc5\x190a\x8b\x84>.Z\\\xee\x1e\xe41bv\xb9\t\x9a\xc5\xe0\x1a\x87\xc8\xeb0\"\x8d\x16\x1f\xd6\x0f\x81\xee\xad\xdfUBxU\n\x81\xd7\xf9ް~\x99\u007f.\xa4|\xfb\xf9\xdd\xe7\x06\xde\x1a\x03!vH\xa2Z\x9b\xdc\\hG\xa7ݫ\xdcq_A\xb2\xe6ǫ\u007f\xc2K\x18\x8as\x9e\xc1\xcd&W\xff^N\xee\fJ(\xda\x14U\x02\x81\xf4M\x11\xbb\x9f\xd4,\xfda\xa9\x10gL\xdb\x10\x1c\xaa\xf3ғ\xeek\t\xcd9\xa4Jv\xf8\x1e\x9b\xcd\xce\xfd\x86\xc9n\xa6ibx\xc9j^6\x17B\xb9\x97\xe4[\x8a\xda\xe1%\xa3/p\xbc\x9cJ\xf5\xb8\xc1\xb3ZtT1\xf1\xf77\xe9\xbcl\x9a\xb9\x9d\x1a\xb5N$\x05=\xc5\\\xb8\xd0\xfc;\x8dz\xe8\x14/\xb8\xed\x19\xa8od\xe5,\x83\xb3-\xea\xbdvX\x02Bh\x17\xaa\xe9\xbb ˃>\xf5K\xa5\xf5vT֩\xadÅo\xbfxu\xf1\xebE\xf1\x17\xf5<\x1bd\xb9\xb5\x98\x06\"\xa5\x12{\xaa\xb2i䠾\xd2\xd2\\\xd0|:\xfd\xdb\xf1\xe2œ\u007f\x0e\xf9U\a_\xceDn\xe0\xd7\xdfV%*\x9a\xbb\xf9\xa2/\x83\u007f\a\x00\x00\xff\xff\xe4\xf3S\x85\xb2\r\x00\x00"), } diff --git a/pkg/apis/velero/v1/backup.go b/pkg/apis/velero/v1/backup.go index 4c43784f00..4571d27294 100644 --- a/pkg/apis/velero/v1/backup.go +++ b/pkg/apis/velero/v1/backup.go @@ -52,6 +52,36 @@ type BackupSpec struct { // +nullable ExcludedResources []string `json:"excludedResources,omitempty"` + // IncludedClusterScopeResources is a slice of cluster scope + // resource type names to include in the backup. + // If set to "*", all cluster scope resource types are included. + // The default value is empty, which means only related cluster + // scope resources are included. + // +optional + // +nullable + IncludedClusterScopeResources []string `json:"includedClusterScopeResources,omitempty"` + + // ExcludedClusterScopeResources is a slice of cluster scope + // resource type names to exclude from the backup. + // If set to "*", all cluster scope resource types are excluded. + // +optional + // +nullable + ExcludedClusterScopeResources []string `json:"excludedClusterScopeResources,omitempty"` + + // IncludedNamespacedResources is a slice of namespace scope + // resource type names to include in the backup. + // The default value is "*". + // +optional + // +nullable + IncludedNamespacedResources []string `json:"includedNamespacedResources,omitempty"` + + // ExcludedNamespacedResources is a slice of namespace scope + // resource type names to exclude from the backup. + // If set to "*", all namespace scope resource types are excluded. + // +optional + // +nullable + ExcludedNamespacedResources []string `json:"excludedNamespacedResources,omitempty"` + // LabelSelector is a metav1.LabelSelector to filter with // when adding individual objects to the backup. If empty // or nil, all objects are included. Optional. diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index 8c488dd4f6..e1b5e3f8ab 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -299,6 +299,26 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.IncludedClusterScopeResources != nil { + in, out := &in.IncludedClusterScopeResources, &out.IncludedClusterScopeResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludedClusterScopeResources != nil { + in, out := &in.ExcludedClusterScopeResources, &out.ExcludedClusterScopeResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IncludedNamespacedResources != nil { + in, out := &in.IncludedNamespacedResources, &out.IncludedNamespacedResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludedNamespacedResources != nil { + in, out := &in.ExcludedNamespacedResources, &out.ExcludedNamespacedResources + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.LabelSelector != nil { in, out := &in.LabelSelector, &out.LabelSelector *out = new(metav1.LabelSelector) diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 9db76b1b57..72e472743c 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -205,6 +205,15 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, backupRequest.ResourceIncludesExcludes = collections.GetResourceIncludesExcludes(kb.discoveryHelper, backupRequest.Spec.IncludedResources, backupRequest.Spec.ExcludedResources) log.Infof("Including resources: %s", backupRequest.ResourceIncludesExcludes.IncludesString()) log.Infof("Excluding resources: %s", backupRequest.ResourceIncludesExcludes.ExcludesString()) + + backupRequest.NamespaceResourceIncludesExcludes = collections.GetScopedResourceIncludesExcludes(kb.discoveryHelper, backupRequest.Spec.IncludedNamespacedResources, backupRequest.Spec.ExcludedNamespacedResources, true) + log.Infof("Including namespaced resources: %s", backupRequest.NamespaceResourceIncludesExcludes.IncludesString()) + log.Infof("Excluding namespaced resources: %s", backupRequest.NamespaceResourceIncludesExcludes.ExcludesString()) + + backupRequest.ClusterResourceIncludesExcludes = collections.GetScopedResourceIncludesExcludes(kb.discoveryHelper, backupRequest.Spec.IncludedClusterScopeResources, backupRequest.Spec.ExcludedClusterScopeResources, false) + log.Infof("Including cluster-scoped resources: %s", backupRequest.ClusterResourceIncludesExcludes.ClusterIncludesString()) + log.Infof("Excluding cluster-scoped resources: %s", backupRequest.ClusterResourceIncludesExcludes.ExcludesString()) + log.Infof("Backing up all volumes using pod volume backup: %t", boolptr.IsSetToTrue(backupRequest.Backup.Spec.DefaultVolumesToFsBackup)) var err error @@ -392,9 +401,11 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, quit <- struct{}{} // back up CRD for resource if found. We should only need to do this if we've backed up at least - // one item for the resource and IncludeClusterResources is nil. If IncludeClusterResources is false - // we don't want to back it up, and if it's true it will already be included. - if backupRequest.Spec.IncludeClusterResources == nil { + // one item for the resource when IncludeClusterResources is nil, or cluster-scoped resource filters + // are not specified. If IncludeClusterResources is false we don't want to back it up, + // and if it's true it will already be included. + if backupRequest.Spec.IncludeClusterResources == nil || + (len(backupRequest.Spec.IncludedClusterScopeResources) == 0 && len(backupRequest.Spec.ExcludedClusterScopeResources) == 0) { for gr := range backedUpGroupResources { kb.backupCRD(log, gr, itemBackupper) } diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index aae4dfecdf..61d35bd411 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -156,7 +156,7 @@ func TestBackupProgressIsUpdated(t *testing.T) { // verifies that the set of items written to the backup tarball are // correct. Validation is done by looking at the names of the files in // the backup tarball; the contents of the files are not checked. -func TestBackupResourceFiltering(t *testing.T) { +func TestBackupOldResourceFiltering(t *testing.T) { tests := []struct { name string backup *velerov1.Backup @@ -2972,3 +2972,298 @@ func assertTarballOrdering(t *testing.T, backupFile io.Reader, orderedResources lastSeen = current } } + +func TestBackupNewResourceFiltering(t *testing.T) { + tests := []struct { + name string + backup *velerov1.Backup + apiResources []*test.APIResource + want []string + }{ + { + name: "no namespaced resources + some cluster resources", + backup: defaultBackup().IncludedClusterScopeResources("persistentvolumes").ExcludedNamespacedResources("*").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("testing").Result(), + ), + }, + want: []string{ + "resources/persistentvolumes/cluster/testing.json", + "resources/persistentvolumes/v1-preferredversion/cluster/testing.json", + }, + }, + { + name: "no namespaced resources + all cluster resources", + backup: defaultBackup().IncludedClusterScopeResources("*").ExcludedNamespacedResources("*").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/backups.velero.io.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/cluster/test2.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/backups.velero.io.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test2.json", + }, + }, + { + name: "some namespaced resources + no cluster resources 1", + backup: defaultBackup().ExcludedClusterScopeResources("*").IncludedNamespaces("foo", "zoo").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + { + name: "some namespaced resources + no cluster resources 2", + backup: defaultBackup().ExcludedClusterScopeResources("*").IncludedNamespacedResources("pods", "deployments").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + { + name: "some namespaced resources + no cluster resources 3", + backup: defaultBackup().ExcludedClusterScopeResources("*").IncludedNamespaces("foo").IncludedNamespacedResources("pods", "deployments").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "some namespaced resources + no cluster resources 4", + backup: defaultBackup().ExcludedClusterScopeResources("*").ExcludedNamespacedResources("pods").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + + { + name: "all namespaced resources + no cluster resources", + backup: defaultBackup().ExcludedClusterScopeResources("*").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + { + name: "all namespaced resources + some additional cluster resources", + backup: defaultBackup().IncludedClusterScopeResources("persistentvolumes").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + { + name: "all namespaced resources + all cluster resources", + backup: defaultBackup().IncludedClusterScopeResources("*").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/backups.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/backups.velero.io.json", + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/zoo/raz.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/cluster/test2.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test2.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/namespaces/zoo/raz.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var ( + h = newHarness(t) + req = &Request{Backup: tc.backup} + backupFile = bytes.NewBuffer([]byte{}) + ) + + for _, resource := range tc.apiResources { + h.addItems(t, resource) + } + + h.backupper.Backup(h.log, req, backupFile, nil, nil) + + assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...) + }) + } +} diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index efa4303ae5..6e170133e5 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -98,17 +98,33 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr log.Info("Excluding item because namespace is excluded") return false, nil } - // NOTE: we specifically allow namespaces to be backed up even if IncludeClusterResources is - // false. - if namespace == "" && groupResource != kuberesource.Namespaces && ib.backupRequest.Spec.IncludeClusterResources != nil && !*ib.backupRequest.Spec.IncludeClusterResources { - log.Info("Excluding item because resource is cluster-scoped and backup.spec.includeClusterResources is false") - return false, nil - } - if !ib.backupRequest.ResourceIncludesExcludes.ShouldInclude(groupResource.String()) { - log.Info("Excluding item because resource is excluded") - return false, nil + if useOldResourceFilters(ib.backupRequest.Spec) { + // NOTE: we specifically allow namespaces to be backed up even if IncludeClusterResources is false. + if namespace == "" && groupResource != kuberesource.Namespaces && + ib.backupRequest.Spec.IncludeClusterResources != nil && !*ib.backupRequest.Spec.IncludeClusterResources { + log.Info("Excluding item because resource is cluster-scoped and is excluded by cluster filter.") + return false, nil + } + + if !ib.backupRequest.ResourceIncludesExcludes.ShouldInclude(groupResource.String()) { + log.Info("Excluding item because resource is excluded") + return false, nil + } + } else { + // NOTE: we specifically allow namespaces to be backed up even if excluded by cluster-scoped resource filters. + if namespace == "" && groupResource != kuberesource.Namespaces && + !ib.backupRequest.ClusterResourceIncludesExcludes.ClusterScopedShouldInclude(groupResource.String()) { + log.Info("Excluding item because resource is cluster-scoped and is excluded by cluster filter.") + return false, nil + } + + if namespace != "" && !ib.backupRequest.NamespaceResourceIncludesExcludes.NamespacedShouldInclude(groupResource.String()) { + log.Info("Excluding namespaced item because resource is excluded.") + return false, nil + } } + } if metadata.GetDeletionTimestamp() != nil { @@ -590,3 +606,26 @@ func zoneFromPVNodeAffinity(res *corev1api.PersistentVolume, topologyKeys ...str return "", "" } + +// useOldResourceFilters checks whether to use old resource filters (IncludeClusterResources, +// IncludedResources and ExcludedResources), depending the backup's filters setting. +func useOldResourceFilters(backupSpec velerov1api.BackupSpec) bool { + // If all resource filters are none, it is treated as using old parameter filters. + if backupSpec.IncludeClusterResources == nil && + len(backupSpec.IncludedResources) == 0 && + len(backupSpec.ExcludedResources) == 0 && + len(backupSpec.IncludedClusterScopeResources) == 0 && + len(backupSpec.ExcludedClusterScopeResources) == 0 && + len(backupSpec.IncludedNamespacedResources) == 0 && + len(backupSpec.ExcludedNamespacedResources) == 0 { + return true + } + + if backupSpec.IncludeClusterResources != nil || + len(backupSpec.IncludedResources) > 0 || + len(backupSpec.ExcludedResources) > 0 { + return true + } + + return false +} diff --git a/pkg/backup/item_backupper_test.go b/pkg/backup/item_backupper_test.go index 2152a53010..667a5497a2 100644 --- a/pkg/backup/item_backupper_test.go +++ b/pkg/backup/item_backupper_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" ) @@ -170,3 +171,41 @@ func Test_zoneFromPVNodeAffinity(t *testing.T) { }) } } + +func TestUseOldResourceFilters(t *testing.T) { + tests := []struct { + name string + backup velerov1api.Backup + useOldResourceFilters bool + }{ + { + name: "backup with no filters should use old filters", + backup: *defaultBackup().Result(), + useOldResourceFilters: true, + }, + { + name: "backup with only old filters should use old filters", + backup: *defaultBackup().IncludeClusterResources(true).Result(), + useOldResourceFilters: true, + }, + { + name: "backup with only new filters should use new filters", + backup: *defaultBackup().IncludedClusterScopeResources("StorageClass").Result(), + useOldResourceFilters: false, + }, + { + // This case should not happen in Velero workflow, because filter validation not old and new + // filters used together. So this is only used for UT checking, and I assume old filters + // have higher priority, because old parameter should be the default one. + name: "backup with both old and new filters should use old filters", + backup: *defaultBackup().IncludeClusterResources(true).IncludedClusterScopeResources("StorageClass").Result(), + useOldResourceFilters: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.useOldResourceFilters, useOldResourceFilters(test.backup.Spec)) + }) + } +} diff --git a/pkg/backup/item_collector.go b/pkg/backup/item_collector.go index 88be4e0789..5586d07842 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -183,29 +183,56 @@ func (r *itemCollector) getResourceItems(log logrus.FieldLogger, gv schema.Group } // If the resource we are backing up is NOT namespaces, and it is cluster-scoped, check to see if - // we should include it based on the IncludeClusterResources setting. + // we should include it based on the IncludeClusterResources or cluster-scoped filters setting. if gr != kuberesource.Namespaces && clusterScoped { - if r.backupRequest.Spec.IncludeClusterResources == nil { - if !r.backupRequest.NamespaceIncludesExcludes.IncludeEverything() { - // when IncludeClusterResources == nil (auto), only directly - // back up cluster-scoped resources if we're doing a full-cluster - // (all namespaces) backup. Note that in the case of a subset of - // namespaces being backed up, some related cluster-scoped resources - // may still be backed up if triggered by a custom action (e.g. PVC->PV). - // If we're processing namespaces themselves, we will not skip here, they may be - // filtered out later. - log.Info("Skipping resource because it's cluster-scoped and only specific namespaces are included in the backup") + if useOldResourceFilters(r.backupRequest.Spec) { + if r.backupRequest.Spec.IncludeClusterResources == nil { + if !r.backupRequest.NamespaceIncludesExcludes.IncludeEverything() { + // when IncludeClusterResources == nil (auto), only directly + // back up cluster-scoped resources if we're doing a full-cluster + // (all namespaces) backup. Note that in the case of a subset of + // namespaces being backed up, some related cluster-scoped resources + // may still be backed up if triggered by a custom action (e.g. PVC->PV). + // If we're processing namespaces themselves, we will not skip here, they may be + // filtered out later. + log.Info("Skipping resource because it's cluster-scoped and only specific namespaces are included in the backup") + return nil, nil + } + } else if !*r.backupRequest.Spec.IncludeClusterResources { + log.Info("Skipping resource because it's cluster-scoped") + return nil, nil + } + } else { + if len(r.backupRequest.Spec.IncludedClusterScopeResources) == 0 && len(r.backupRequest.Spec.ExcludedClusterScopeResources) == 0 { + if !r.backupRequest.NamespaceIncludesExcludes.IncludeEverything() { + // when IncludedClusterScopeResources and ExcludedClusterScopeResources + // are not specified, only directly back up cluster-scoped resources, + // if we're doing a full-cluster + // (all namespaces) backup. Note that in the case of a subset of + // namespaces being backed up, some related cluster-scoped resources + // may still be backed up if triggered by a custom action (e.g. PVC->PV). + // If we're processing namespaces themselves, we will not skip here, they may be + // filtered out later. + log.Info("Skipping resource because it's cluster-scoped and only specific namespaces are included in the backup") + return nil, nil + } + } else if !r.backupRequest.ClusterResourceIncludesExcludes.ClusterScopedShouldInclude(gr.String()) { + log.Info("Skipping resource because it's cluster-scoped") return nil, nil } - } else if !*r.backupRequest.Spec.IncludeClusterResources { - log.Info("Skipping resource because it's cluster-scoped") - return nil, nil } } - if !r.backupRequest.ResourceIncludesExcludes.ShouldInclude(gr.String()) { - log.Infof("Skipping resource because it's excluded") - return nil, nil + if useOldResourceFilters(r.backupRequest.Spec) { + if !r.backupRequest.ResourceIncludesExcludes.ShouldInclude(gr.String()) { + log.Infof("Skipping resource because it's excluded") + return nil, nil + } + } else { + if !clusterScoped && !r.backupRequest.NamespaceResourceIncludesExcludes.NamespacedShouldInclude(gr.String()) { + log.Info("Skipping namespaced resource, because it's excluded.") + return nil, nil + } } if cohabitator, found := r.cohabitatingResources[resource.Name]; found { diff --git a/pkg/backup/request.go b/pkg/backup/request.go index a94faa2d77..e5b6be4b53 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -40,17 +40,19 @@ type itemKey struct { type Request struct { *velerov1api.Backup - StorageLocation *velerov1api.BackupStorageLocation - SnapshotLocations []*velerov1api.VolumeSnapshotLocation - NamespaceIncludesExcludes *collections.IncludesExcludes - ResourceIncludesExcludes *collections.IncludesExcludes - ResourceHooks []hook.ResourceHook - ResolvedActions []framework.BackupItemResolvedActionV2 - ResolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction - VolumeSnapshots []*volume.Snapshot - PodVolumeBackups []*velerov1api.PodVolumeBackup - BackedUpItems map[itemKey]struct{} - CSISnapshots []snapshotv1api.VolumeSnapshot + StorageLocation *velerov1api.BackupStorageLocation + SnapshotLocations []*velerov1api.VolumeSnapshotLocation + NamespaceIncludesExcludes *collections.IncludesExcludes + ResourceIncludesExcludes *collections.IncludesExcludes + NamespaceResourceIncludesExcludes *collections.IncludesExcludes + ClusterResourceIncludesExcludes *collections.IncludesExcludes + ResourceHooks []hook.ResourceHook + ResolvedActions []framework.BackupItemResolvedActionV2 + ResolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction + VolumeSnapshots []*volume.Snapshot + PodVolumeBackups []*velerov1api.PodVolumeBackup + BackedUpItems map[itemKey]struct{} + CSISnapshots []snapshotv1api.VolumeSnapshot } // BackupResourceList returns the list of backed up resources grouped by the API diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index f18ce49092..5c53bca68b 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -150,6 +150,30 @@ func (b *BackupBuilder) ExcludedResources(resources ...string) *BackupBuilder { return b } +// IncludedClusterScopeResources sets the Backup's included cluster resources. +func (b *BackupBuilder) IncludedClusterScopeResources(resources ...string) *BackupBuilder { + b.object.Spec.IncludedClusterScopeResources = resources + return b +} + +// ExcludedClusterScopeResources sets the Backup's excluded cluster resources. +func (b *BackupBuilder) ExcludedClusterScopeResources(resources ...string) *BackupBuilder { + b.object.Spec.ExcludedClusterScopeResources = resources + return b +} + +// IncludedNamespacedResources sets the Backup's included namespaced resources. +func (b *BackupBuilder) IncludedNamespacedResources(resources ...string) *BackupBuilder { + b.object.Spec.IncludedNamespacedResources = resources + return b +} + +// ExcludedNamespacedResources sets the Backup's excluded namespaced resources. +func (b *BackupBuilder) ExcludedNamespacedResources(resources ...string) *BackupBuilder { + b.object.Spec.ExcludedNamespacedResources = resources + return b +} + // IncludeClusterResources sets the Backup's "include cluster resources" flag. func (b *BackupBuilder) IncludeClusterResources(val bool) *BackupBuilder { b.object.Spec.IncludeClusterResources = &val diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index 52aeab3d52..2cd888a246 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -82,23 +82,27 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command { } type CreateOptions struct { - Name string - TTL time.Duration - SnapshotVolumes flag.OptionalBool - DefaultVolumesToFsBackup flag.OptionalBool - IncludeNamespaces flag.StringArray - ExcludeNamespaces flag.StringArray - IncludeResources flag.StringArray - ExcludeResources flag.StringArray - Labels flag.Map - Selector flag.LabelSelector - IncludeClusterResources flag.OptionalBool - Wait bool - StorageLocation string - SnapshotLocations []string - FromSchedule string - OrderedResources string - CSISnapshotTimeout time.Duration + Name string + TTL time.Duration + SnapshotVolumes flag.OptionalBool + DefaultVolumesToFsBackup flag.OptionalBool + IncludeNamespaces flag.StringArray + ExcludeNamespaces flag.StringArray + IncludeResources flag.StringArray + ExcludeResources flag.StringArray + IncludeClusterScopeResources flag.StringArray + ExcludeClusterScopeResources flag.StringArray + IncludeNamespacedResources flag.StringArray + ExcludeNamespacedResources flag.StringArray + Labels flag.Map + Selector flag.LabelSelector + IncludeClusterResources flag.OptionalBool + Wait bool + StorageLocation string + SnapshotLocations []string + FromSchedule string + OrderedResources string + CSISnapshotTimeout time.Duration client veleroclient.Interface } @@ -116,8 +120,12 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { flags.DurationVar(&o.TTL, "ttl", o.TTL, "How long before the backup can be garbage collected.") flags.Var(&o.IncludeNamespaces, "include-namespaces", "Namespaces to include in the backup (use '*' for all namespaces).") flags.Var(&o.ExcludeNamespaces, "exclude-namespaces", "Namespaces to exclude from the backup.") - flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources).") - flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io.") + flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources). Cannot work with include-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources.") + flags.Var(&o.ExcludeResources, "exclude-resources", "Resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io. Cannot work with include-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources.") + flags.Var(&o.IncludeClusterScopeResources, "include-cluster-scope-resources", "Cluster-scoped resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.") + flags.Var(&o.ExcludeClusterScopeResources, "exclude-cluster-scope-resources", "Cluster-scoped resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.") + flags.Var(&o.IncludeNamespacedResources, "include-namespaced-resources", "Namespaced resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.") + flags.Var(&o.ExcludeNamespacedResources, "exclude-namespaced-resources", "Namespaced resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io(use '*' for all resources). Cannot work with include-resources, exclude-resources and include-cluster-resources.") flags.Var(&o.Labels, "labels", "Labels to apply to the backup.") flags.StringVar(&o.StorageLocation, "storage-location", "", "Location in which to store the backup.") flags.StringSliceVar(&o.SnapshotLocations, "volume-snapshot-locations", o.SnapshotLocations, "List of locations (at most one per provider) where volume snapshots should be stored.") @@ -129,7 +137,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { // like a normal bool flag f.NoOptDefVal = "true" - f = flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources in the backup") + f = flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources in the backup. Cannot work with include-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources.") f.NoOptDefVal = "true" f = flags.VarPF(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", "", "Use pod volume file system backup by default for volumes") @@ -160,7 +168,7 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto // Ensure that unless FromSchedule is set, args contains a backup name if o.FromSchedule == "" && len(args) != 1 { - return fmt.Errorf("A backup name is required, unless you are creating based on a schedule.") + return fmt.Errorf("a backup name is required, unless you are creating based on a schedule") } errs := collections.ValidateNamespaceIncludesExcludes(o.IncludeNamespaces, o.ExcludeNamespaces) @@ -168,6 +176,12 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto return kubeerrs.NewAggregate(errs) } + if o.oldAndNewFilterParametersUsedTogether() { + return fmt.Errorf("include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n" + + "include-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources are new new filter parameters.\n" + + "They cannot be used together") + } + if o.StorageLocation != "" { location := &velerov1api.BackupStorageLocation{} if err := client.Get(context.Background(), kbclient.ObjectKey{ @@ -297,13 +311,13 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { func ParseOrderedResources(orderMapStr string) (map[string]string, error) { entries := strings.Split(orderMapStr, ";") if len(entries) == 0 { - return nil, fmt.Errorf("Invalid OrderedResources '%s'.", orderMapStr) + return nil, fmt.Errorf("invalid OrderedResources '%s'", orderMapStr) } orderedResources := make(map[string]string) for _, entry := range entries { kv := strings.Split(entry, "=") if len(kv) != 2 { - return nil, fmt.Errorf("Invalid OrderedResources '%s'.", entry) + return nil, fmt.Errorf("invalid OrderedResources '%s'", entry) } kind := strings.TrimSpace(kv[0]) order := strings.TrimSpace(kv[1]) @@ -331,6 +345,10 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro ExcludedNamespaces(o.ExcludeNamespaces...). IncludedResources(o.IncludeResources...). ExcludedResources(o.ExcludeResources...). + IncludedClusterScopeResources(o.IncludeClusterScopeResources...). + ExcludedClusterScopeResources(o.ExcludeClusterScopeResources...). + IncludedNamespacedResources(o.IncludeNamespacedResources...). + ExcludedNamespacedResources(o.ExcludeNamespacedResources...). LabelSelector(o.Selector.LabelSelector). TTL(o.TTL). StorageLocation(o.StorageLocation). @@ -358,3 +376,21 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro backup := backupBuilder.ObjectMeta(builder.WithLabelsMap(o.Labels.Data())).Result() return backup, nil } + +func (o *CreateOptions) oldAndNewFilterParametersUsedTogether() bool { + haveOldResourceFilterParameters := false + haveNewResourceFilterParameters := false + + if (len(o.IncludeResources) > 0) || (len(o.ExcludeResources) > 0) || (o.IncludeClusterResources.Value != nil) { + haveOldResourceFilterParameters = true + } + + if len(o.IncludeClusterScopeResources) > 0 || (len(o.ExcludeClusterScopeResources) > 0) || (len(o.IncludeNamespacedResources) > 0) || (len(o.ExcludeNamespacedResources) > 0) { + haveNewResourceFilterParameters = true + } + + if haveOldResourceFilterParameters && haveNewResourceFilterParameters { + return true + } + return false +} diff --git a/pkg/cmd/cli/schedule/create.go b/pkg/cmd/cli/schedule/create.go index abd819d3bf..c1564f7fd3 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -135,19 +135,23 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { }, Spec: api.ScheduleSpec{ Template: api.BackupSpec{ - IncludedNamespaces: o.BackupOptions.IncludeNamespaces, - ExcludedNamespaces: o.BackupOptions.ExcludeNamespaces, - IncludedResources: o.BackupOptions.IncludeResources, - ExcludedResources: o.BackupOptions.ExcludeResources, - IncludeClusterResources: o.BackupOptions.IncludeClusterResources.Value, - LabelSelector: o.BackupOptions.Selector.LabelSelector, - SnapshotVolumes: o.BackupOptions.SnapshotVolumes.Value, - TTL: metav1.Duration{Duration: o.BackupOptions.TTL}, - StorageLocation: o.BackupOptions.StorageLocation, - VolumeSnapshotLocations: o.BackupOptions.SnapshotLocations, - DefaultVolumesToFsBackup: o.BackupOptions.DefaultVolumesToFsBackup.Value, - OrderedResources: orders, - CSISnapshotTimeout: metav1.Duration{Duration: o.BackupOptions.CSISnapshotTimeout}, + IncludedNamespaces: o.BackupOptions.IncludeNamespaces, + ExcludedNamespaces: o.BackupOptions.ExcludeNamespaces, + IncludedResources: o.BackupOptions.IncludeResources, + ExcludedResources: o.BackupOptions.ExcludeResources, + IncludedClusterScopeResources: o.BackupOptions.IncludeClusterScopeResources, + ExcludedClusterScopeResources: o.BackupOptions.ExcludeClusterScopeResources, + IncludedNamespacedResources: o.BackupOptions.IncludeNamespacedResources, + ExcludedNamespacedResources: o.BackupOptions.ExcludeNamespacedResources, + IncludeClusterResources: o.BackupOptions.IncludeClusterResources.Value, + LabelSelector: o.BackupOptions.Selector.LabelSelector, + SnapshotVolumes: o.BackupOptions.SnapshotVolumes.Value, + TTL: metav1.Duration{Duration: o.BackupOptions.TTL}, + StorageLocation: o.BackupOptions.StorageLocation, + VolumeSnapshotLocations: o.BackupOptions.SnapshotLocations, + DefaultVolumesToFsBackup: o.BackupOptions.DefaultVolumesToFsBackup.Value, + OrderedResources: orders, + CSISnapshotTimeout: metav1.Duration{Duration: o.BackupOptions.CSISnapshotTimeout}, }, Schedule: o.Schedule, UseOwnerReferencesInBackup: &o.UseOwnerReferencesInBackup, diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index 9be8ed59ee..182f86533f 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -148,6 +148,32 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) { d.Printf("\tCluster-scoped:\t%s\n", BoolPointerString(spec.IncludeClusterResources, "excluded", "included", "auto")) + if len(spec.IncludedClusterScopeResources) == 0 { + s = "*" + } else { + s = strings.Join(spec.IncludedClusterScopeResources, ", ") + } + d.Printf("\tIncluded cluster-scoped:\t%s\n", s) + if len(spec.ExcludedClusterScopeResources) == 0 { + s = emptyDisplay + } else { + s = strings.Join(spec.ExcludedClusterScopeResources, ", ") + } + d.Printf("\tExcluded cluster-scoped:\t%s\n", s) + + if len(spec.IncludedNamespacedResources) == 0 { + s = emptyDisplay + } else { + s = strings.Join(spec.IncludedNamespacedResources, ", ") + } + d.Printf("\tIncluded namespaced:\t%s\n", s) + if len(spec.ExcludedNamespacedResources) == 0 { + s = emptyDisplay + } else { + s = strings.Join(spec.ExcludedNamespacedResources, ", ") + } + d.Printf("\tExcluded namespaced:\t%s\n", s) + d.Println() s = emptyDisplay if spec.LabelSelector != nil { diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index be1c1872e7..26c545d611 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -463,11 +463,30 @@ func (c *backupController) prepareBackupRequest(backup *velerov1api.Backup, logg request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("error getting namespace list: %v", err)) } + // validate whether Included/Excluded resources and IncludedClusterResource are mixed with + // Included/Excluded cluster-scoped/namespaced resources. + if oldAndNewFilterParametersUsedTogether(request.Spec) { + validatedError := fmt.Sprintf("include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n" + + "include-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources are new new filter parameters.\n" + + "They cannot be used together") + request.Status.ValidationErrors = append(request.Status.ValidationErrors, validatedError) + } + // validate the included/excluded resources for _, err := range collections.ValidateIncludesExcludes(request.Spec.IncludedResources, request.Spec.ExcludedResources) { request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded resource lists: %v", err)) } + // validate the cluster-scoped included/excluded resources + for _, err := range collections.ValidateScopedIncludesExcludes(request.Spec.IncludedClusterScopeResources, request.Spec.ExcludedClusterScopeResources) { + request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid cluster-scoped included/excluded resource lists: %s", err)) + } + + // validate the namespaced included/excluded resources + for _, err := range collections.ValidateScopedIncludesExcludes(request.Spec.IncludedNamespacedResources, request.Spec.ExcludedNamespacedResources) { + request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid namespaced included/excluded resource lists: %s", err)) + } + // validate the included/excluded namespaces for _, err := range collections.ValidateNamespaceIncludesExcludes(request.Spec.IncludedNamespaces, request.Spec.ExcludedNamespaces) { request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err)) @@ -1095,3 +1114,22 @@ func (c *backupController) recreateVolumeSnapshotContent(vsc snapshotv1api.Volum return nil } + +func oldAndNewFilterParametersUsedTogether(backupSpec velerov1api.BackupSpec) bool { + haveOldResourceFilterParameters := false + haveNewResourceFilterParameters := false + + if (len(backupSpec.IncludedResources) > 0) || (len(backupSpec.ExcludedResources) > 0) || (backupSpec.IncludeClusterResources != nil) { + haveOldResourceFilterParameters = true + } + + if len(backupSpec.IncludedClusterScopeResources) > 0 || (len(backupSpec.ExcludedClusterScopeResources) > 0) || + (len(backupSpec.IncludedNamespacedResources) > 0) || (len(backupSpec.ExcludedNamespacedResources) > 0) { + haveNewResourceFilterParameters = true + } + + if haveOldResourceFilterParameters && haveNewResourceFilterParameters { + return true + } + return false +} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 56d31ab909..ab00b4bad4 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -183,6 +183,12 @@ func TestProcessBackupValidationFailures(t *testing.T) { backupLocation: defaultBackupLocation, expectedErrs: []string{"encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified"}, }, + { + name: "use old filter parameters and new filter parameters together", + backup: defaultBackup().IncludeClusterResources(true).IncludedNamespacedResources("Deployment").IncludedNamespaces("default").Result(), + backupLocation: defaultBackupLocation, + expectedErrs: []string{"include-resources, exclude-resources and include-cluster-resources are old filter parameters.\ninclude-cluster-scope-resources, exclude-cluster-scope-resources, include-namespaced-resources and exclude-namespaced-resources are new new filter parameters.\nThey cannot be used together"}, + }, } for _, test := range tests { diff --git a/pkg/util/collections/includes_excludes.go b/pkg/util/collections/includes_excludes.go index 231689be04..d47bced30f 100644 --- a/pkg/util/collections/includes_excludes.go +++ b/pkg/util/collections/includes_excludes.go @@ -101,6 +101,29 @@ func (ie *IncludesExcludes) ShouldInclude(s string) bool { return ie.includes.Len() == 0 || ie.includes.Has("*") || ie.includes.match(s) } +// NamespacedShouldInclude returns whether the specified namespaced item +// should be included or not. Excludes list has higher priority. +// Except resources listed in excludes, other things should be included. +func (ie *IncludesExcludes) NamespacedShouldInclude(s string) bool { + if ie.excludes.Has("*") || ie.excludes.match(s) { + return false + } + + // len=0 means include everything + return ie.includes.Len() == 0 || ie.includes.Has("*") || ie.includes.match(s) +} + +// ClusterScopedShouldInclude returns whether the specified cluster-scoped item +// should be included or not. Excludes list has higher priority. +// Include resources listed in the includes, and exclude resources in the excludes. +func (ie *IncludesExcludes) ClusterScopedShouldInclude(s string) bool { + if ie.excludes.Has("*") || ie.excludes.match(s) { + return false + } + + return ie.includes.Has("*") || ie.includes.match(s) +} + // IncludesString returns a string containing all of the includes, separated by commas, or * if the // list is empty. func (ie *IncludesExcludes) IncludesString() string { @@ -120,6 +143,12 @@ func asString(in []string, empty string) string { return strings.Join(in, ", ") } +// ClusterIncludesString returns a string containing all of the cluster-scoped includes, separated by commas, +// or if the list is empty +func (ie *IncludesExcludes) ClusterIncludesString() string { + return asString(ie.GetIncludes(), "") +} + // IncludeEverything returns true if the includes list is empty or '*' // and the excludes list is empty, or false otherwise. func (ie *IncludesExcludes) IncludeEverything() bool { @@ -177,6 +206,35 @@ func ValidateNamespaceIncludesExcludes(includesList, excludesList []string) []er return errs } +// ValidateScopedIncludesExcludes checks provided lists of namespaced or cluster-scoped +// included and excluded items to ensure they are a valid set of IncludesExcludes data. +func ValidateScopedIncludesExcludes(includesList, excludesList []string) []error { + var errs []error + + includes := sets.NewString(includesList...) + excludes := sets.NewString(excludesList...) + + if includes.Len() > 1 && includes.Has("*") { + errs = append(errs, errors.New("includes list must either contain '*' only, or a non-empty list of items")) + } + + if excludes.Len() > 1 && excludes.Has("*") { + errs = append(errs, errors.New("excludes list must either contain '*' only, or a non-empty list of items")) + } + + if includes.Len() > 0 && excludes.Has("*") { + errs = append(errs, errors.New("when exclude is '*', include cannot have value")) + } + + for _, itm := range excludes.List() { + if includes.Has(itm) { + errs = append(errs, errors.Errorf("excludes list cannot contain an item in the includes list: %v", itm)) + } + } + + return errs +} + func validateNamespaceName(ns string) []error { var errs []error @@ -237,6 +295,37 @@ func GenerateIncludesExcludes(includes, excludes []string, mapFunc func(string) return res } +func GenerateScopedIncludesExcludes(includes, excludes []string, mapFunc func(string, bool) string, namespaced bool) *IncludesExcludes { + res := NewIncludesExcludes() + + for _, item := range includes { + if item == "*" { + res.Includes(item) + continue + } + + key := mapFunc(item, namespaced) + if key == "" { + continue + } + res.Includes(key) + } + + for _, item := range excludes { + if item == "*" { + res.Excludes(item) + continue + } + + key := mapFunc(item, namespaced) + if key == "" { + continue + } + res.Excludes(key) + } + return res +} + // GetResourceIncludesExcludes takes the lists of resources to include and exclude, uses the // discovery helper to resolve them to fully-qualified group-resource names, and returns an // IncludesExcludes list. @@ -260,3 +349,25 @@ func GetResourceIncludesExcludes(helper discovery.Helper, includes, excludes []s return resources } + +func GetScopedResourceIncludesExcludes(helper discovery.Helper, includes, excludes []string, namespaced bool) *IncludesExcludes { + resources := GenerateScopedIncludesExcludes( + includes, + excludes, + func(item string, namespaced bool) string { + gvr, resource, err := helper.ResourceFor(schema.ParseGroupResource(item).WithVersion("")) + if err != nil { + return item + } + if resource.Namespaced != namespaced { + return "" + } + + gr := gvr.GroupResource() + return gr.String() + }, + namespaced, + ) + + return resources +} diff --git a/pkg/util/collections/includes_excludes_test.go b/pkg/util/collections/includes_excludes_test.go index 672a309d23..a20a97ec43 100644 --- a/pkg/util/collections/includes_excludes_test.go +++ b/pkg/util/collections/includes_excludes_test.go @@ -295,3 +295,261 @@ func TestValidateNamespaceIncludesExcludes(t *testing.T) { }) } } + +func TestValidateScopedIncludesExcludes(t *testing.T) { + tests := []struct { + name string + includes []string + excludes []string + wantErr []error + }{ + // includes testing + { + name: "empty includes is valid", + includes: []string{}, + wantErr: []error{}, + }, + { + name: "asterisk includes is valid", + includes: []string{"*"}, + wantErr: []error{}, + }, + { + name: "include everything not allowed with other includes", + includes: []string{"*", "foo"}, + wantErr: []error{errors.New("includes list must either contain '*' only, or a non-empty list of items")}, + }, + // excludes testing + { + name: "empty excludes is valid", + excludes: []string{}, + wantErr: []error{}, + }, + { + name: "asterisk excludes is valid", + excludes: []string{"*"}, + wantErr: []error{}, + }, + { + name: "include everything not allowed with other includes", + excludes: []string{"*", "foo"}, + wantErr: []error{errors.New("excludes list must either contain '*' only, or a non-empty list of items")}, + }, + // includes and excludes combination testing + { + name: "asterisk exlucdes doesn't work with non-empty includes", + includes: []string{"foo"}, + excludes: []string{"*"}, + wantErr: []error{errors.New("when exclude is '*', include cannot have value")}, + }, + { + name: "excludes cannot contain items in includes", + includes: []string{"foo", "bar"}, + excludes: []string{"bar"}, + wantErr: []error{errors.New("excludes list cannot contain an item in the includes list: bar")}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + errs := ValidateScopedIncludesExcludes(tc.includes, tc.excludes) + + require.Equal(t, len(tc.wantErr), len(errs)) + + for i := 0; i < len(tc.wantErr); i++ { + assert.Equal(t, tc.wantErr[i].Error(), errs[i].Error()) + } + }) + } +} + +func TestNamespacedShouldInclude(t *testing.T) { + tests := []struct { + name string + includes []string + excludes []string + item string + want bool + }{ + { + name: "empty string should include every item", + item: "foo", + want: true, + }, + { + name: "include * should include every item", + includes: []string{"*"}, + item: "foo", + want: true, + }, + { + name: "item in includes list should include item", + includes: []string{"foo", "bar", "baz"}, + item: "foo", + want: true, + }, + { + name: "item not in includes list should not include item", + includes: []string{"foo", "baz"}, + item: "bar", + want: false, + }, + { + name: "include *, excluded item should not include item", + includes: []string{"*"}, + excludes: []string{"foo"}, + item: "foo", + want: false, + }, + { + name: "include *, exclude foo, bar should be included", + includes: []string{"*"}, + excludes: []string{"foo"}, + item: "bar", + want: true, + }, + { + name: "an item both included and excluded should not be included", + includes: []string{"foo"}, + excludes: []string{"foo"}, + item: "foo", + want: false, + }, + { + name: "wildcard should include item", + includes: []string{"*.bar"}, + item: "foo.bar", + want: true, + }, + { + name: "wildcard mismatch should not include item", + includes: []string{"*.bar"}, + item: "bar.foo", + want: false, + }, + { + name: "exclude * should include nothing", + excludes: []string{"*"}, + item: "foo", + want: false, + }, + { + name: "wildcard exclude should not include item", + includes: []string{"*"}, + excludes: []string{"*.bar"}, + item: "foo.bar", + want: false, + }, + { + name: "wildcard exclude mismatch should include item", + excludes: []string{"*.bar"}, + item: "bar.foo", + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + includesExcludes := NewIncludesExcludes().Includes(tc.includes...).Excludes(tc.excludes...) + + if got := includesExcludes.NamespacedShouldInclude((tc.item)); got != tc.want { + t.Errorf("want %t, got %t", tc.want, got) + } + }) + } +} + +func TestClusterScopedShouldInclude(t *testing.T) { + tests := []struct { + name string + includes []string + excludes []string + item string + want bool + }{ + { + name: "empty string should include nothing", + item: "foo", + want: false, + }, + { + name: "include * should include every item", + includes: []string{"*"}, + item: "foo", + want: true, + }, + { + name: "item in includes list should include item", + includes: []string{"foo", "bar", "baz"}, + item: "foo", + want: true, + }, + { + name: "item not in includes list should not include item", + includes: []string{"foo", "baz"}, + item: "bar", + want: false, + }, + { + name: "include *, excluded item should not include item", + includes: []string{"*"}, + excludes: []string{"foo"}, + item: "foo", + want: false, + }, + { + name: "include *, exclude foo, bar should be included", + includes: []string{"*"}, + excludes: []string{"foo"}, + item: "bar", + want: true, + }, + { + name: "an item both included and excluded should not be included", + includes: []string{"foo"}, + excludes: []string{"foo"}, + item: "foo", + want: false, + }, + { + name: "wildcard should include item", + includes: []string{"*.bar"}, + item: "foo.bar", + want: true, + }, + { + name: "wildcard mismatch should not include item", + includes: []string{"*.bar"}, + item: "bar.foo", + want: false, + }, + { + name: "exclude * should include nothing", + excludes: []string{"*"}, + item: "foo", + want: false, + }, + { + name: "wildcard exclude should not include item", + includes: []string{"*"}, + excludes: []string{"*.bar"}, + item: "foo.bar", + want: false, + }, + { + name: "wildcard exclude mismatch should not include item", + excludes: []string{"*.bar"}, + item: "bar.foo", + want: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + includesExcludes := NewIncludesExcludes().Includes(tc.includes...).Excludes(tc.excludes...) + + if got := includesExcludes.ClusterScopedShouldInclude((tc.item)); got != tc.want { + t.Errorf("want %t, got %t", tc.want, got) + } + }) + } +}