From c2a9cef97842dea8bca0f77214fa12fd995a6275 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 +- design/cluster-scope-resource-filter.md | 6 - pkg/apis/velero/v1/backup.go | 30 + pkg/apis/velero/v1/zz_generated.deepcopy.go | 20 + pkg/backup/backup.go | 33 +- pkg/backup/backup_test.go | 579 +++++++++++++++++- pkg/backup/item_backupper.go | 58 +- pkg/backup/item_backupper_test.go | 39 ++ pkg/backup/item_collector.go | 63 +- 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 ++++++++ 21 files changed, 1410 insertions(+), 89 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 99e05ae2e5..31ec563070 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/design/cluster-scope-resource-filter.md b/design/cluster-scope-resource-filter.md index 6dab5cd96c..cd9de91416 100644 --- a/design/cluster-scope-resource-filter.md +++ b/design/cluster-scope-resource-filter.md @@ -8,7 +8,6 @@ - [High-Level Design](#high-level-design) - [Parameters Rules](#parameters-rules) - [Using scenarios:](#using-scenarios) - - [no namespaced resources + no cluster resources](#no-namespaced-resources--no-cluster-resources) - [no namespaced resources + some cluster resources](#no-namespaced-resources--some-cluster-resources) - [no namespaced resources + all cluster resources](#no-namespaced-resources--all-cluster-resources) - [some namespaced resources + no cluster resources](#some-namespaced-resources--no-cluster-resources) @@ -67,14 +66,9 @@ Restore and other code pieces also use resource filtering will be handled in fut * If both `--include-cluster-scope-resources` and `--exclude-cluster-scope-resources` are not present, it means no additional cluster resource is included per resource type, just as the existing `--include-cluster-resources` parameter not setting value. Cluster resources are related to the namespace scope resources, which means those are returned in the namespace resources' BackupItemAction's result AdditionalItems array, are still included in backup by default. Taking backing up PVC scenario as an example, PVC is namespaced, PV is in cluster scope. PVC's BIA will include PVC related PV into backup too. -* If the backup contains no resource, validation failure should be returned. - ### Using scenarios: Please notice, if the scenario give the example of using old filtering parameters (`--include-cluster-resources`, `--include-resources` and `--exclude-resources`), that means the old parameters also work for this case. If old parameters example is not given, that means they don't work for this scenario, only new parameters (`--include-cluster-scope-resources`, `--include-namespaced-resources`, `--exclude-cluster-scope-resources` and `--exclude-namespaced-resources`) work. -#### no namespaced resources + no cluster resources -This is not allowed. Backup or restore cannot contain no resource. - #### no namespaced resources + some cluster resources The following command means backup no namespaced resources and some cluster resources. 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..469e6479f5 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 @@ -391,12 +400,24 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, // no more progress updates will be sent on the 'update' channel 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 { - for gr := range backedUpGroupResources { - kb.backupCRD(log, gr, itemBackupper) + if useOldResourceFilters(backupRequest.Spec) { + // 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 when 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 { + for gr := range backedUpGroupResources { + kb.backupCRD(log, gr, itemBackupper) + } + } + } else { + // 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 when cluster-scoped resource filters are not specified. + // If cluster-scoped resource filters are set, whether or not to backup up the resource, + // depending on whether CRD is included by the filters setting. + if 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 24dacce5f5..eb18319a5a 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -156,12 +156,13 @@ 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 apiResources []*test.APIResource want []string + actions []biav2.BackupItemAction }{ { name: "no filters backs up everything", @@ -759,6 +760,49 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, + { + name: "some namespaced resources + only related cluster resources", + backup: defaultBackup().IncludedNamespaces("foo").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Volumes(builder.ForVolume("foo").PersistentVolumeClaimSource("test-1").Result()).Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVCs( + builder.ForPersistentVolumeClaim("foo", "test-1").VolumeName("test1").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/persistentvolumeclaims/namespaces/foo/test-1.json", + "resources/persistentvolumeclaims/v1-preferredversion/namespaces/foo/test-1.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil + }, + }, + }, + }, } for _, tc := range tests { @@ -773,7 +817,7 @@ func TestBackupResourceFiltering(t *testing.T) { h.addItems(t, resource) } - h.backupper.Backup(h.log, req, backupFile, nil, nil) + h.backupper.Backup(h.log, req, backupFile, tc.actions, nil) assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...) }) @@ -2972,3 +3016,534 @@ 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 + actions []biav2.BackupItemAction + }{ + { + 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: "some namespaced resources + only related cluster resources 3", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods", "persistentvolumeclaims").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Volumes(builder.ForVolume("foo").PersistentVolumeClaimSource("test-1").Result()).Result(), + builder.ForPod("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVCs( + builder.ForPersistentVolumeClaim("foo", "test-1").VolumeName("test1").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + }, + want: []string{ + "resources/persistentvolumeclaims/namespaces/foo/test-1.json", + "resources/persistentvolumeclaims/v1-preferredversion/namespaces/foo/test-1.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil + }, + }, + }, + }, + { + name: "some namespaced resources + only related cluster resources 4", + backup: defaultBackup().IncludedNamespaces("foo").ExcludedNamespacedResources("deployments").Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("foo", "bar").Volumes(builder.ForVolume("foo").PersistentVolumeClaimSource("test-1").Result()).Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVCs( + builder.ForPersistentVolumeClaim("foo", "test-1").VolumeName("test1").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + }, + want: []string{ + "resources/persistentvolumeclaims/namespaces/foo/test-1.json", + "resources/persistentvolumeclaims/v1-preferredversion/namespaces/foo/test-1.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil + }, + }, + }, + }, + { + name: "some namespaced resources + some additional cluster resources 3", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods").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(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + ), + }, + want: []string{ + "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/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "some namespaced resources + some additional cluster resources 4", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods").IncludedClusterScopeResources("*").ExcludedClusterScopeResources("customresourcedefinitions.apiextensions.k8s.io").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/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/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "some namespaced resources + all cluster resources 1", + backup: defaultBackup().IncludedNamespaces("foo").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.PVCs( + builder.ForPersistentVolumeClaim("foo", "test-1").VolumeName("test1").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + }, + want: []string{ + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/persistentvolumeclaims/namespaces/foo/test-1.json", + "resources/persistentvolumeclaims/v1-preferredversion/namespaces/foo/test-1.json", + "resources/persistentvolumes/cluster/test1.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test1.json", + "resources/persistentvolumes/cluster/test2.json", + "resources/persistentvolumes/v1-preferredversion/cluster/test2.json", + "resources/pods/namespaces/foo/bar.json", + "resources/pods/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "some namespaced resources + all cluster resources 2", + backup: defaultBackup().IncludedNamespacedResources("pods").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/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", + }, + }, + { + name: "some namespaced resources + all cluster resources 3", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods").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/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/v1-preferredversion/namespaces/foo/bar.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, tc.actions, nil) + + assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...) + }) + } +} diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 54d68db1bc..2cb5c8997b 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -98,17 +98,34 @@ 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 && + (len(ib.backupRequest.Backup.Spec.ExcludedClusterScopeResources) > 0 || len(ib.backupRequest.Spec.ExcludedClusterScopeResources) > 0) && + !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 { @@ -592,3 +609,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..35a14084b5 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -183,29 +183,58 @@ 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 excluded by cluster resource filter: include: %v, exclude: %v", + r.backupRequest.Backup.Spec.IncludedClusterScopeResources, + r.backupRequest.Backup.Spec.ExcludedClusterScopeResources) 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 bcccd53739..7b420778bb 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -133,19 +133,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..60ace5d672 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 = emptyDisplay + } 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 = "*" + } 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) + } + }) + } +}