From a1e4f5448812295d97fc0aa491d271e91f8f7ba7 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 | 64 +- internal/hook/item_hook_handler_test.go | 4 +- pkg/apis/velero/v1/backup_types.go | 30 + pkg/apis/velero/v1/zz_generated.deepcopy.go | 20 + pkg/backup/backup.go | 30 +- pkg/backup/backup_test.go | 936 +++++++++++++++++- pkg/backup/item_backupper.go | 17 +- pkg/backup/item_collector.go | 22 +- pkg/backup/request.go | 2 +- pkg/builder/backup_builder.go | 24 + pkg/cmd/cli/backup/create.go | 81 +- pkg/cmd/cli/schedule/create.go | 32 +- pkg/cmd/util/output/backup_describer.go | 54 +- pkg/controller/backup_controller.go | 31 + pkg/controller/backup_controller_test.go | 6 + pkg/test/api_server.go | 1 + pkg/test/resources.go | 10 + pkg/util/collections/includes_excludes.go | 310 +++++- .../collections/includes_excludes_test.go | 630 ++++++++++++ site/content/docs/main/api-types/backup.md | 20 + site/content/docs/main/api-types/schedule.md | 20 + site/content/docs/main/manual-testing.md | 6 + site/content/docs/main/resource-filtering.md | 66 +- 27 files changed, 2351 insertions(+), 137 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 823efd976c..559d07e79d 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 22405ad12d..2583b69079 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 43eb169af2..bf7f237966 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=]s\x1c9n\xef\xfa\x15(\xe5\xc1wU\x9aѺ\xf2\x90\x94\u07bc\xb2]Q\xed\xc6VY:\xdfC\x92\aN7f\x86+6\xd9G\xb2G\x9eK忧\b\xb2\xbf\xd9ݜ\xb1t\xb7we\xbe캇\x04A\x00\x04\x01\x10\x84.V\xab\xd5\x05+\xf9WԆ+y\x03\xac\xe4\xf8͢t\xff2\xeb\xa7\u007f7k\xae\xae\x0fo/\x9e\xb8\xcco\xe0\xb62V\x15_ШJg\xf8\x1e\xb7\\r˕\xbc(в\x9cYvs\x01\xc0\xa4T\x96\xb9\xcf\xc6\xfd\x13 S\xd2j%\x04\xea\xd5\x0e\xe5\xfa\xa9\xda\xe0\xa6\xe2\"GM\xc0\xeb\xa9\x0f?\xad\xffm\xfd\xd3\x05@\xa6\x91\x86?\xf2\x02\x8deEy\x03\xb2\x12\xe2\x02@\xb2\x02o`ò\xa7\xaa4\xeb\x03\n\xd4j\xcdՅ)1ss\xed\xb4\xaa\xca\x1bh\u007f\xf0C\x02\x1e~\r?\xd3h\xfa \xb8\xb1\xbft>\xfeʍ\xa5\x1fJQi&\x9a\x99\xe8\x9b\xe1rW\t\xa6\xeb\xaf\x17\x00&S%\xde\xc0'7E\xc92\xcc/\x00\xc2rh\xcaU@\xf8\xf0\xd6C\xc8\xf6X0\x8f\v\x80*Q\xbe\xbb\xbf\xfb\xfa\xaf\x0f\xbd\xcf\x009\x9aL\xf3\xd2\x12Qѽ#\x90-\v\x1c\xc1\xb8ܢ\xf6L\xdcjU\x10L\x94\xb9\x97>\x12]\xc1Q\x0e\xc9o\xaaM\xc1\xad\xe3\xfb_*4N\xc8\xd5\x1anI\xc5\xc0\x06\xa1*s'\x99k\xb8\x93p\xcb\n\x14\xb7\xcc\xe0\xab3\xc0Qڬ\x1ca\xd3X\xd0Վ\xc3Ξj\x9d\x1fj]6\xc1/\xaf\x10\x1eJ\xccz\x1bƍ\xe2[\x9eѶ\x80\xadҭ\xbe\xf0\xeaj\xdd\x03\x19߲\xaee\x86?HV\x9a\xbd\xb2N\xff\xaa\xca\x0e{\f\x10\xba}\xb8\x1b\f\xa8\x91\t\xa8\x91Z\xa9\f\xe6n\x9f=3n\x1dz#\x98\xe0\x00\xc1W\xd205<\xd24\x95\x01[iI\xbb\xf4\v\xb2\xfc\xf8\xa8\xfed\x10\U0008a135>+\xae`\x83[\xa51\x02W\xa3\x1b\xef:\xa3֎0\x86PR\x95]\xc3\xe3\x1e\x1d\x19Y%l\x90{n\xe0\xedOPpYY\\\x8f\xa0M0\xd8\x13\x85\xc0\xf8\x15\x98G\xf5\xd1xV-\x90\xef\xfdİ\x0e\x11\x9f\xf7h\xf7\xa8\xa1T\xb5\n\x8e\xacr\xcb\x05\x829\x1a\x8bE\xe0x\xad\xf86\x81\xfa$\x14B\x04\x10\x066\xc7\x1a\xe7\xf1:\xddy\xcb6\x02o\xc0\xeaj<\x9d'\xc3F)\x81l\xa8\x84\x87t\xf8\x82\xc6\xf2l\x81\n\x97C2\xf8Q\x11\"\xe8\xf0\x03\xad-B\x87M+k\x96=!\xb0\x9a\x1a\xeep\x10\xa2C\xc4\x1e\x05\xe0\xbf%\xbcw\x9a+s\xfad\x8c-\x04\xcd\xc5Q\x90\xb6\x94\n\x84\x92;\xd4~6w*)K_^\x85\x9c\x1e\xf13\x88\xe9\a\xd2\xf6\x92^m;:tc\xc8\t\xc2\xed\u06dd\xf7\xf0\x1a\xf6p\x03w\xd2\xf9-\x81\x1et#\u09db?\x1f\xfa\xad\xa8\f\x05\x89\xa5\x92+:*ױ\x99<\xb1\x13A*\xdd\xe3\xc8\x18\xb5fR?a\"\xd8Gw\x92\xf8\xf1\xfe\x8eC\xb0\f\xf3:\xc6I\x91yfq\xc73(P\xef\xe6\x0e\x8en+\x9d~OC!Q\xeb\xfav\xa2\x84\xa5\x1d\xedu\v\xaa;\x1a\x82뷕۹\t\xbdjf/v\x9d\b\xc8Ow]^\x11\x1d\xb1d\u007f,R\x97\xe59ݥ2q\u007f\x82\xc6?\x81\x17\xe3\xb3\xdf#\xe6OȂQ\x88\xf4\u007f\xdd1G\x02\xfd\u007fP2\xae\x13\xf6\xf0;\xba\x1a\x15\xd8\x1b\x1b\xa2X\xddi\xdc\f܀\xe3\uf049\xf1UOdq\xca\xe9\x16\x14\xfe Wۑ\xc5r\x05\xcf{e\xfc\x99J\xa1\xd9E\x90\xdc\xc0\xe5\x13\x1e/\xafFz\xe0\xf2N^\xfa\x03\xfedu\xd3X\vJ\x8a#\\\xd2\xd8\xcb\xef1\x82\x12%1\xa9\x1b]A\xa7\x9a\xcaΗ\xac-\x017\xb0\xb9wuf\xee\x1c\xd6IrX*\x13\xb9M\x9a@\xe5^\x19\xeb#\x8b=\xb3\xf4\x94(\x16x\x19\n\xd1+`[\u007f\xf3\xadt}\xa7\xe9\xd4\xde \xe0\xea\xb8f\xe65\xaccc\x13\x11\xf3@\x9dcu\xd9\xee`\xafO/\xfdE'M\xc222.\x16\xe1\x96Zeh̼\x88$h\xeb\x85 a\x13 dށ\xf1\x17\x86\xf3Aɺ\xa5\x1b\xa4\x8eH'\x9a\xf2\x1f\xbeu\xa2\x97n\xf3\xbb\u007f/\tߩx\x01\xed٢`Û\xf1$\x14o\xfd\xc8z\x9b\x04@\xde5л\x8a\xb6z\xba\x05\x19\x04\xe9\xf7pL\x17\\\xde\xd1\x04\xf0\xf6ŏ\xf5FI\xe29\x86\xfbm=\xb6%z\xf3\x81vo\xaaE\xa4(r\xaf\xb1ǹq\x9c\xdb\x19\x8a\x89 \xa5\xb2\xddp\x82\x83[\xaa\xfc\x8d\x81-\xd7\xc6v\x11M\x15\x8aja\xf7\xb7\xedT\xcfI~\xd0\xfa,\xc7\xe9\xb3\x1f\xd9\td\xed\xd5s\x9d_0y\x15\x1bkt)\x84\xc0\xb7\xc0-\xa0\xccT%)\xfc\xe2\xb6:M\xe1Y\xe0\x15t2\xc9\xd2\x14\x84k(\xab\"\x8d\x00+\x92:.g\xe34\xdd\xee\x1f\x19\x17\xaf\xc16;\x95\x86\x11k=\xb6\xd5\xf9\x18\xddD\x91\x82}\xe3EU\x00+\x1c\xe9Sݞ\xad\xcf\xe2\xe8q\xbc\xc9\xe5 \xb8t\x8cX\xe56U)Ц\xeeH\x9f\xb5ᶉ\xe196\as\x90\x02%\x81\xc1\x96q1qy>n'\xd1\xf6\x14_#(\x8b\x97s\"\xd2&_\x11)\x12\x02\xb1\x89\xc6⼶.u\xba\xa9x\xaf1\xcd<[\nJ\xd7\xe6Y\xa9\xb9\x93%\xf5\xd2\x16Z\x101&\x8f?L\xb4Q\xfba\xa2-\xb4\x1f&\xdad\xfba\xa2-\xb7\x1f&Zh?L\xb4\xba\xfd0\xd1~\x98hs\xdd\xe6\xb4\xf5\x12F\xfe\xc5\xc9ď\x8bX$\\Oϡ8\x03?dS\xdc\xfa\xd7'\xa9\x19\x96w\xf1Q\x91\xac\xe0\xf0\xaceE/rb\x12\xd0&]\xb4GI\x93r\xe96H-\xde>\x81~!\t\xf3;\xb2oS\x12~\x96\xd2|\xfay\xa6M\x9aM\x9dh\xaa\xeaI\"t\xa8_\xf68\xb3\xb7\x9bC\xd2\xcf\xd7!;\xb7\xc6\xf4\uf783\x9a\x90\x8a\xb3\x90\x803\x9f\x98;G\xaf\x81\xeb\xd1'\x98\xee%\x8c\xfe~\xe8e\xb1\xf8\\\x06QN{\xc4q\x17\x19\xb2\xf4\x8c#\xb2\x1e\xf2Q\xccQf{\xad\xa4\xaaLpk\x1c\xf4w\xe4]\x85\x1b\x17\xca6\x89\xbd\xbd\x88\xc0|\v{UE\x12Cgh\xb7\x90&4\x9d\x1c\x14\xae\xc2в\xc3\xdbu\xff\x17\xabB\xaa\x10\xecɊi\xde\xffJK&:;\x85\xa8\x9f\"4qF\x9dzg\x96\x9e)\x9d\x9e$4\x9f\xd5sJj\xd00\xf1g\x12\xe8rBP\x8a뼐\xfcsF\xcaOb\xba\xe7w\xdf\f\xa6$\xf5\x9c\x95ʳ\x98\x11\x99\x98\xc0\xd3O͙\ayB\xdaN\x12q\x96StNN\xcc\t\x890\xb3\xebHNlj$\xda\xcc\x02\x9eL\u0099K\xafY\bˍSoғjfAS\xc2\xcdr*\xcd\xcb%̾\x84\x130\xadj\x16\xd3a\x16\x9d\x84y\xfc\x16\x13^NIsY\xa4ؙ)-M\xca\xcaļ\xa7&\xb2\xf4\x13U&\x80\xa6\xa4\xafL\xa4\xa7L@\x9cMZIMJ\x99\x80\xbdp\xec\xceJ\xc9̏\xf1\x97аx\xbe\x89\xbf\x95D\x9d\xbb0\xa5{\xe6⒋\xf2y\xd0\xdd\U00072d9a\xe6\xcdϘ\xe5\xc9\xed\xfet\U000f3a04奠\xfb\x8c\x03ϣ^\xb3\xdd\xe3\xb1y\xd7\xfa\x9b\xa2w^\x9b#A\xfa\xfc\xa5\x11\xcf\xf5\xc0\x88f\x06\x9eQ\b`1\xe1\x1a\xad<\xf3\x8f\xf93\xb5B\xa7\xf3݆\v/vÛ\xff+/\xc1\xf4\x94-\x16\xf2\xb5{,\x1c\x94\xfa\xe9\xef\t\xfe\u05fc\x81\xe8mY\xfa\xf6\x97\n\xf5\x11\xd4\x01uk1,<\xa4\xf0\x1b\xcd8\xef\xa6\xde\xfaA\u007f\xf8\x12\x12\x03ù\xddp\xf0N\xfa#,\nv\x80#\xc1q{^4\xbcv\xea\xcd\xf9\x01\x13]\xe3\x91\x1fՌ\x8e\xfc\xbed{\xa6\xbeBx]\xd7\xe1t\xe7a\xf1\xd8~\x15\a\xe2|\x17b\x06dꫂ\xb4\x1b\xb8\xc5W\x04\xaf\xe5J,9\x13\xc9VT\xda+\x81\xd7x\x1dp«\x80\x13\x9c\x8a\xd3܊d2\xa5d\xff\xbf\x8as\xf1\x8a\xee\xc5k8\x18\xe7\xb9\x18\v \aY\xfd)\xf9\xfaI\xb7\xcb\xc9\x17,)\xb7\xc3\xcbw \xf3y\xf8\t\xf9\xf7\t\xb7#K\x98&\xe4ٟ\x96_\x9f@\xc3Wr>^\xc9\xfdx\r\a\xe4u]\x90E'dQrf\u007f>;\xbc\xaet\x8ez\xf66\"U\xd4f\x85l\xe0/\xf4\xe7\x1c\xc4\xe6\xeb\x127\xaeW\xcf4\x8d\x85\x94\x9b\xe7\xaf\x19\xfc\xc2e\xee\xf9ᄪs\x8e\xf7\xaeHZ\xc3\"\x1e\xa1o\xad\xb6P\x02\xcc߫\x18,\x99\xa6\xbaq\x9b\xa3\xbf\x9b5k\xf8\xc0\xb2\xfd\x00\xfa>\xea'l\x95.\x98\x85\xcb\xe6R\xea\xda\x03w\xff\xbe\\\x03|Tͭ_\xb7\xa4\x84\xe1E)\x8e\xce\x0f\x88\xc0\xbc\xec\x828O \xa2\xc2dB\xbd\xaaP\xc0g\xc1\xf7{\xe8\xf7\x8e\xdcfֵ\x8bj\xb8&\xee\xf80y\x84\xfb\xafd\x9dPŔ\xac\xad\x1e\x13\xec\x8f\xda\xfb\x1b\x16\x97\xf9\xf9\xe5\xef5\x8dU\x9a\xed\xf0W\xe5K\x90-Ѡ\u07fbW\u007f.h\x8d:Ϡ~\x86\x12;MC1\xb4\x01\xb06}hT\b\xcba\x19S'3;\xd1Z\xb1\xb0\x98\xc7\xc7_\xfd\x02,/p\xfd\xbe\xf2\x17\\\xab\x92i\x83\x8e\x9a\xf5\xc2\xfc\xa0\x8d\xfb߽z\x8eE9TX\xf3\xcfC\xbc5R\x8a\x12]U\x9f\x84\xfd\xa1WP\xad&ђ\x88~\x8d\x8f\xea\xb8h\x1d&\xf9\xdd\x1e\x95\xd0)8\x9d\x9a\x92\x14\xbc\xa0'F/[\xf1hJ\u007fOUݣJs\xcbu\xf7|A\xbaPe3$\xbaU\x9a\xca\x15\x85buT\xde\xe7\xac\xd2{\x9b沴\xb9\x8a5\xef\xacu\xb6f\xcc\xfe\x8a\xa071\xb6\xd6\xeaVY&@Vņ\x8e\x89\x98Ji\x86\xd05\xee\xec\xfd\xadO+\x9aa\x9c'5\x97\x16w\xa3\xb8Ul\xad\xb7!/霵6c\xd3\xd7j\xaa,Cc\xb6\x95\x10\xc7&'ꔅG`\xbe\x14)>2.\u03a2\x83\x1f8A\x04\xbf\xb6I=\x9a\xc4\xe6P\xfb\x02e^o\xde\xd1Q\xe0\x1ae\n\x9eF\x87\xc0\x82^\xe1\xdfy\x02\u070eGPyW\x9dwR\x16\x9a\x02\x80\xcf̴l\x8eY\x94-8?\x92\\:\a\rs\xc0\x03JP\x922ݨ\x0e\x96/A<\x1c\x13\x81څ\x12R\xe9\xaaR(\x96\xd7\a\\m\xbc\x85\xb2\xb5\x8ft|\xeb\x03\xea7f\x06fS\xd21B\x84\xb1dzk\xea\x06rfq\x15\x05\x9at\xf4Gumfx_\xcf'+\xadۇ\xbb\xa9\x91\x93\x12\\w\x88\xf1oT@\xf4;\x95\xd4xe\xa9*j\xbc\xb2%\x05\xd5SG\x91ŵ\n\xeaŗI{u\xb1\xda!u\xf2\x16\x00\xa5\x04\xd7\x055}Np\x81ư]]\xe6\xf0\xd9\x19`;\x94H\xf6\u007f,\xf0\xee#$m\x0ei\xbfȟ\x0f\xe5\xb2\xccV,LP\xa7\xc0tz\xbd\x89)`\xa1v\xbe\x14*\xaf\vAז\xe9\x894\xf9Vr\x9db\xc9~h::\xda\xd0-\f1\xa2-܍\x82\xef\xb83\x03\x1d\x93vLo\xd8\x0eW\x99\x12\x02I\u05ce\xf1z\xcd\xcd\x1a2u\xbf 3\x8bK\xfb\xd8\xed\x1bB~\x9e۾F\x0e\xf3\xd9`T\xc7\xd9r\x8dma\xf4\x11B\x8a&>\xc9r\xf5T\x88\x96\x10\x1fc\xda\xed[o\xb0\xa0W\x83#\x19*\x8a_\x05_(\x1e\xd6)\xd8oJ_A\xc1\xa5\xfb\x8fs{)&W\x0f>\t\u007f\xaa^\xb9\x80\xf7\xbd\xeb\xd3<\x98\xe8ؑXo\x88)O-\x9e$\xbf\x82O8v,|\xde;\xe6\x14\x85\x8e\xd5Mw]\xee\xe4\xbdV;\x8df\xbc\xabV\xf0g\xc6-\x97\xbb\x8fJߋj\xc7eko\x9c\xd4\xf9\x9ei˙\x10G\x8fO\fQ.\x99\xe0\u007f\x8dq\xa7\xfb\xe32\xa0F\xddF~K@c\xea\x87\xf7\xe8\x8e\xdaIw#.\b\x81\xaeK\xb2\x10\xba\xb5Q3.\xbd\xecR\x8e\xfdFU\xb6\xa7\xfcZ\xe5\x19\x11\xe4z\xce5|R\x16\xeb\xcb\x18އ\xe9\x8e\v4v\x85ۭ\xd2\xd6\a\xe9V+\xe0\xdb\xe0\xbeĢ2\x8c\v\xbaL\xf6\x85ׁ\xdb6\x9f\xa6\xddo\x14\x99Ф6\xa8:]\xc1\x8e>ߚe\x99\xf3\x8e\xf1\xdaX&\"\x1a\xf9\xbb\xd2\x17\xc9Ot\xfb\x05\xf3?E,\xc7\x11\xc1\xef\xba\xfd\x9b\xa2\x17\xcdyL\xe0<\xe5\xe8=\x8c?\x8d\xa2g3\xd0+\t\x94𬹵\xee\x04\xe8\u07b6\x83u:_\b0N\vN\x948\x9d;\x8b\xe8wg-\xdcMG\xff\xfb\x01\x89\xa6\xf3\x94\xb1\x11\x16\xa7\x1c[6D\x82\x89e\xf9\xf7\x96\xdc\xd4c\x1d+\xb3=\x93;'TZU\xbb}-\x97\x13g\xf9T\xf0\xbcrHAI\x1a\xc2\xd47\x9d\xb6Ҳ\x13=\rw\x9fy\a]\x96=Mb\x1ans\xea?\xfeq\x1d\x8a\x96\xae\xb6Z\x15\xab\xc0\v\xba\xa0\xbc\n\x11M͕s\xd8\xed>Jr\xf0U\xd5Cu@\x12\x83\xb2D\t\xcc\x04|\x12\x1e\x83γu.\xa4h\x99\xb6\xa9~\xd0C\xaf\xf3\x82\vD\x90\xe3\xf8>\x84\x88\xad\u007f\x14{;\xfc3,W`\xb8\xac\xff\ue20f\a{Q0\xce3\xd2H\xc1\xb5\xe8\xdd\xf3ȧ\xe9y0}\xf4\xff\xb6\xceˡ9\x13?\xa4X\xc1_\a\xdd\a/\v\xa8\xbc~\xd3%X\xae\x11z\xfc\x81o\xfdux\xe6\xb0\xfe\xe3\xdf\xfd\xc5\xc0!\xc9\xcaz3k`\x91\xed\xd4XJ\v\xc5\xf4\xef\x05:\xcb\xc7 \xf6m\xb77'\x19\xe9\x87\xf3\xdcΗ\xf49\xeb?\x89\xf32\x9e\xd8\xe1(Ǫ\x19\xf2\xbd\x1b\xe9\xd6\"\xe2\xf3\x01\xf5\x01%\x94\"\xa8\xe0\xc8,w\x94!\xa8\x93\xd2Xx\x8e\aŷ\xf5ԷB\xc1\x98\a\xa1`{\n8\x0f\xe7i\xec-\xd92\xbc\x05-\xab\xe1p\x8e\f[!\x18\x92\xbe\x12\xee\xd3\xe13*M\xb3\x19*\\\xf7\xc9\xe0zE\x88 \xfd\x0fvn\x11:l\x1bY\xd3\xe4\t\x81\x04j\x18\xe3\xc0X\x8b\x88\x1d\n\xc0\u007fqxg4Wf\xf4\xc9\x10[\xf0\x9a\x8b\"\xb3ڒ\v`\x82\xefQ\xbaьUx\xa6\x8c\x99\xe1%\x16\xe2\x889\x18\x85!\x91\x19\xcd\a\xbb\xca\xe8\x92!\x9d\x01\x8c,\x8f\xca\x00\xe5J#\xc9\xd7חd\x10~\xcdX\x95c~\xe7\\\x81\a\xe3\xc4\x04\x97n\xa0\x0ez|z?\xd5\xd7[\x11F3\xeb\u007fԮ\x86i\x14]\x97\xc1\x96\x9cJt~\x9a\xe1\x9aG\xaf1\x12^\u007f\xc1f\a\n\xb5ir\xfd\xcf\xd77\x86\x99\x11\xa0\x9dA\xbbC( \x12\xeb\xd9\x0fyA5\x16\x91\xf9O.\xfaDF\x10)\xc9i\x84\r\x8d\xf7\xb8\x94\t\x91\x9e=\x16\xf0\xd0\xe2\xf7eBo\xd8?\x16\x1b\x16S_\xd9\x00\x87PnHoB\x8a\x0e\xe5\x95u\xd3#\xd31T0^\f\xe5\x0e\x9e\xf5\xc0\x1bJ\u007f/tY*\x94c\xa2Xˀ\x971\x13\xbb\x90\xa8\xbd\xfe\x8e\x89r\x10\xe2i\x8e\x10\xffn\xda4^0d6f\x86-\x1eȑ\n\xe9\xa7\xde\xd8f\xfc\x8aY\xa51fˈ\x86\x9c\xeev(\r\x9c\xf2@\x14*\x17\b\x8d\x13dܱ\x83\xd6j\x8f\xfe؛G\xc3H#\xa9v\xe6c\xa8\x1b\xe3ܷ2\xe11\x88\x1a\xdf\xcbZ\xb3\x9c\x1ei^\x11f\r\x1bᙛ\x0f\xa9\xf1\x8a\x19\xc8\t&\x0fpvf3`n8\xd1q\x94\x05G\x10\x12\n\x13\x1d\f\x9b\xf6\xe3\x99\xe6\x19\x9b\xf6\x96\x18\xdb/\x9c\x88ʊ\xa1\xf2C9g\xab\xd1\x017\xa3\xa0k\x8e\xb8Ȓ\x91-2P\xc80\xd3B\xc6\xc91\xc7d\xf7\xa4\xe8\xb5\x11*F4\\ףo&6\x01\x12\xac\xbb\u007f\xa0\xd9\xc1\xb9NF\x82,\x1c\xc8\x05*\xbb\xcaIY\xb2\xd3\xd8$a\x8e\xf3~\x90\xa9\x85\xde<3K\xbe\x0f/\xb6\xf8\x9b'A76ό\x96\xecR\xb6\x16\a\xd0br\xda\xff\x98\x84\rj\xff\f\xa1\xdd\f\xba^Vh\rI)*\xeb\x01aQ\xea\xd3\rP\x1d\xde\xceA4\x81G3\xfe\x1f\x981\xcb%~\xd3\xefyQ\x89\x9f\xe4\xca\x1cDÕz\xf8? S\xac\xb1x\xf0\xb6\"\x99!\xbf\xb6{\xdd\x00\xdd\xd5\f\xc9o`G\x99F\xd9\xe3\xcc7\xad\x97K\x10#\xc5ޙ\xa7 :;\xbc\xffj#\x93\x13F\xb7 s\x86\\\xec\x06\x1e\xcb\r<\x1f\x84r6\xd5nT̂\xa4\n\xae\x9f\xf0t}3\xd0\x03\xd7\x1b~\xed\f\xfcbuS{\v\x82\xb3\x13\\۾\xd7\xdf\xe2\x04%JbR3[\x90\x91\xea*\x9bX2x\x02\xa6c]\x85`\xdc\xdc)\xac\x93\xe4\xb0\x14*\xb2\xb7:\x82ʽP\xdae\x16;n\xe9\x92,\x168\x19\xf2\xd9+ ;W\a\"d\xd8\xe17j\xaf\x97p5\\S\xd3\x1a\xd6e\xf3}F\xcc\x015\x81\xd5u\xb3\x82\x9d>\xbdv\x9b\tv\x10\x92Y\xe7b\x16n)E\x86JM\x8bH\x82\xb6\x9eI\x12\xd6\tB\xe2\x02\x18\xb7}>\x9d\x94\fO\xbaCj\x88\xb4Е\u007f\xff\xb5\x95\xbd4\x8b\xdf\xfc='|K\xf1\x02\xbbf\x8b\x82\xf4\xebD\x92P\xbcs=\xc32\xf1\x80\\h \xf7\x95]\xea\xe9\x1e\xa4\x17\xa4\xef\xc1L\x17\x94o\xec\x00\xf0\xe6\xe2f\xbdV\x92x\x8e\xe3~\x17\xfa6D\xaf_\xd8՛\xea\x11\t\x9b\xb9\x97\xd8\xe1\xdc0\xcfm\x1c\xc5D\x90\\\xe8v:\xc1\xc0-E\xfeJ\xc1\x8eJ\xa5ۈ\xa6\nE5\xb3\xfa\x9bgi\xe4\xc4\xdfKyV\xe0\xf4\xc9\xf5l%\xb2\x0e\xe29Tی\x16&\xc4\x1e\xbb)\x84@w@5 \xcfD\xc5m\xfa\xc5,u;\x84c\x81S\xd0\xc9$KS\x10\xe6A^\x15i\x04XY\xa9\xa3|2O\xd3n\xfe\x81\xd0\xd8\xd6\xf0\xf0Y\xc86=V\x94\x14{:l\v\xd5I\xed\xb2\xa9\x82|\xa5EU\x00)\f\xe9SÞ\x9d\xabi\xeap\xbc\xael\xb2p\xad\x19\xd1\xc2,\xaa\x92\xa1N]\x91\xae\x86\xc9,\x13Es\xac\r\xb3\x97\x02\xc1\x81\xc0\x8eP6RJ2|\x16\xd1vI\xac\xe1\x95\xc5傈\xb4\xc1W\x96\x14\t\x89\xd8DgqZ[\x972\xddU\xbc\x97\x98\xe6\x9e\xcd%\xa5\x83{VJjdI\\\xdaC\xf3\"F\xf8釋6x~\xb8h3\xcf\x0f\x17m\xf4\xf9\xe1\xa2\xcd??\\4\xff\xfcp\xd1\xc2\xf3\xc3E\xfb\xe1\xa2M5\x9b\xd2\xd6s\x18\xb9\xf3W#?\xceb\x91\xb0==\x85\xe2\x04|_MየS+,7\xf1^\x91\x1ay_\x04\xbd\xb2շ1\th\x8a.\x1aSR\x97\\\x9a\x05\x12\xc4\xdb\x1d'\x99)\xc2\xfc\x86Z\xf40\xe89\xb5蛩\xbe\x97\xa9E\xf7\xe8\xf5]\xee\xcbT\xa2\x87\xb9\xc7O\xa1D \xfa\x02\x17WnQ \t)v\xb7\x9f\x9e\x8f\x8c\xd8\x1bl\x00\xf7w.\xa1\x1d\x94x%W\x18\x0f*\xbc._\xf6>\xc2\xef\x01\u007f\"\x10\xa92\xc2\xf0\xfd\x91w1Ulj\xd9'SdR\xe1ਉ#\xdbEY\xdd\x02\xb8\xefS\"\x97\xcaaj\xa5\xfb\x14\xbd\x86\x8a\xa5E\xb0\xefu\x05k,>\x95\xde6\xa4\x9d\x11\xdcD\xba̝\x12\x8c\xcc\xc7\x06\xfd\xeaij\x83\x14\\T\xca\xe7\t\f\xf4\xb76]\xe1\xb70m\xf9V\xa2R}\x03\aQE*\xad'h7Sw7^m\xe7\xf7\x96Q\x93\xe3\x9bu\xf7\x17-|\xed\x1d\xe7\xdd1G\x9fJ\x97\x14Zl\xe9\xa7\x13\x1ai\xd5yg\xd7\xe4uk\xeeF\x9c\xbe\xa5\x9b\xd0\xe9G\x0fҫ\xee\xa6\xcb\xe4\x96\xd4\xda\xf5+\xe9F\x81\xceWإ\xe4\xa2f\xaa\xe9Ψ\xa1K\xac\x9f\xfe\xe6\xad\xf6\x94*\xb9\xb3j\xe3fK\x8c\x13+⺵n\xd3 \x17\xd4\xc1%\x11g\xbe\xe6mq\xa5\x9b\xaf,\x9b\x9cGr}[\xa4rm\x12\xf0hU\xdbT\xbd\xdaL\x9e{X˖^\xa56\t\xdaV\xb0\xcdצ]\xae\x02\xfd\x12Q\xf5\xb8\xaa\x99\xad/\x9b\x8d\xba\xa7\xf1\x9b\xad [R76K\xb13k\xc4\xea\x1a\xb0\x91q\x97V\x86u+\xbfF\x80\xa6ԃ\x8d\xd4{\x8d@\x9c\xac\x02K\xad\xf2\x1a\x81=cv'\xa5d\xe2\xc7\xf8E\x1b0k\xdf\xd8\xef%Q\xe7NLȎ\xbb8\x17\xa2|\xea57\xbc\f^Ӵ\xfb\x19\xf3<\xa9>,w?\x8b\x8aiZ2\xbbAx\xa4y4\r\xa5\x0fx\xaa\xafM\xf8M\u0603\x93ۓ\x85\xf4\xe9s-\x9e\xeb\x9e\x13M\x14<#c@b\xc25\x98y\xe6\xee\x8a\xc9\xc4\n\x8d\xce7\v\xce_\bᯔ\xb9q\x12lφ\xc6\xf6P\xf4\x01\v\x03%\xdc,\xb1 \xfe\x9av\x10\x9d/k\xdf\xfd\xadBy\x02qD\xd9x\f3'\x93\xdcBS&\xba\tK\xdf\xeb\x0fwCQ\xcfqn\x16\x1c\xbc\xe5΄E\xc1\xf6p\xb4p̚g5\xaf\x8dz3q\xc0H\xd3x*UԽ#\xbf\xcf\xf9\x9e\xa9\xc7z^6tX\x1e<̚\xed\x17\t \xce\x0f!&@\xa6\x1e\xd3I\xdbҞ=\x96\xf3R\xa1\xc4\\0\x91\xecE\xa5\x1d\xbby\x89\xe36\v\x8e\xd9,\b*\x96\x85\x15\xc9dJ9N\xf3\"\xc1\xc5\v\x86\x17/\x11`\x9c\x17b̀\xec\x1d\x93I9\x00\x93T\xae\x91\xbcc\x99Rn1\xbf\xa98}\xb0%\xe1@K\xc2v\xe3\x1c\xa6\t\aW\x96\x1dXI\xa0\xe1\v\x05\x1f/\x14~\xbcD\x00\xf2\xb2!\xc8l\x102+9\x93?\x9f\x9d^\x172G9\xb9\x1b\x91*j\x93B\u058b\x17\xbac\xf6r\xf3\xe1\x065Ӫ\xe3\x9a\xc6R\xca\xf5y\xf2\f~\xa1\xdc\xefn\x1a\xa1j\xd9\xf1\xce\x16I\xe3X\xc43\xf4\x8d\xd7\xe6o\x98t\xfb*\nK\"\xed\xbe\xe7\xf6\xe4\x8a\x1d\xd4\x1aޓ\xecЃ~\x88\xc6\t;!\v\xa2\xe1\xbaޔz퀛\xbf\xaf\xd7\x00\x1fD\xbd\x8d\u07be\xa3EѢd'\x13\aD`^\xb7A\x9c'\x10QaR\xfe:D\u007f?\xdcL\xec\xf7\xd0m\x1d)\x0f\bW\xe3\x05\xb8*\x1e\xf8\x10~\x82\xfb/\xd6;\xb1W\x10e\xcduL\xde\xff\b\xd1_\xff\xb6\xa6\x9f/_(\xa0\xb4\x90d\x8f\xbf\nw\xc3\xe5\x1c\r\xba\xad;כz\xad\x11\nw¹\xae\x985\xf5wm\xf6\x805\xf5x\x83{\x16\r\x961u2\xb1\x12\xb5f3\x93y|\xfc\xd5M@\xd3\x02\xd7\xef*\xb7\xc1\xb5*\x89Th\xa8\x19&\xe6:m\xcd\u007f\x0f\xe29\x96\xe5\x10~\xce?\xf7\xf1\x96hk\xfel\xed\xc7\"쏝\xfb:\x03\x89\xe6D\xf4K\xbcW+Dk1ɭ\xf6\xa8\x84\x8e\xc1i]Yl\x93\x17\xf6\xcc\xdee\xaf\x10\x1b\xd3\xdfc\x97\xbaڋL\xe7\xafuu\xf7\x9d\xfaK\x9c}\xe5h%\xed\xfd_\xfe.T{_\xd6Y7\xbbn\xeb\xcd\xd2z+V\xbd\xd5\xda\xf8\x9a1\xff+\x82\xdeHߠյЄ\x01\xaf\x8a\xad5\x131\x95Rw\xb1۸\x93\xfb\xb7\xaeNo\x82q\x8eԔk\xdc\x0f\xf2V\xb1\xb9\xde\xf9B\xbfs\xe6Z\xf7M\x9f\xab\xaa\xb2\f\x95\xdaU\x8c\x9d\xea\"\xc3%\x13\x8f\xc0\xbc\x14)>\x10\xca\u03a2\x83\xeb8B\x047\xb7Q=\x9a\xc4f_݄<\x0f\x8bw`\n\xcccKo\x97\xd1\xc1\xb3\xa0s\xaf\xfc4\x01\xee\x86=\xec\xed\xe12o\x95,\xd4\xf7\xcb>\x13հ9\xe6Q6\xe0\\O\x1b\xd2\x19h\x98\x03\x1e\x91\x83\xe0\xb6t\xd4^,\xe7n\xb8\xef\xf7\x89@mC\xf1\xb5\xa9U\xc9\x04Ƀ\x81\vΛ\xbf\x15\xfdњoyD\xf9JM\xc0\xaco\f\x8e\x10a(\x99Λ\xba\x85\x9ch\\E\x81&\x99\xfe\xa8\xae\xcd\x14\xed\xea\xf9d\xa5u\xf7\xb0\x19\xeb9*\xc1\xa1A\x8c\u007f\x83\xfb\xa9\xbfQI\rg\x96\xaa\xa2\x863\x9bSP\x1du\x14\x99\\\xa3\xa0.>M\xbbVg\xaf\x0f\xb5\x8d\x9c\a`k\xec\xc3}ͮȾ@\xa5\xc8>\xdc\x1b\xfal\x1c\xb0=r\xb4\xfe\u007f,\xf1\xee2$MQv\xf7\xd6L\x97\xca%\x99\xae\x88\x1f \x94\xc0\xb4Z\xbd\x8a)`&\xf6\xee\xa6m\x1a\xbe3\x10<Ӆ4\xf9ZR\x99\xe2ɾ\xaf\x1b\x1a\xda\xd8]\x18ˈ\xe6\xbb\x10\xc8\xe8\x9e\x1a7\xd00iO\xe4\x96\xecq\x95\t\xc6\xd0\xea\xda!^/\xb9X}\xe9\xfbg$jvj\x1f\xdam}\xca\xcfq\xdb]:E\\5\x98\xfdL\x80\xa6\x12\x9b\xefn\f\x10\x12v\xe0E\x9e\xab\xa3B\xf4\v\x15CL\xdbm\xc3\x02\xf3z\xd5\a\x92\xfe\x83\x157>\x16\x8a\xa7u\n\xf2\x9b\x907PPn\xfe1a\xaf\xcdɅ\u038b\xf0\xb7\xd7\xc1\xce\xe0}o\xda\xd4'\x90Z~$\x86\x051\x16\xa9\xc5O\x9d\xac\xe0#\x0e\x03\vw\x90\x04s\x9b\x85\x8e}\x96\xc34\xd9\xf0{)\xf6\x12\xd5pU\xad\u0bc4j\xca\xf7\x1f\x84\xbcg՞\xf2\xc6\xdfX\xd4\xf8\x9eHM\tc'\x87O\fQ\xca\t\xa3\u007f\x8fq\xa7\xfd\xe3<\xa0Z\xddF~K@c\xec\x87whL\xedh\xb8\x11\x17\x04O\xd79Y\xf0͚\xac\x19\xe5Nv\xed\xa1\x95\xad\xa8tG\xf95\xca3\"\xc8a\xcc5|\x14\x1a\xc3f\f\xed\xc24\xe6\x02\x95^\xe1n'\xa4vI\xba\xd5\n\xe8·/\xb1\xac\f\xa1\xccn&\xbb\xefz\x00\xd5M=M\xb3\xdelfBZ\xb5a\xaf{,\xc8\xc9\x1d` Yf\xa2c|\xad4a\x11\x8d\xfcM\xe5\x8b6N4\xeb\x05\xf3\xbfD<\xc7\x01\xc17\xed\xf6\xf5-2\xb5=\xb6\xe0\x1c\xe5\xec\x013g\x8d\xa2\xb6\x19\xec\xb1#\xe4\xf0,\xa9\xd6\xc6\x02\xb4w\xdbA\x1b\x9d\xcf\x18(\xa3\x05G\xee\f\x9e\xb2E\xf6w\xe3-lƳ\xff݄D\xddx\xcc\xd9\xf0\x93\x13\x86-[K\x82\x91i\xb9\xeaz\xaaB_\xc3\xca\xec@\xf8\xde\b\x95\x14\xd5\xfe\x10\xe4rĖ\x8f%\xcf+\x83\x14\x94VC\xa8\xb0ө+\xc9[\xd9\xd3\xfa\xe0D\x83.ɞF1\xf5\xbb9\xe1\xdbR\xaf\xfd-\xc0\xab\x9d\x14\xc5\xca\xf3\xc2nP\xde\xf8\x8c\xa6\xa4\xc2\x04\xec\xfa\x10%9\xb8\x8fv\xf8\xeb6\xad\x18\x94%r \xca\xe3\x93p\xbaz\x9a\xadS)EM\xa4N\x8d\x83\x1e:\x8dgB \v9\x8e\xef\x83\xcfغS\xe6w\xfd\xaf|݀\xa2<|\xd6\xca僝((\x13\x19I\xb4ɵ\xe8\xde\xf3 \xa6\xe9D0]\xf4\u007f\xdf\xe0\xe5X\xdb\xc4\xf7)^\xf0\x97^\xf3\xde\xc9\x02\xfb\xf5\x96\xba\x89\xf7\\#\xf4\xf8\x13ݹ\xed\xf0\xcc`\xfd\xe7\xff\xf7\x13\x03\xc7$/\xebդ\x83e}\xa7\xdaS\x9a\xf9V\xcb=C\xe3\xf9(Į\xef\xf6j\x91\x93~\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ܦ\x13\xbdϧ\xe8\xf2\xef\xe0_\xaa,\x8d]9$5\xb7d\xed\xc3V\x1cǵ\xe3\xec%\x95\x03\x83z$\xb2\b\b\xdd\xccz\xf3\xe9S\r\xd2\xfc\xd1hfׇp\x134\xcd\xe3\xf1\xfa\x81\x16UU-T0\xf7\x18\xc9x\xb7\x02\x15\f~et\xf2E\xf5ÏT\x1b\xbfܽ[<\x18\u05ec\xe0&\x11\xfb\xfe\x0eɧ\xa8\xf1=n\x8d3l\xbc[\xf4ȪQ\xacV\v\x00\xe5\x9cg%\xdd$\x9f\x00\xda;\x8e\xdeZ\x8cU\x8b\xae~H\x1b\xdc$c\x1b\x8c9\xf9\xb8\xf4\xeem\xfdC\xfdv\x01\xa0#\xe6\xe9_L\x8fĪ\x0f+p\xc9\xda\x05\x80S=\xae\xa0\xf1\x8f\xcez\xd5D\xfc;!1\xd5;\xb4\x18}m\xfc\x82\x02jY\xb4\x8d>\x85\x15\x1c\x06\xca\xdc\x01P\xd9\xcc\xfb!\xcd]I\x93G\xac!\xfeen\xf4\xa3\x19\"\x82MQ\xd9s\x10y\x90\x8ck\x93U\xf1lx\x01@\xda\a\\\xc1'\x81\x11\x94\xc6f\x010\xec=ê\x86\xdd\xedޕT\xba\xc3^\x15\xbc\x00>\xa0\xfb\xe9\xf3\xed\xfd\xf7\xeb\x93n\x80\x06IG\x13838\xc1\f\x86@\xc1\x80\x00\xd8\xefA\x81r\xa0\"\x9b\xad\xd2\f\xdb\xe8{\xd8(\xfd\x90\xc2>+\x80\xdf\xfc\x85\x9a\x81\xd8G\xd5\xe2\x1b\xa0\xa4;P\x92\xaf\x84\x82\xf5-l\x8d\xc5z?)D\x1f0\xb2\x19Y.\xedH\\G\xbd\x13\xe0\xafeo%\n\x1aQ\x15\x12p\x87#?\xd8\ft\x80\xdf\x02w\x86 b\x88H\xe8\x8a\xceN\x12\x83\x04)7젆5FI\x03\xd4\xf9d\x1b\x11\xe3\x0e#CD\xed[g\xfe\xd9\xe7&aH\x16\xb5\x8aG9\x1c\x9aq\x8c\xd1)\v;e\x13\xbe\x01\xe5\x1a\xe8\xd5\x13D\xcc<%w\x94/\x87P\r\xbf\xfa\x88`\xdc֯\xa0c\x0e\xb4Z.[\xc3cQi\xdf\xf7\xc9\x19~Z\xe6\xfa0\x9b\xc4>Ҳ\xc1\x1d\xda%\x99\xb6RQw\x86Qs\x8a\xb8T\xc1T\x19\xba˅U\xf7\xcd\xff\xe2P\x86\xf4\xfa\x04+?\x89̈\xa3q\xed\xd1@\xd6\xfc\x95\x13\x10\xd5\x17\xc1\x94\xa9e\x17\a\xa2\xa5Kع\xfb\xb0\xfe\x02\xe3\xd2\xf90\xa6\xec\x17\xe5\xec'\xd2\xe1\b\x840\xe3\xb6\x18\xcb!f\xe5INtM\xf0\xc6q\xfe\xd0֠\x9b\xd2Oi\xd3\x1b\xa6Q\xccrV5\xdcd\xa7\x81\rB\n\x8dblj\xb8up\xa3z\xb47\x8a\xf0??\x00a\x9a*!\xf6eGpl\x92\xd3\xe0\xc2\xda\xd1\xc0\xe8d\x17\xcekR\xea\xeb\x80ZNO\b\x94\x99fkt.\r\xd8\xfa\b\xeaP\xf9\x03\x81\xf5I\xe6\xf9\xca\xcd\xe0Tl\x91\xa7\xbd\x13,_r\x90,\xffةS\xa3\xf9?\xd6m-^A\x03\x90\xe2\x1e\xdf\xd5g\x19/c\x80Y\xf5\xce\"\x19E,4\b\xafb\x05bRǘΗ\x96\x86.\xf5\xf3\vT\xf0s\xc6\xfcѷW\xc7o\xbcc\x91\xfbՠ{oS\x8fk\xa7\x02u\xfe\x99\xd8[\xc6\xfe\xb7\x80\xb1\\\xa5WC\xc7\x1by\u007fK\x9d\aޡx9^\xde\xc5\x10p\x87\x94\xecEd\x87\xa0\x17\xae\xf7\xa2Mܬo\xbf\x85\x9e\v\xe1W\x0f\xe0BI\x8e-_\xbd\xcf\xebK.\xefQ_2\xa5\xdcG\b\xf2\xa4\x89\x0e\x19\xe9`\x8d\x8f\x86\xbbٌ\x00\x8f\x9d\xd1]\x9e\x98\xc5)\xaeK\xe4\xb5\xc9\x1e\xf6\xed\xf0\xa5\xa6Mę\x02\xa9r\xe1\xcct\v\xf8\xb3\xee\vNti\x81jp\x87\x17\xb9\x19+N\xf4\r~\x96\xe3G\xaau\x8a\x11\x1d\x0fY\xf2\xfd>\x9d\xf0RC\x1b]\xe0\xf7\xbb\x8fϸ\xda\xfbCd~\xc1*\xe3\n\x9a\x10\xb1\"\xd3ʫD\xc6\xc4ײߜ\x93Q\xda\xe9+锨\xd9\x13ů\xc1\x94\x82y\x06\xe2\x87}`1_t\xe5b\x9d\xbe\x03sB\xa4\xfch\xd1j\xfa\\\x92\xb6Ah\xd0\"c\x03\x9b\xa7r\x8b<\x11c\u007f\x8e{\xebc\xafx\x05r\xe1Vlfd$ou\xb5\xb1\xb8\x02\x8e\xe9\x92\xcaf7\x1e:E3ex\xb2\xe7\xcf\x123'\x8c}1^U\x06\\\xf4\xfa\n>\xe1\xe3L\xef\xe7\xe85\x12\xe1y\x19]\xdc\xc9l\x11\x9cu\x92\xbc\x8a\x9a#\x96\x86\xc7\xf6\xd0s(\x19\xa55\x06\xc6\xe6\xd3\xf4\x0f\xe6ի\x93_\x92\xfc\xa9\xbdkL\xf9\xf9\x82?\xfe\\\x94\xac\xd8\u070f\u007f\x1a\xd2\xf9o\x00\x00\x00\xff\xff\xdb\xd9+\xab\xf6\r\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z\xdds\x1b\xb7\x11\u007f\xe7_\xb1\xe3<\xa8\x991\x8f\x89\xdbi;|s\xa4\xa6\xa36\x915\x96\xad\x17\x8f\x1f\xc0\xc3\xf2\x0e\xd1\x1d\x80\x028\xcal&\xff{g\xf1q\xbc\x0f\x90\x944ur/6\xf1\xb1\xf8a\xbfw\xa1\xc5r\xb9\\0-\xee\xd1X\xa1\xe4\x1a\x98\x16\xf8š\xa4_\xb6x\xf8\xbb-\x84Z\xed\xbe_<\b\xc9\xd7p\xd9Y\xa7\xda\xf7hUgJ\xbc\u00ad\x90\xc2\t%\x17-:ƙc\xeb\x05\x00\x93R9FÖ~\x02\x94J:\xa3\x9a\x06ͲBY\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=Msܸrw\xfd\x8a.\xe5\xb0I\x95f\xbc\xae\x1c\x92\xd2͑\xbd\x15\xd5sl\x97\xa5\xe7K\x92\x03\x86\xec\xd1`E\x02|\x008\xf2\xe4\xd5\xfb\xef\xa9n\x00\xfc\x1ar\b\x8e\xa4\xca\xe6\x95p\xd9\x15\ah4\xba\x1b\xfd\x014\xda\x17\xab\xd5\xeaBT\xf2\a\x1a+\xb5\xba\x06QI\xfc\xe9P\xd1_v\xfd\xf8\xafv-\xf5\xbb\xfd\xfb\x8bG\xa9\xf2k\xb8\xa9\xad\xd3\xe5w\xb4\xba6\x19~ĭT\xd2I\xad.Jt\"\x17N\\_\x00\b\xa5\xb4\x13\xf4\xd9ҟ\x00\x99V\xce\xe8\xa2@\xb3z@\xb5~\xac7\xb8\xa9e\x91\xa3a\xe0q\xea\xfd\xaf\xeb\u007fY\xffz\x01\x90\x19\xe4\xe1\xf7\xb2D\xebDY]\x83\xaa\x8b\xe2\x02@\x89\x12\xaf\xc1f;\xcc\xeb\x02\xedz\x8f\x05\x1a\xbd\x96\xfa\xc2V\x98\xd1l\x0fF\xd7\xd55\xb4?\xf8A\x01\x13\xbf\x8a\xbb0\x9e?\x15Һ?\xf5>\u007f\x96\xd6\xf1OUQ\x1bQt\xe6\xe3\xafV\xaa\x87\xba\x10\xa6\xfd~\x01`3]\xe15|\xa1\xa9*\x91a~\x01\x10\x16\xc6S\xaf@\xe49\x93J\x14ߌT\x0e͍.\xea2\x92h\x059\xda\xcc\xc8\xca1)\xee\x9cp\xb5\x05\xbd\x05\xb7\xc3\xee<\xd4~\xb7Z}\x13nw\rk\xcb\xfd\xd6\xd5N\xd8\xf8\xab'\x91\a\x10>\xb9\x03\xe1f\x9d\x91\xeaal\xb6\x0fpc\xb4\x02\xfcY\x19\xb4\x842\xe4\xccY\xf5\x00O;T\xe04\x98Z1*\xff&\xb2Ǻ\x1aA\xa4\xc2l=\xc03`\xd2\xff8\x87\xcb\xfd\x0e\xa1\x10ց\x93%\x82\b\x13\u0093\xb0\x8c\xc3V\x1bp;i\xe7iB@z\xd8zt>\x0f?{\x84r\xe10\xa0\xd3\x01\x15\xa5z}$\x91=\x98\x1f\x1e0\x01\x18\x93\xa8\x12\xb5e\xe1hG\u007f\xeb~\xf2\x006Z\x17(\xd4E\xdbi\xff\xde\xcb^\xb6\xc3R\\\x87κB\xf5\xe1\xdb\xed\x8f\u007f\xbe\xeb}\x86\x81,\x05J\x81\xb4 \xe0\ao\f0a\v\x83\xdb\t\a\x06\x89\xf3\xa8\x1c\xf5\xa8\f\xae\"u\xf3\x06$\x806P\xa1\x91:\x97Y\xe4\n\x0f\xb6;]\x179l\x90\x18\xb4n\x06TFWh\x9c\x8c[Ϸ\x8e\xaa\xe9|\x1d`\xfc\v-\xca\xf7\U00092216\x85/l(\xcc\x03\x1d\xfc\xfe\x90\xb6ş\x99\xd4\x03\f\xd4I(Л\xdf1sk\xb8CC`\"֙V{4D\x81L?(\xf9?\rlKR\xefX\x18\x1d\x06}\xd06\xde\xc0J\x14\xb0\x17E\x8dW T\x0e\xa58\x80A\x9a\x05jՁ\xc7]\xec\x1a\xfeC\x1b\x04\xa9\xb6\xfa\x1av\xceU\xf6\xfaݻ\a颊\xcdtY\xd6J\xba\xc3;֖rS;m\xec\xbb\x1c\xf7X\xbc\xb3\xf2a%L\xb6\x93\x0e3W\x1b|'*\xb9b\xd4\x15\xab\xd9u\x99\xffC\xe4\xa8\xfd\xa5\x87\xeb\xd1~\xf3\x8d\x15\xe1\t\x0e\x90F\xf4\x02\xe3\x87\xfaU\xb4\x84\xa6OD\x9d\xef\x9f\xee\xee\xbb\xc2$\xed\x90\xfaL\xf7\x8e\x84\xb5, \x82I\xb5Ű\xa3\xb7F\x97\f\x13U^i\xa9\x1c\xff\x91\x15\x12Ր\xfc\xb6ޔ\xd2\x11\xdf\xffR\xa3uī5ܰ\xdd!9\xac+ځ\xf9\x1an\x15܈\x12\x8b\x1ba\xf1\xd5\x19@\x94\xb6+\"l\x1a\v\xba&s\xd8\xd9S\xad\xf3C4o\x13\xfc\x8a{\xfc\xae¬\xb7eh\x9c\xdcʌ7\x06k\xcfF\x05\f4\xa8o㻖\u007fa55\xfc:\xc0\xc3\xeb\xb28+Z\xb2\x1fn\xc7\x1cn\xcd\x18ɕ\x87F:E\xe9!wǴ`\x87\x12\x01\xca\f&}\xad\x97jߎ`BPu\xeb\t\x1c\x8f\xb8\xca?aY\x91ژA\xf1>t#\x14\x89>y\xe3NE\xc3\x1fլ\x0e\xda\x15\x8e\x94\x1bO\xb7C\xe2\xdb^\xe6A{\x1dq\x15Nr\x96Zf\xe5\x9d\x12\x95\xddiG6N\xd7n\xac\xd7`\x017w\xb7\x83A\x1d\xce\x13VlÙ\xd1NÓ\x90ǜ\xf6\x8d\xe4\xf2\xe6\xee\x16~\x90K\x84\x11&xK\x0e\xae6\x8a\xd5\xf1w\x14\xf9\xe1^\xff\xd9\"\xe45k\xa5h\x97\xaf&\x00opK\x9b\xde \xc1\xa0\x01h\f\xed\x01˨\xe9ڭ\xd9\xe1\xc8q+\xea\xc2\x05%'-\xbc\xff\x15J\xa9j\x87\xc7|\x87Ӽ\xf7Dbp~5\xf6^\xfff=#\x13H\xfaqb\xe8Ȗ\xaat\x0e{\xee7EUY \u0603uX\xc2&@il5s\x85\xf5AQ\x040\x166\x87\x88\xfb\xf8\xba\xc9\v\x17\x9b\x02\xaf\xc1\x99z|\xdaS[w\x8c6\xdf\xd1:\x99%P\xe6rH\x1a?r\x840\x86\u007f\x98 ʀ\x02d\xe4\xc5#9\x9a\x81B\xe4-\x14E\x87\xb8\xf3T\x01\xf8/\x05\x1f\xc9\xc0edv\xae\x839\x93X\xb0\tU\x1a\n\xad\x1e\xd0\xf8\x19\xc9Ux\x92E\xc1[\x1aK\xbd\xef9Y\xddF\xb6\xc5`AF\x12\xb65\x99\x9d5\x90\xecOʈT֡\xc8ח\xaf\xc5<\xfc\x99\x15u\x8ey\x13\xe6\x8c\xea\x92\x01\xe3>\x1d\r\xe2\x80PHE\x9a\x99\xc2/\"\xbaj~\x9d\xa0\x06\xfb\x9a\xc2 \x19\f\x90\xca\xc3$Ґ\xa2\xd9L(ij\xd2a9\x81\xe7\xecN^@5a\x8c8\x9c\xa0Y\f\x9a\x97\x90\xac\x19\x13\\\xb1BfH\xc4j\x1c.\xa6\x1a\x93fb}\xff\x0f\t\xb6\xd3\xfa1\x85H\xffN\xfdZ\xc7\x122>\x9b\x80\r\xee\xc4^jc\x87\xd1\t\xfeĬv\x93\xbbM8\xc8\xe5v\x8b\x86`q@\xdd\xc4ߧ\x88uڬR3\xa7\x19\u007f\xb4\xae\x96\xe9\xc4<\xa6\xc6\xd4R\xd8}\x99\x84\n\x8c8Y=\xd6\r\xb9\xdc˼\x16\x05\xab\t\xa12\xbf>\xd1\xe07\xa5\xdcf\x04\xe2\b\u007f\xaf\x8c\xe2*\x88K=\xafT+$\xb7\xaf\xd4f\xcan\xf9v\ff\x9a\f\x1b\xc1\xce\xe4\x94\v\xd76S\x17h\x03*\xde\xfc\xb5z\xe7\xaa\xe5\x94\x0f\xe8\n\xb1\xc1\x02,\x16\x989m\xa6ɓ\"\x04\xbe\xa5\xea\xcf\tʎhҾ\xbf5\xabD\xdbF\x0e\xd9Nf;o\xacH\xca\x18\x16\xe4\x1a-k\fQU\xc5\xe1Ԣ!E2\xc2dsJ\xa3m\t\xeac\bwJ\x91\xb4-Q\a\xb7mF\x1b\xf7\xa9ވ\xcd\x1b\xd1{h\xaag\t\xfb\xed\xd1\xf0\x97\x17v\"\xb7D\xbb\x86\xdb-`Y\xb9\xc3\x15H\x17\xbf\xa6@%W\xb1\xc5\xe3\xef\x8cq\xe7\xed\x96\xdb\xe1\xe8\x17\xdf-/µ\x06\x8d\xbf\x13\xa6\xb1\xb1\xba\v\xb6j\x11\xc3>wG^\x81\xdc6\f˯(\x86tȾ\xd4\x1c\xa2\x1dGg\x96s/I\xa0T\xdbK\xad\x14.\xdb}j\x8e\x81\x12F\fh5\x04\xe0\xfd\xf2\x18\xc30\x0f\x12@B\xe3T\xf0\t\xa64X\xfa\x93\xd1{\xde\x1f\xed\x17\xf6\x00?|\xf9\x88\xf9\x1c\xc9 ]R\x8f\x16\xf5a\xe0\xe9tQ\xe0\x05&\x81\xec,\x8aݴ&\xc6\xf3\xe7\xdfW \xe0\x11\x0f\u07b3\x1a\r.\xc7\x1a\xb1V4 \r\xf2a<\xab\x91G<0\xa8p\xba\x9e\x04o\x89\xa8\xf8\xf6\x88\x87Ԯ\x03\xa2\x12~\xe1\\\xcfS\x97>\xf0*R\xb6R\xdb\x1a\xa2\x86\xbd\x03N\xa7-\x16\x96)\xa5\xd8\"\xc5\xcf\\vðޕ\xd2#\x1e~\xb1\x9e}\xb4kv\xb2Z@\x01R\xd8`\x91wX\xbcK\xf9!\n\x997\x93\xf1>Y\x00\xf1V]\xc1\x17\xed\xe8?\x9f~JK(\xaa\x1c>j\xb4_\xb4\xe3/\xafJb\xbf\x883\t\xec\a\xf3\xb6T\xde,\x10]\x16\xcd\xdf\xe2\xc0&\x94D\xb4a\x9b\xb4p\xab(>\xf3\xf4Y¦\x1dF\xe4qCv\xba{\xda*ټ\xb3?\x94D\xfdnJ\xc72˲\x90_\xc7>\x88Gһ\x1f\xa5\xe0c뿒ye\xf1\xfe[\x9a5\x14\xd2\xd85|\xe0\x84\x96\x02\xbb\xe3\xe3)ag\xaa$\x90\x84\x89\xb4@r\xb2\x17\x05\xb9\x0f\xa4\xbc\x15`\xe1\x9d\t\xbd=\xf2\xa0\xd2T\xcc\xd3N[o\xf3\x9bc\xf5\xcbG<\\^\x1di\xaf\xcb[u\x99\x06\x93t\xfe\x91\xd2j\xbc\x16\xad\x8a\x03\\\xf2o\x97\xec\x98-\xd9\"g8o\v\xa4:\xb9+\xa7\x8e,\t\x05(֎^\v\rn\x12,ȅ\x9f[E\xb2LW\xdaN\xdc.N\xa0\xf5M[\xe7\x0f\x00{\xee\xf6\xc8\taJ\xf4\x17N\rAl\x1d\x1a\xb0N\x9b\x98\xcc@jwp@N\x9c\xb7\xf3\xbc'V7\xa7\x91\x1e0\x05\x99\x97\xad\x86\xf0:\xfd\xd2g9\xd0\xff\xcf\xc3\xcc\xd8Ybؕ\xd1\x19Z;/J\x89\x96c\xe6\xc0\xb69\xac\x15>x\xdb&\xa9攣\xe4ؖ\xb9\xe2D\xda3\x02\x9bO?;\xe7Τ\x86\xe8\xef\x14Q>\aG\xe0DDz\x14\xc3Ědto\xfc\xe8\xb8\x01\x030\x1f0\x99\x87\x9a\x95\xca2\xbf9\x88\xe4\x1f\xcd\xf1(\xa5\xba\xe5\x89\xe0\xfd\xab9+\x10U9\x9e\x1b\xca\xdc\xc4\xf1-C\x9a\x0f\xa9\xf1+\xc4\xf4\f\xcdw5\x06{\x9c=\xbe\xc9H\xe7\x14\x903\xad\xb4\xeb\x1eք\x99~\xb1\xb0\x95ƺ\x16\xe1\x05P\xa5\xe5k\xea\u05cd1\xd5'c\xce\x0e1\xbf\xfaѝcŝ~\nIMK\x02\xebH\xfc\x9d\xd8#\xc8-H\a\xa82]+>\xf0\"uA\xd3,\x80\xe8\x99\xe8\x8dI\xa2\xcd\xec\fVu\x99N\x90\x15K\xa7T\xb3\xa7c\xdd!\xbf\t\x99v:\x05\xe7\xb1՝J\x1c\x1ak\xfdl\xa8\x90A\xd4\xcd^+\xc5OY\xd6%\x88\x92ز$n\xdc\xfaܣ\x98\xea\xe6y\xfd$\xa4\v\x19\xc4\xfebu\x996\xcdtY\x15\xe80f\x15eZY\x99c\xe3>\x04\xfe\x8f\xe6hM5\x01[!\x8b\xda,\xd0ы9\xb34n\v\xea\xe9僱tDVL\xcc\xc4C\xf7\x05N\xf3\xbc\xfd\xa8\xcc2\x97\xf9\x9b\xc1\x97wM+#IJ\xf5\x9cw:\v\x93\xbd\u05few\x1a\x84W\xa8Ô{:\v\x951ysO\x9b\xf6枾\xb9\xa7o\xee頽\xb9\xa7o\xee\xe9\x9b{:\xde\xde\xdc\xd3N{sO\x93\xedG\n\x86+>\xb9=\xd1!\t\xab\xc4\x14\x8c9\xb4g\xe6\n\x99F7Em\x1d\x9a%\x19ҷ\xe3#G^\x03d\xbeˊ_\xe8NIM\x9b\xba\xd2\x1a\xbd&e\x9a\xb6d\xdcL\xfe\x1dU\x82\x17\xfe\x02\xd9\xf6\xa9\ttsis\xfd\xdc\xf1&]\xcd\xff\xdf\x04A\x9c\x8e\xd3\a\xee\xf9\xf7yݜ\xab~\xee\x1b\xc7\x01\x11\xe3?d^ybZ\xdbL2\xdb\xe9D\xfc)\v\x1fi9\xb8\\\xe8\x13\xd3\xf4\x12\xbf\xffشtX~\xad\xc2vH\u007f0v;2\xec\x19OƄ=\xa8lg\xb4ҵ\ra\"\xcd\xf0!\xf3o\xe8\xe2D\xf6\xf8\x8d\xd7\x14\x9b,\xbc\x87\x9d\xae'\x92\xbdg蚐\x827\x9dx\x17\xaerщ\xfd\xfbu\xff\x17\xa7C\x1a\xde\x04\xd6O\xd2\xed\xfcCF\x8a\xe7\xd5C7\xd7?n\xde\xf0\x98y(x\x13\x10\xb5\x01%\v/\x95\x11BO&\xe1k\xe5\xcf\r\xceV~\xf3\xd1kz\xb2\xde\xd2\x14\xbd&\xa9j\xde\xd4>#1o٫\x89\xd9$\xbc\x14\xa4!%\xf5n<\xa9n\x06ꒄ\xbbԃ\x89\x84\xe4\xba\xf4\x94\xba4\xf2\x00\x97 HM\xa4KveS\x93\xe6^'U.1A\xae\x93\xf66\v\xf2̴\xb8d\x82\xa5\xa5\xc0%'\xbeu\xd2\xd9\xe6\xa9u\"\xddm<\x89m\x16\xe4X\x92[J\xeaZ\x12\xae\xc9\tkM\x1a\xda\xfc\xf1\xea\xb3\xd2\xd4^>!\xfe%\x83\x9f\xd3IgI\xa9fI\x01\xd2<\xceI\xc9dKSȒ\xa8\xba4]\xacI\x05;1qR\x92\xd8q\x02ة\xa5̦\x86M\xa7}\x9d\x02;\x96\x10\x96\x90\xecu\x02d7\rl\xb1\x1b0+M3\x1d\xc6˄\xc46ok\x8b\xff\v\t|\ue8b5\xe9\xb9\xc0)\xa1\xda\xd7\xc1\x10\xe2}\xf4\xfa\xc6\xdc\xea\xe9\xc0\xd7;\xdbg\xb8\xd5\x13 o\xb7Pօ\x93Uѩ\xd3\xe1vxh\xea\x00\xfc\xae\xf9=\xea\xe6\xc0о~o\x04x\nd?@\x10\x16\x9e\xb0(\xe8\xbfGT\xc8|U\x9cL\xaf\x90l\xce\xf4\xdd@\xa8\u007f\x10J\xea\\\xf9\x04H~\xac\xcb\xf6\xac$H\xb1l\xc2\x191\xe9ig\xd7\xfb\xe8\xfc\xed/5\x9a\x03\xe8=\x9aƫ\x99\x14\xb3\xf6\x11Wؚ\x96\"\xbc\xa8J\x82N\xf2\xb5\x99\xfa\xaaez74\x1b\x1a>(of\x87\xb82,\xd2!mptJuR,4\x05B\xe9\x06\xc2\xc4\xf8\x14_zɫ\xa6\xd7\b\x95^\"XJr+^#`z\xad\x90iiд\xe4>7\xe9U\xd2k\x84NK\x82\xa7E\x1e`\xfa\xab\xa3\xd7zm\xf4\nA\xd4\xd9a\xd4\"ҥ\xbe&Z\x1cL%\xaco\xe6\xf5БǕ\x00r\xf2\xd5\xd0x@\x95\x00\xf1\xe8\xb5\xd0lH\x95\xb2\x0f\x86A׳\xdf\xfe$\xe76,\xba`K\xcdKH\xbb\xfb\x9a\u007fӓ\xf8\x96'\xf1f,\x05\xfb\xc47;\xcb\xdf\xea$\xd2\xf9\xcc`\xeb\xe4ԉor\x16\x85[g\x06\\'!\x9ez\x83s:\xe4:}\x9c6|{s\x86;\x91 a\xb3]\x9e}M\xa2M\x8ef\xf6\xc6i\x89h\xce\n\xe5 &\xea\xcf?\xb8k\x89%ϨW\xf76k\x8a;\xba)\r\x90\xc1\x9f\xa4\xca=oH\b;\xfeE\xefJ\xacu~\xa6o\\Z\x8f3\xd4\n\xf5wi\x16+A\x8a\x94\xc3\"\xbe\xf3\xb7k\xf8$\xb2]3\xc3\x04H\x9ew',l\xb5)\x85\x83\xcb\xe6\x92\U0009d7c0\xfe\xbe\\\x03\xfc\xa6\x9b\xdb\xe1N\xe9\x9f\t\xa8V\x96Uq\xa0\xe8\a.\xbb`\x9e'8\x93\xc2gC\r\xc4P\xe4-!\xfe\xbd\xeb\x8f\x18\xab\xc1\x19j\xddE\xd8'\xd8,\xd4\x01\xbe\xfd`/\x8a+bem\xe5\xb0\xe0#\xc5\bxPXl\x02\xe4T\x11\xcdEĚ\xbe)\xb7N\x1b\U000407f5\xafs\x9aB\xad\xfe\x88^\xa9۠\xa3b\xdeLx\b7\xb1\xb2X\xa1z\b\xb0M\xa7;*\xbdH\xd8N)\xaf\x99\xfd\xed\\\x91\xb0\xb8\xfb\xfb\xcf~AN\x96\xb8\xfeX\xfbk\xd0U%\x8cE\xa2t\\\xa8\x1f\xb4\x99\xb6o;\xfdĵ\v\xbb\xc5H;垑\xd3\xf78A\xe2\xac\xd5\xec{\xe5>#\xe9R\x84\xfd\xc7\xf8Ȏ:\xe90\xf1T\x9e\x83\xdeN\xc2\x12\xd6\xeaL\xb2\x06\xe2\xa3 Κ{\xbd\xbay\xa7,\xc9\teQ[\xfc\xfa\xa4\xd0|\x8f\x1b\xd5ު\xa9j\xa3=\x12\xfe\xf9h\xe0d\xa5Q\xa7Y\xef\r\xba\x8f\xd9;\x15\bd}e\xd6x\xa6%mS\x8f\xf7\x98t3\xfb\u007fz\xef\x8f\xfb\xac\xab\xf1\x12\xb8\xab\xa6*\xefE\x02e}\xe5ٔ\x82˾Dm&*W\x9b`V\xb3\xdap\x11A\x02\x82\xbe\xc6\xdey%\x97\xdb\x12\xf63\xbcl\x8bڷQ\xfel\t\xfd\x11\xfe5E\x93'\xab\b{\x9b\xeaKܯ\b\xfey\xec\x1c\xdd\a\\tq\xae\xc04\xf5i2\x9e\x03\xa1y`,\xd6x7\x85\xfax\n\xeb\n\xbe\xe0\xd3\xc8\xd7O\x8a\x16q|\x97\xe6\xf3T1糁\xb1r\xf3'\x97\xb8oFq\x92\xf0\x88\xb6諹A\xf7A\xf6\x11\x97\xeem\xba\xf8\x84\xe01\xb6\xfe\xa3\xdc\xfa\x83\x9b\x8c\xd6\xf4OG=&\x15\xd7I\xa55\xa5\xb0F\xb7\xd4\xd1G\x8bfϵr\xa3\x90\x04\x1b\xde\xfdRo\xdaڙ\xf0\u05ff]\xb4\xbbRd\x19V.d\xb9u\xffi\x8fK_\xfb6\xfe\xcb\x1d\xfcg\xa6\x95w\xb0\xed5\xfc\xe7\u007f_@0\xc0?\xe2?\xcfA\x1f\xff7\x00\x00\xff\xff\xfa\xbf\x85\x18\be\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=\xcbrܺr{}E\xd7d\xe1\xe4\x96f|\\Y$\xa5\x9d#\xfbTTױ]\x96\xae7I\x16\x18\xb2G\x83#\x12\xe0\x05\xc0\x91'\xb7ο\xa7\x1a\x0f\xbe\x86 \xc1\x91T\xe7Q\xc2\xc6\x16\x06h4\xba\x1b\xfd\x00\x1a\xe0\xc5z\xbd\xbe`\x15\xff\x8eJs)\xae\x80U\x1c\u007f\x18\x14\xf4\x97\xde<\xfc\xbb\xdep\xf9\xf6\xf0\xee⁋\xfc\n\xaekmd\xf9\r\xb5\xacU\x86\x1fp\xc7\x057\\\x8a\x8b\x12\r˙aW\x17\x00L\bi\x18Uk\xfa\x13 \x93\xc2(Y\x14\xa8\xd6\xf7(6\x0f\xf5\x16\xb75/rT\x16x\x18\xfa\xf0\xd3\xe6\xdf6?]\x00d\nm\xf7;^\xa26\xac\xac\xae@\xd4Eq\x01 X\x89W\xa0\xb3=\xe6u\x81zs\xc0\x02\x95\xdcpy\xa1+\xcch\xb4{%\xeb\xea\n\xda\x1f\\'\x8f\x89\x9bŭ\xefo\xab\n\xae\xcd_{՟\xb86\xf6\xa7\xaa\xa8\x15+:\xe3\xd9Z\xcd\xc5}]0\xd5\xd6_\x00\xe8LVx\x05\x9fi\xa8\x8ae\x98_\x00\xf8\x89١\xd7\xc0\xf2ܒ\x8a\x15_\x15\x17\x06յ,\xea2\x90h\r9\xeaL\xf1\xcaXR\xdc\x1afj\rr\af\x8f\xddq\xa8\xfc\xa2\xa5\xf8\xca\xcc\xfe\n6ڶ\xdbT{\xa6ï\x8eD\x0e\x80\xaf2G\xc2M\x1b\xc5\xc5\xfd\xd8h\xef\xe1ZI\x01\xf8\xa3R\xa8\te\xc8-g\xc5=<\xeeQ\x80\x91\xa0jaQ\xf9\x0f\x96=\xd4\xd5\b\"\x15f\x9b\x01\x9e\x1e\x93~\xe5\x1c.w{\x84\x82i\x03\x86\x97\b\xcc\x0f\b\x8fL[\x1cvR\x81\xd9s=O\x13\x02\xd2\xc3֡\xf3iX\xed\x10ʙA\x8fN\aT\x90\xea͉D\xf6`\xbe\xbf\xc7\x04`\x96D\x15\xab\xb5\x15\x8e\xb6\xf7\xd7n\x95\x03\xb0\x95\xb2@&.\xdaF\x87wN\xf6\xb2=\x96\xec\xca7\x96\x15\x8a\xf7_o\xbe\xff\xebm\xaf\x1a\x06\xb2\xe4)\x05\\\x03\x83\xefva\x80\xf2K\x18̞\x19PH\x9cGa\xa8E\xa5p\x1d\xa8\x9b7 \x01\xa4\x82\n\x15\x979\xcf\x02Wlg\xbd\x97u\x91\xc3\x16\x89A\x9b\xa6C\xa5d\x85\xca\xf0\xb0\xf4\\騚N\xed\x00\xe374)\xd7\xcaI\"j+|~Aa\xee\xe9\xe0\xd6\a\xd7-\xfe\x96I=\xc0@\x8d\x98\x00\xb9\xfd\x053\xb3\x81[T\x04&`\x9dIq@E\x14\xc8\xe4\xbd\xe0\xff\xd7\xc0\xd6$\xf5\xc6\n\xa3A\xaf\x0f\xdab\x17\xb0`\x05\x1cXQ\xe3%0\x91CɎ\xa0\x90F\x81Zt\xe0\xd9&z\x03\xff%\x15\x02\x17;y\x05{c*}\xf5\xf6\xed=7A\xc5f\xb2,k\xc1\xcd\xf1\xadՖ|[\x1b\xa9\xf4\xdb\x1c\x0fX\xbc\xd5\xfc~\xcdT\xb6\xe7\x063S+|\xcb*\xbe\xb6\xa8\v\xabf7e\xfeO\x81\xa3\xfaM\x0fד\xf5\xe6\x8aU\x84\x13\x1c \x8d\xe8\x04\xc6uu\xb3h\tMUD\x9do\x1fo\xef\xba\xc2\xc4\xf5\x90\xfa\x96\xee\x1d\tkY@\x04\xe3b\x87~E\xef\x94,-L\x14y%\xb90\xf6\x8f\xac\xe0(\x86\xe4\xd7\xf5\xb6\xe4\x86\xf8\xfe\xf7\x1a\xb5!^m\xe0\xda\xda\x1d\x92ú\xa2\x15\x98o\xe0F\xc05+\xb1\xb8f\x1a_\x9c\x01Di\xbd&¦\xb1\xa0k2\x87\x8d\x1d\xd5:?\x04\xf3\x16\xe1WX\xe3\xb7\x15f\xbd%C\xfd\xf8\x8egvaX\xed٨\x80\x81\x06ue|\xd5\xda_\xac\x9a\x1a\xd6\x0e\xf0p\xba,\x8c\x8a\x9a\xec\x87\xd9[\x0e\xb7f\x8c\xe4\xcaA#\x9d\"䐻cZ\xb0C\t\x0fe\x06\x93\xbe\xd6K\xb5o'0\xc1\xab\xbaM\x04\xc7\x13\xaeڟ\xb0\xacHm̠x\xe7\x9b\x11\x8aD\x9f\xbcq\xa7\x82\xe1\x0fjVz\xed\n'\xca\xcd\x0e\xb7G\xe2ہ\xe7^{\x9dp\x15&9K%\xd3\xfcV\xb0J\xef\xa5!\x1b'k3\xd6j0\x81\xebۛA\xa7\x0e\xe7\t+k\xc3-\xa3\x8d\x84G\xc6O9\xed\n\xc9\xe5\xf5\xed\r|'\x97\b\x03Lp\x96\x1cL\xad\x84U\xc7ߐ\xe5\xc7;\xf97\x8d\x90\xd7V+\x05\xbb|\x19\x01\xbc\xc5\x1d-z\x85\x04\x83:\xa0R\xb4\x06\xb4EM\xd6fc\x1d\x8e\x1cw\xac.\x8cWr\\û\x9f\xa0\xe4\xa26x\xcaw\x98\xe6\xbd#\x92\x05\xe7f\xa3\xef\xe4\xcf\xda12\x81\xa4\x1f\"]G\x96T%s8\xd8v1\xaa\xf2\x02A\x1f\xb5\xc1\x12\xb6\x1eJc\xab-W\xac>(\n\x0fF\xc3\xf6\x18p\x1f\x9f7y\xe1l[\xe0\x15\x18U\x8f\x0f;\xb5t\xc7h\xf3\r\xb5\xe1Y\x02eVCҸ\x9e#\x84Q\xf6\x87\bQ\x06\x14 #\xcf\x1e\xc8\xd1\xf4\x14\"o\xa1(:ĝ\xa7\n\xc0\xff\b\xf8@\x06.#\xb3s\xe5\xcd\x19\xc7\u009aP!\xa1\x90\xe2\x1e\x95\x1b\x91\\\x85G^\x14vIc)\x0f='\xab[ȶ(,\xc8H®&\xb3\xb3\x01\x92\xfd\xa8\x8cp\xa1\r\xb2|\xb3z)\xe6Ꮼ\xa8s̯\x8bZ\x1bT\xb7\x14\xf4\x84`pT\xad\fx\xf8q\xaa\xbfw7\n\x9e!q!smbl\xa4\xae\x1d\xc7\xe3X\xa1\x8b\xf3\x88\xa3\x1e\xcd֣p\xbc\xdd\xc0\xcd\x0e4Ɣ\x90\x91\xb0\xfa\xcb\xeaҲ\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⎶\xdb+\x8c\vbM\xc1\xb5\xe9qf\xe8\x8e7\x98Q\xe4F\x14\"\x97\x99\v\a\x93\x14M\x87\x13\xbfg\x9a\x9d#\xd011n\xe4\xc6\xcb\xe6\x9eŤ\xef\x0fH\xb0\xbd\x94\x0f)D\xfaOj׆i\x90ٝ>\xd8\xe2\x9e\x1d\xb8Tz\x18\xeb\xe3\x0f\xccj\x13\xb5]\xcc@\xcew;T\x04\xcbnO5\xbbYSĚvR\xa9\xa8iƟ̫e:1\xcfR#6\x15\x1b\fD\xa1\x82E\x9c|Hkis~\xe0y\xcd\nkt\x99\xc8\xdc\xfcX\x83_\xccU\x98\x11\x88\x13\xfc\x9di\x0f\xb3 .\xf5b<)\x90\x82\xa8R\xaa\x98\x12v\xe5\x14L\x9c\f[fC\xb3X@\xd4\x16U\x17\xa8=*Ιl\xf5\xcee\xcb)\xb7=R\xb0-\x16\xa0\xb1\xc0\xccH\x15'O\x8a\x10\xb8\x92\xaa?#\x94\x1dѤ\xfd\xe8eV\x89\xb6\x85\u009b=\xcf\xf6\xce\xf5#)\xb3\xb0 \x97\xa8\xad\xc6`UU\x1c\xa7&\r)\x92\xe1\a\x9bS\x1amIP\x1fC\xb81EҖD\x1dܖ\x19mܧz#6\xafD\xef\xa1)\x9e$\xec7'ݟ_؉\xdc\x1c\xb5\xf5బ\xcc\xf1\x12\xb8\t\xb5)P{N\x9d\xfe\x931\xee\xbc\xd5r3\xec\xfd\xec\xab\xe5Y\xb8֠\xf1'a\x9a5V\xb7\xdeV-bاn\xcfKໆa\xf9%\xecxa\xd0\xfaRs\x88v\x1c\x9dY\xce='\x81Rm/\x95\x92\x99l\xff\xb1\xd9TM\xe81\xa0\xd5\x10\x80\xf3\xcbC\fcy\x90\x00\x12\x1a\xa7\u009e\ap\x85\xa5;g\xb8\xb3룭\xb1\x1e\xe0\xfb\xcf\x1fb\xe1`\xbf$J\xeaɤ\xde\x0f<\x9d.\nv\x82I ;\x93\xb2nZ\x13\xe3\xb9ӤK`\xf0\x80G\xe7Y\x8dnՌ\x15b-k@*\xb4G[V\x8d<\xe0т\xf2gUI\U0001620a+\x0fxLm: *\xe1\xe7w\xc9\x1du\xa9\xc2\xce\"e)\xb5\xa5!\xaa_;`d\xdada\x99R\n%P\xfc\xcci7\f\xeb\x1d\xd0>\xe0\xf1\x8dv\xec\xa3U\xb3\xe7\xd5\x02\n\x90\xc2\x06\x8dv\x85\x85\x93\xc9\xef\xac\xe0y3\x98]'\v ވK\xf8,\r\xfd\xf3\xf1\aׄ\xa2\xc8\xe1\x83D\xfdY\x1a[\xf3\xa2$v\x938\x93\xc0\xae\xb3]\x96\u0099\x05\xa2ˢ\xf1[\x1c\xac\t%\x11m\xd8\xc65\xdc\b\x8a\xcf\x1c}\x96\xb0i\x8f\x019\x87VYk{\xb6)\xa4X[3\x1dF[\x00\xb4\x8b\x97g\x95T=N].\x848\x8a\xa2G\uf3ac\x95\xfb\xe5\xe4Tx\xaa(\xac\n\x96a\x1e\xcex\xec\x1143x\xcf3(Q\xdd#Td7҅j\x81&w\xe5\f)Lw-B\xf1fa\xe4Du\xac\xaci\xd5'\xb6\flNj\x1e9o\x9en\x9e6Kkޭ?\x94D\xfdn\x82\xd42˲\x90_\xa7>\x88Cҹ\x1f%\xb3\x87@\xff \xf3j\xc5\xfb\xd74kȸ\xd2\x1bxo\xd3\xc3\n\xec\xf6\x0f\xbb\x84\x9d\xa1\x92@\x12&\\\x03\xc9Ɂ\x15\xe4>\x90\xf2\x16\x80\x85s&\xe4\xeeăJS1\x8f{\xa9\x9d\xcdo\x0e\xa9V\x0fx\\]\x9eh\xafՍX\xa5\xc1$\x9d\u007f\xa2\xb4\x1a\xafE\x8a\xe2\b+\xfb\xdb\xca:fK\x96\xc8\x19\xce\xdb\x02\xa9Nnj\x13\xb1\x96\x84\x02\x14k\a\xaf\x85:7\xe9J\xe4\xc2\xcf\xcd\"Y\xa6+\xa9#g\xf5\x11\xb4\xbeJm\xdc\x06`\xcf\xdd\x1e\xd9!L\x89\xfe\xfc\xae!\xb0\x9d=/3R\x85\xd4 R\xbb\x83\rr⼞\xe7\xbd;\xb9\xf1\xbb\x91\x0e0\x05\x99\xabVC8\x9d\xber\x87G\xf4\xffy\x98\x99u\x96,\xecJ\xc9\f\xb5\x9e\x17\xa5D\xcb1\xb3a\xdbl\xd62\x17\xbc\xed\x92Ts\xcaVr(\xcb\\q\"\xed\x19\x81\xcd\xc7\x1f\x9d}gRC\xf4w\x8a(\x9f\x83#ش\xe1\xb2d\xc34\xb5dt\xaf]\xef\xb0\x00=0\x170\xa9\xfb\xda*\x95e~\xb3\x17\xc9ߛ\xe3Qrqc\a\x82w/\xe6\xac@P\xe5xn(s\x1d\xfa\xb7\fi*R\xe3W\b\xc9NҞ\xd5(\xecq\xf6\xf4$#\x9dS@δ\x90\xa6\xbbY\xe3Gz\xa3aǕ6-\xc2\v\xa0rm\x93>^6\xc6\x14\x1f\x95:;\xc4\xfc\xe2zw\xb6\x15\xf7\xf2ѧ\b.\t\xac\x03\xf1\xf7\xec\x80\xc0w\xc0\r\xa0\xc8d-\xec\x86\x17\xa9\v\x1af\x01D\xc7DgL\x12mf\xa7\xb3\xa8\xcbt\x82\xac\xadtr1\xbb;\xd6\xed\xf23\xe3i\xbbSp\x1e[\xcdT\x1a\xdeX\xe9\xe7\x16\xfa|\xbcn.h\xc9~\xf0\xb2.\x81\x95Ė%q\xe3\xcee\xf2\x85\xc4Q\xc7\xebGƍ\xcf\xc7w\a\xab˴i&˪@\x83!G/\x93B\xf3\x1c\x1b\xf7\xc1\xf3\u007f4\xe31V\x18\xec\x18/j\xb5@G/\xe6\xccҸͫ\xa7\xe7\x0f\xc6\xd2\x11Y[b&n\xba/p\x9a\xe7\xedG\xa5\x96\xb9\xcc_\x15>\xbfkZ)NR*\xe7\xbc\xd3Y\x98\xd6{\xed{\xa7^x\x998\xc6\xdc\xd3Y\xa8\x16\x93W\xf7\xb4)\xaf\xee\xe9\xab{\xfa\xea\x9e\x0eʫ{\xfaꞾ\xba\xa7\xe3\xe5\xd5=\xed\x94W\xf74\xd9~\xa4`\xb8\xb6;\xb7\x13\r\x92\xb0JL\xc1\x98C{f,\x9fi\xe4/W,ɐ\xbe\x19\xef9r\xb7\xc6_\x8cXی\xfb\x98Դ\xa9+\xad\xd1kR\xa6iI\x86\xc5\xe4n%&x\xe1\xcfpw% p\xeeݕ\x9b\xa9\xfe\xcfwwţ9\xdc8w\x97\"\xc0Č^\xfa͕@\x87\xdeͷ\b\xd4\xe6>\x9cO\fsiH%\xb2p\xa4\xe3rH\xf2Ȩ1\a\xb6\x87\xc6h\x9b\xdf8i\xfe$\xd9r\xd1}\x83\x93\\˗\xb9@\x13\x91\x94\x05\\]\xfde\xf5\xc7 \xffYT\x8f\x12\xdb\xfd/6\xbb\x96\xb0NѺ\x8b\xe9\xdd\xf4\xc8~\x9a\xea\x1fG\x9aϑ\xe1\xd4;31\xbd\x14Wg\x1db\xaa\xde\x1d\x8d\xdf7-\r\x96_*o\xb9\xd2oJߌt{\xc2]i\xa6\x8f\"\xdb+)d\xad\xfd\x8e\x0e\x8d\xf0>s\x97\xc7\xc3@\xfa\xf4rs\x8cM\x1a\xde\xc1^֑{\x193tMȖ\x8d\xe7\xc8\xfa\xac\v4\xec\xf0n\xd3\xff\xc5H\x9f1\x1b\xc1\xfa\x91\x9b\xbd\xbb\xc1\xcf\xf2\x9c\x1c\xf3ε\x9c\xb0x\xfd+\x1eC\xc1\x8b@\x94\n\x04/\x9cT\x06\b}\xa3\xf9\xa5r[|g\xfb)\xf3\x1bM\xe9y\xb5K\xb3i\x9b\xfc\xc7y\xaf\xf8\t9\xb4\xcb.8\xcd\xe6˦ \r)Y\xb2\xe3\xf9\xaf3P\x97\xe4Ʀ\xee!&\xe4\xc1\xa6g\xbf\xa6\x91\a\xec\xdb;\xa99\xaf\xc9Qgj~\xeb\xcbd\xb5&\xe6\xb2v2TgA\x9e\x99\xc1\x9aL\xb0\xb4l\xd5\xe4\x1c\xd5N\xe6\xe9<\xb5&2S\xc7\xf3MgA\x8e壦d\x99&᚜[\xdad\x8cΟ\x84<)\xa3\xf4\xf9\xef\xae<\xe7>\xc5t~hRVh\xd2^\xc6<\xceIy\x9fK\xb3=\x93\xa8\xba4\xb3\xb3\xc9ڜ\x188)\x9f\xf34Wsj*\xb3Y\x9c\xf1\f\xcd)\xb0c\xb9\x9b\ty\x99\x13 \xbb\x19\x9b\x8b݀Yi\x9ai0\xfe>V(\xf3\xb6\xb6\xf8-$𩓖\xaa\xe7\x02\xa7\x84j_\x06]\x88\xf7\xc1\xeb\x1bs\xabはs\xb6\xcfp\xab# ovPօ\xe1U\xd1y\xa0\xca\xec\xf1\xd8<\x80\xf3\x8b\xb4WǷG\v\xed˷F\x80c \xfb\x01\x02\xd3\xf0\x88EA\xff\x9eP!s\xcf\xc1er\x8dds\xe2\xc7x\xfe\xe1\x1f\xff\x96ܥ\xdbز\xf7\xea\xad=+\tRx/茘t\xda\xd9u>\xba\xad\xfb{\x8d\xea\b\xf2\x80\xaa\xf1j\xa2b\xd6\u07b7\xf4KSS\x84\x17T\x89\xd7I\xeeQ¾j\x89\xaf\x86fA\xc3{\xe1\xcc\xec\x10W\v\x8btH\x1b\x1cM\xa9N\x8a\x85b \x84l D\xfa\xa7\xf8\xd2K. \xbeD\xa8\xf4\x1c\xc1R\x92[\xf1\x12\x01\xd3K\x85LK\x83\xa6%\xa9\x17I\x17\b_\"tZ\x12<-\xf2\x00\xd3/\b\xbe\xd4\xc5\xc0\x17\b\xa2\xce\x0e\xa3\x16\x91.\xf5\xe2\xdf\xe2`*a~3\x17\xfdN<\xae\x04\x90\xd1\v~\xe3\x01U\x02ē\x8b}\xb3!U\xca:\x18\x06]O\xbe\xa6\x97\x9c\x86\xb4\xe8,<5\x85(\xed\x98z\xfe\xfa]\u2d7b\xc4C\xec\x14\xec\x13\xaf\xd7-\xbfV\x97H\xe73\x83\xadɡ\x13\xaf\xcf-\n\xb7\xce\f\xb8&!N]\x97\x9b\x0e\xb9\xa6\xb7ӆ\xd7\xe4\xcep'\x12$l\xb6ɓ\x8fI\xa4\xcaQ͞8-\x11\xcdY\xa1\x1c\xc4D\xfd\xf1\ag-\xe1\xadOj\xd5=͊qG6\xafxd\xf0W.\xfc\xd99\taǿ\xe8\x1d\x89\xb5\xceO\xfcĥ\xf58\xfd#\xd9\xee,McŔ=U\xdf\x1e]z\x8e\xde\xc0G\x96\xed\x9b\x11\" \xed\xb8{\xa6a'U\xc9\f\xac\x9aCʷn\x00\xfa{\xb5\x01\xf8Y6\x89\x1c\x9dW\xba\"P5/\xab\xe2H\xd1\x0f\xac\xba`\x9e&8Q\xe1\xd3\xfe\xf1_\xff\xbaiB\xfc{\xdb\xef1\xf6\xf8\xb4\u007f\xe45\xc0\x9e`3\x13G\xf8\xfa\xddzQ\xf6\xf1\xba\xac}\xe4\xcf\xfbH!\x02\x1e\xbc\x01\x18\x01\x19{=z\x11\xb1\xe2I-\xdaH\xc5\xee\xf1\x93t\x0f|\xa7P\xabߣ\xf7ƻ\xd7Q!\xc5\xcd\xdfY\x8d\xcc,|\x9aa\b\xb0\xcd|=ys\x98\xb0\x8d)\xaf\x99\xf5mL\x910\xb9\xbb\xbbOnB\x86\x97\xb8\xf9P\xbbc\xd0uŔF\xa2t\x98\xa8봍۷\xbd|\xb4\x8f\xf6v_\xe1\xee|\xe7\x00m\xa6\xad\xcde:k6\x87\xde;ׁt)\xc2\xfe}\xbcgG\x9dt\x988\x95\xe7 wQXLk\x99q\xab\x81\xecV\x90Mp}\xb9'.\xa7,Ʉ\xb2\xa85~y\x14\xa8\xbe\x85\x85\xaaoD\xec\x99\xed\x1e\t\xffv\xd21\xfaĶ\x91V\xef\r\x9a\x8f\xd9;\xe1\t\xa4ݓ\xe4aO\x8b\xeb\xe6!\xfaS\xd2ͬ\xff\xf8\xda\x1f\xf7Y\xd7\xe3o\xbf\xaf\x9b\xe7\xe8/\x12(\xeb\x9e\\O\xf9Ҁ{\x9b=c\x95\xa9\x957\xabY\xad\xec{\x9f\x04\x04\xdds\x98\xe7}k\xa0\xfdv\xcb\f/ۯ\xb9\xb4Q\xfe\xec\xb7cF\xf8\xd7|- \xfa|\xbe\xb3\xa9\xee\xdb.k\x82\u007f\x1e;Gׁ}\x1fu\xee\xcb\nԦ\xb9\x9c\xe0\tm;\x86wUoc\xa8\x8fg\x9b\xaf\xe13>\x8e\xd4~\x144\x89ӳ4\x97R\x8e\xb9\xdd\x1b\x18\xfb\xce\xca\xe4\x14\x0fM/\x9b\xcf?\xa2-\xfajn\xd0|\x90}d߬o\x9a\xb8\xdc\xfd1\xb6\xfe3߹\x8d\x9b\x8c\xe6\xf4/'-\xa2\x8akRi\xc5\x14\xd6\xe8\x92:\xa9Ԩ\x0e\xf6\x91\xf8 $ކwk\xeam\xfb\xcc-\xfc\xe3\u05cbvU\xb2,\xc3\xca\xf8,7\x87\xb9\xfb\xa6\xd5\xca=\xfa\x1e>Ye\xff̤p\x0e\xb6\xbe\x82\xff\xfe\xdf\v\xf0\x06\xf8{\xf8.\x15U\xfe\u007f\x00\x00\x00\xff\xff\xe7ip\xed\x01l\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..8aa82c5c58 100644 --- a/design/cluster-scope-resource-filter.md +++ b/design/cluster-scope-resource-filter.md @@ -8,13 +8,26 @@ - [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) + - [scenario 1](#scenario-1) + - [scenario 2](#scenario-2) + - [scenario 3](#scenario-3) + - [scenario 4](#scenario-4) - [some namespaced resources + only related cluster resources](#some-namespaced-resources--only-related-cluster-resources) + - [scenario 1](#scenario-1-1) + - [scenario 2](#scenario-2-1) + - [scenario 3](#scenario-3-1) - [some namespaced resources + some additional cluster resources](#some-namespaced-resources--some-additional-cluster-resources) + - [scenario 1](#scenario-1-2) + - [scenario 2](#scenario-2-2) + - [scenario 3](#scenario-3-2) + - [scenario 4](#scenario-4-1) - [some namespaced resources + all cluster resources](#some-namespaced-resources--all-cluster-resources) + - [scenario 1](#scenario-1-3) + - [scenario 2](#scenario-2-3) + - [scenario 3](#scenario-3-3) - [all namespaced resources + no cluster resources](#all-namespaced-resources--no-cluster-resources) - [all namespaced resources + some additional cluster resources](#all-namespaced-resources--some-additional-cluster-resources) - [all namespaced resources + all cluster resources](#all-namespaced-resources--all-cluster-resources) @@ -67,14 +80,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. @@ -94,6 +102,7 @@ velero backup create ``` #### some namespaced resources + no cluster resources +##### scenario 1 The following commands mean backup all resources in namespaces default and kube-system, and no cluster resources. Example of new parameters: @@ -109,7 +118,7 @@ velero backup create --include-namespaces=default,kube-system --include-cluster-resources=false ``` - +##### scenario 2 The following commands mean backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in all namespaces, and no cluster resources. Although PVC's related PV should be included, due to no cluster resources are included, so they are ruled out too. Example of new parameters: @@ -125,7 +134,7 @@ velero backup create --include-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset --include-cluster-resources=false ``` - +##### scenario 3 The following commands mean backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in namespace default and kube-system, and no cluster resources. Although PVC's related PV should be included, due to no cluster resources are included, so they are ruled out too. Example of new parameters: @@ -143,7 +152,7 @@ velero backup create --include-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset --include-cluster-resources=false ``` - +##### scenario 4 The following commands mean backup all resources except Ingress type resources in all namespaces, and no cluster resources. Example of new parameters: @@ -161,42 +170,22 @@ velero backup create ``` #### some namespaced resources + only related cluster resources +##### scenario 1 This means backup all resources in namespaces default and kube-system, and related cluster resources. ``` bash velero backup create --include-namespaces=default,kube-system ``` -The following commands mean backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in all namespaces, and related cluster resources (PVC's related PV). - -Example of new parameters: -``` bash -velero backup create - --include-namespaced-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset -``` - -Example of old parameters: -``` bash -velero backup create - --include-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset -``` - -The following commands mean backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in namespaces default and kube-system, and related cluster resources. PVC related PV is included too. - -Example of new parameters: -``` bash -velero backup create - --include-namespaces=default,kube-system - --include-namespaced-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset -``` - -Example of old parameters: +##### scenario 2 +This means backup pods and configmaps in namespaces default and kube-system, and related cluster resources. ``` bash velero backup create --include-namespaces=default,kube-system - --include-resources=persistentvolumeclaim,deployment,service,endpoint,pod,replicaset + --include-namespaced-resources=pods,configmaps ``` +##### scenario 3 This means backup all resources except Ingress type resources in all namespaces, and related cluster resources. Example of new parameters: @@ -212,6 +201,7 @@ velero backup create ``` #### some namespaced resources + some additional cluster resources +##### scenario 1 This means backup all resources in namespace in default, kube-system, and related cluster resources, plus all StorageClass cluster resources. ``` bash velero backup create @@ -219,6 +209,7 @@ velero backup create --include-cluster-scope-resources=storageclass ``` +##### scenario 2 This means backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in all namespaces, and related cluster resources, plus all StorageClass cluster resources, and PVC related PV. ``` bash velero backup create @@ -226,6 +217,7 @@ velero backup create --include-cluster-scope-resources=storageclass ``` +##### scenario 3 This means backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in default and kube-system namespaces, and related cluster resources, plus all StorageClass cluster resources, and PVC related PV. ``` bash velero backup create @@ -234,6 +226,7 @@ velero backup create --include-cluster-scope-resources=storageclass ``` +##### scenario 4 This means backup PVC, Deployment, Service, Endpoint, Pod and ReplicaSet resources in default and kube-system namespaces, and related cluster resources, plus all cluster scope resources except StorageClass type resources. ``` bash velero backup create @@ -243,6 +236,7 @@ velero backup create ``` #### some namespaced resources + all cluster resources +##### scenario 1 The following commands mean backup all resources in namespace in default, kube-system, and all cluster resources. Example of new parameters: @@ -259,6 +253,7 @@ velero backup create --include-cluster-resources=true ``` +##### scenario 2 This means backup Deployment, Service, Endpoint, Pod and ReplicaSet resources in all namespaces, and all cluster resources. ``` bash velero backup create @@ -266,6 +261,7 @@ velero backup create --include-cluster-scope-resources=* ``` +##### scenario 3 This means backup Deployment, Service, Endpoint, Pod and ReplicaSet resources in default and kube-system namespaces, and all cluster resources. ``` bash velero backup create diff --git a/internal/hook/item_hook_handler_test.go b/internal/hook/item_hook_handler_test.go index 2fe6a2bbb4..4ff5ca44e4 100644 --- a/internal/hook/item_hook_handler_test.go +++ b/internal/hook/item_hook_handler_test.go @@ -1332,8 +1332,8 @@ func TestGetRestoreHooksFromSpec(t *testing.T) { { Name: "h1", Selector: ResourceHookSelector{ - Namespaces: collections.NewIncludesExcludes().Includes([]string{"ns1", "ns2", "ns3"}...).Excludes([]string{"ns4", "ns5", "ns6"}...), - Resources: collections.NewIncludesExcludes().Includes([]string{kuberesource.Pods.Resource}...), + Namespaces: collections.NewIncludesExcludes().Includes("ns1", "ns2", "ns3").Excludes("ns4", "ns5", "ns6"), + Resources: collections.NewIncludesExcludes().Includes(kuberesource.Pods.Resource), }, RestoreHooks: []velerov1api.RestoreResourceHook{ { diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index 869e374a62..71c9814ba6 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.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 71ec427ed3..bf90e3f23b 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 6987142d0f..fc9442054e 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -209,9 +209,22 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, log.Infof("Including namespaces: %s", backupRequest.NamespaceIncludesExcludes.IncludesString()) log.Infof("Excluding namespaces: %s", backupRequest.NamespaceIncludesExcludes.ExcludesString()) - 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()) + if collections.UseOldResourceFilters(backupRequest.Spec) { + backupRequest.ResourceIncludesExcludes = collections.GetGlobalResourceIncludesExcludes(kb.discoveryHelper, log, + backupRequest.Spec.IncludedResources, + backupRequest.Spec.ExcludedResources, + backupRequest.Spec.IncludeClusterResources, + *backupRequest.NamespaceIncludesExcludes) + } else { + backupRequest.ResourceIncludesExcludes = collections.GetScopeResourceIncludesExcludes(kb.discoveryHelper, log, + backupRequest.Spec.IncludedNamespacedResources, + backupRequest.Spec.ExcludedNamespacedResources, + backupRequest.Spec.IncludedClusterScopeResources, + backupRequest.Spec.ExcludedClusterScopeResources, + *backupRequest.NamespaceIncludesExcludes, + ) + } + log.Infof("Backing up all volumes using pod volume backup: %t", boolptr.IsSetToTrue(backupRequest.Backup.Spec.DefaultVolumesToFsBackup)) var err error @@ -398,10 +411,12 @@ 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 { + // back up CRD(this is a CRD definition of the resource, it's a CRD instance) for resource if found. + // We should only need to do this if we've backed up at least one item for the resource + // and the CRD type(this is the CRD type itself) is neither included or excluded. + // When it's included, the resource's CRD is already handled. When it's excluded, no need to check. + if !backupRequest.ResourceIncludesExcludes.ShouldExclude(kuberesource.CustomResourceDefinitions.String()) && + !backupRequest.ResourceIncludesExcludes.ShouldInclude(kuberesource.CustomResourceDefinitions.String()) { for gr := range backedUpGroupResources { kb.backupCRD(log, gr, itemBackupper) } @@ -492,6 +507,7 @@ func (kb *kubernetesBackupper) backupCRD(log logrus.FieldLogger, gr schema.Group log.WithError(errors.WithStack(err)).Errorf("Error getting CRD %s", gr.String()) return } + log.Infof("Found associated CRD %s to add to backup", gr.String()) kb.backupItem(log, gvr.GroupResource(), itemBackupper, unstructured, gvr) diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index bde9a393f8..477cf0717f 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -157,12 +157,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", @@ -760,6 +761,95 @@ func TestBackupResourceFiltering(t *testing.T) { "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", }, }, + { + name: "new filters' default value should not impact the old filters' function", + backup: defaultBackup().IncludedNamespaces("foo").IncludeClusterResources(true).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/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", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "Resource's CRD should be included", + backup: defaultBackup().IncludedNamespaces("foo").Result(), + apiResources: []*test.APIResource{ + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("volumesnapshotlocations.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("test.velero.io").Result(), + ), + test.VSLs( + builder.ForVolumeSnapshotLocation("foo", "bar").Result(), + ), + test.Backups( + builder.ForBackup("zoo", "raz").Result(), + ), + }, + want: []string{ + "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/volumesnapshotlocations.velero.io/namespaces/foo/bar.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "Resource's CRD is not included, when CRD is excluded.", + backup: defaultBackup().IncludedNamespaces("foo").ExcludedResources("customresourcedefinitions.apiextensions.k8s.io").Result(), + apiResources: []*test.APIResource{ + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("volumesnapshotlocations.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("test.velero.io").Result(), + ), + test.VSLs( + builder.ForVolumeSnapshotLocation("foo", "bar").Result(), + ), + test.Backups( + builder.ForBackup("zoo", "raz").Result(), + ), + }, + want: []string{ + "resources/volumesnapshotlocations.velero.io/namespaces/foo/bar.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/bar.json", + }, + }, } for _, tc := range tests { @@ -774,7 +864,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")...) }) @@ -887,7 +977,7 @@ func TestCRDInclusion(t *testing.T) { }, }, { - name: "include cluster resources=false excludes all CRDs when backing up selected namespaces", + name: "include-cluster-resources=false excludes all CRDs when backing up selected namespaces", backup: defaultBackup(). IncludeClusterResources(false). IncludedNamespaces("foo"). @@ -899,12 +989,12 @@ func TestCRDInclusion(t *testing.T) { builder.ForCustomResourceDefinitionV1Beta1("test.velero.io").Result(), ), test.VSLs( - builder.ForVolumeSnapshotLocation("foo", "vsl-1").Result(), + builder.ForVolumeSnapshotLocation("foo", "bar").Result(), ), }, want: []string{ - "resources/volumesnapshotlocations.velero.io/namespaces/foo/vsl-1.json", - "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/vsl-1.json", + "resources/volumesnapshotlocations.velero.io/namespaces/foo/bar.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/bar.json", }, }, { @@ -3179,3 +3269,837 @@ 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 2", + 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, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "some namespaced resources + only related cluster resources 3", + 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, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "some namespaced resources + some additional cluster resources 1", + backup: defaultBackup().IncludedNamespaces("foo").IncludedClusterScopeResources("customresourcedefinitions").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(), + ), + 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/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, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "some namespaced resources + some additional cluster resources 2", + backup: defaultBackup().IncludedNamespacedResources("persistentvolumeclaims").IncludedClusterScopeResources("customresourcedefinitions").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(), + ), + 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/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", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "some namespaced resources + some additional cluster resources 3", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods", "persistentvolumeclaims").IncludedClusterScopeResources("customresourcedefinitions").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(), + ), + 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/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, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + name: "some namespaced resources + some additional cluster resources 4", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("pods", "persistentvolumeclaims").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.PVCs( + builder.ForPersistentVolumeClaim("foo", "test-1").VolumeName("test1").Result(), + ), + test.PVs( + builder.ForPersistentVolume("test1").Result(), + builder.ForPersistentVolume("test2").Result(), + ), + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").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/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", + }, + actions: []biav2.BackupItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumeclaims"}}, + executeFunc: func(item runtime.Unstructured, backup *velerov1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, string, []velero.ResourceIdentifier, error) { + additionalItems := []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "test1"}, + } + + return item, additionalItems, "", nil, nil + }, + }, + }, + }, + { + 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 + 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", + }, + }, + { + name: "namespace resource should be included even it's not specified in the include list, when IncludedNamespaces has specified value 1", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("Secrets").Result(), + apiResources: []*test.APIResource{ + test.Secrets( + builder.ForSecret("foo", "bar").Result(), + builder.ForSecret("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("foo").Result(), + ), + test.Namespaces( + builder.ForNamespace("foo").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/foo.json", + "resources/namespaces/v1-preferredversion/cluster/foo.json", + "resources/secrets/namespaces/foo/bar.json", + "resources/secrets/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "namespace resource should be included even it's not specified in the include list, when IncludedNamespaces has specified value 2", + backup: defaultBackup().IncludedNamespaces("foo").IncludedClusterScopeResources("persistentvolumes").Result(), + apiResources: []*test.APIResource{ + test.Secrets( + builder.ForSecret("foo", "bar").Result(), + builder.ForSecret("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("foo").Result(), + ), + test.Namespaces( + builder.ForNamespace("foo").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/foo.json", + "resources/namespaces/v1-preferredversion/cluster/foo.json", + "resources/secrets/namespaces/foo/bar.json", + "resources/secrets/v1-preferredversion/namespaces/foo/bar.json", + "resources/deployments.apps/namespaces/foo/bar.json", + "resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json", + "resources/persistentvolumes/cluster/foo.json", + "resources/persistentvolumes/v1-preferredversion/cluster/foo.json", + }, + }, + { + name: "namespace resource should be included even it's not specified in the include list, when IncludedNamespaces is asterisk.", + backup: defaultBackup().IncludedNamespaces("*").IncludedClusterScopeResources("persistentvolumes").Result(), + apiResources: []*test.APIResource{ + test.Secrets( + builder.ForSecret("foo", "bar").Result(), + builder.ForSecret("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("foo").Result(), + ), + test.Namespaces( + builder.ForNamespace("foo").Result(), + builder.ForNamespace("zoo").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/foo.json", + "resources/namespaces/v1-preferredversion/cluster/foo.json", + "resources/namespaces/cluster/zoo.json", + "resources/namespaces/v1-preferredversion/cluster/zoo.json", + "resources/secrets/namespaces/foo/bar.json", + "resources/secrets/namespaces/zoo/raz.json", + "resources/secrets/v1-preferredversion/namespaces/foo/bar.json", + "resources/secrets/v1-preferredversion/namespaces/zoo/raz.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/foo.json", + "resources/persistentvolumes/v1-preferredversion/cluster/foo.json", + }, + }, + { + name: "when all namespace resources are involved, cluster resources should be included too", + backup: defaultBackup().IncludedNamespaces("*").IncludedNamespacedResources("*").Result(), + apiResources: []*test.APIResource{ + test.Secrets( + builder.ForSecret("foo", "bar").Result(), + builder.ForSecret("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("foo").Result(), + builder.ForPersistentVolume("bar").Result(), + ), + test.Namespaces( + builder.ForNamespace("foo").Result(), + builder.ForNamespace("zoo").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/foo.json", + "resources/namespaces/v1-preferredversion/cluster/foo.json", + "resources/namespaces/cluster/zoo.json", + "resources/namespaces/v1-preferredversion/cluster/zoo.json", + "resources/secrets/namespaces/foo/bar.json", + "resources/secrets/namespaces/zoo/raz.json", + "resources/secrets/v1-preferredversion/namespaces/foo/bar.json", + "resources/secrets/v1-preferredversion/namespaces/zoo/raz.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/foo.json", + "resources/persistentvolumes/v1-preferredversion/cluster/foo.json", + "resources/persistentvolumes/cluster/bar.json", + "resources/persistentvolumes/v1-preferredversion/cluster/bar.json", + }, + }, + { + name: "IncludedNamespaces is asterisk, but not all namespaced types are include, additional cluster resource should not be included.", + backup: defaultBackup().IncludedNamespaces("*").IncludedNamespacedResources("secrets").Result(), + apiResources: []*test.APIResource{ + test.Secrets( + builder.ForSecret("foo", "bar").Result(), + builder.ForSecret("zoo", "raz").Result(), + ), + test.Deployments( + builder.ForDeployment("foo", "bar").Result(), + builder.ForDeployment("zoo", "raz").Result(), + ), + test.PVs( + builder.ForPersistentVolume("foo").Result(), + builder.ForPersistentVolume("bar").Result(), + ), + test.Namespaces( + builder.ForNamespace("foo").Result(), + builder.ForNamespace("zoo").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/foo.json", + "resources/namespaces/v1-preferredversion/cluster/foo.json", + "resources/namespaces/cluster/zoo.json", + "resources/namespaces/v1-preferredversion/cluster/zoo.json", + "resources/secrets/namespaces/foo/bar.json", + "resources/secrets/namespaces/zoo/raz.json", + "resources/secrets/v1-preferredversion/namespaces/foo/bar.json", + "resources/secrets/v1-preferredversion/namespaces/zoo/raz.json", + }, + }, + { + name: "Resource's CRD should be included", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("volumesnapshotlocations.velero.io", "backups.velero.io").Result(), + apiResources: []*test.APIResource{ + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("volumesnapshotlocations.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("test.velero.io").Result(), + ), + test.VSLs( + builder.ForVolumeSnapshotLocation("foo", "bar").Result(), + ), + test.Backups( + builder.ForBackup("zoo", "raz").Result(), + ), + }, + want: []string{ + "resources/customresourcedefinitions.apiextensions.k8s.io/cluster/volumesnapshotlocations.velero.io.json", + "resources/customresourcedefinitions.apiextensions.k8s.io/v1beta1-preferredversion/cluster/volumesnapshotlocations.velero.io.json", + "resources/volumesnapshotlocations.velero.io/namespaces/foo/bar.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/bar.json", + }, + }, + { + name: "Resource's CRD is not included, when CRD is excluded.", + backup: defaultBackup().IncludedNamespaces("foo").IncludedNamespacedResources("volumesnapshotlocations.velero.io", "backups.velero.io").ExcludedClusterScopeResources("customresourcedefinitions.apiextensions.k8s.io").Result(), + apiResources: []*test.APIResource{ + test.CRDs( + builder.ForCustomResourceDefinitionV1Beta1("backups.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("volumesnapshotlocations.velero.io").Result(), + builder.ForCustomResourceDefinitionV1Beta1("test.velero.io").Result(), + ), + test.VSLs( + builder.ForVolumeSnapshotLocation("foo", "bar").Result(), + ), + test.Backups( + builder.ForBackup("zoo", "raz").Result(), + ), + }, + want: []string{ + "resources/volumesnapshotlocations.velero.io/namespaces/foo/bar.json", + "resources/volumesnapshotlocations.velero.io/v1-preferredversion/namespaces/foo/bar.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 98c0cb41ab..c8de43a11e 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -128,17 +128,24 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti log.Info("Excluding item because namespace is excluded") return false, itemFiles, 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") + + // NOTE: we specifically allow namespaces to be backed up even if it's excluded. + // This check is more permissive for cluster resources to let those passed in by + // plugins' additional items to get involved. + // Only expel cluster resource when it's specifically listed in the excluded list here. + if namespace == "" && groupResource != kuberesource.Namespaces && + ib.backupRequest.ResourceIncludesExcludes.ShouldExclude(groupResource.String()) { + log.Info("Excluding item because resource is cluster-scoped and is excluded by cluster filter.") return false, itemFiles, nil } - if !ib.backupRequest.ResourceIncludesExcludes.ShouldInclude(groupResource.String()) { + // Only check namespace-scoped resource to avoid expelling cluster resources + // are not specified in included list. + if namespace != "" && !ib.backupRequest.ResourceIncludesExcludes.ShouldInclude(groupResource.String()) { log.Info("Excluding item because resource is excluded") return false, itemFiles, nil } + } if metadata.GetDeletionTimestamp() != nil { diff --git a/pkg/backup/item_collector.go b/pkg/backup/item_collector.go index 0b21359f30..00f4f2bc77 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -249,26 +249,6 @@ func (r *itemCollector) getResourceItems(log logrus.FieldLogger, gv schema.Group return items, nil } - // 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. - 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") - 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") @@ -293,7 +273,7 @@ func (r *itemCollector) getResourceItems(log logrus.FieldLogger, gv schema.Group namespacesToList := getNamespacesToList(r.backupRequest.NamespaceIncludesExcludes) // Check if we're backing up namespaces for a less-than-full backup. - // We enter this block if resource is Namespaces and the namespae list is either empty or contains + // We enter this block if resource is Namespaces and the namespace list is either empty or contains // an explicit namespace list. (We skip this block if the list contains "" since that indicates // a full-cluster backup if gr == kuberesource.Namespaces && (len(namespacesToList) == 0 || namespacesToList[0] != "") { diff --git a/pkg/backup/request.go b/pkg/backup/request.go index 24b7ecaa67..bf1e5dd914 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -44,7 +44,7 @@ type Request struct { StorageLocation *velerov1api.BackupStorageLocation SnapshotLocations []*velerov1api.VolumeSnapshotLocation NamespaceIncludesExcludes *collections.IncludesExcludes - ResourceIncludesExcludes *collections.IncludesExcludes + ResourceIncludesExcludes collections.IncludesExcludesInterface ResourceHooks []hook.ResourceHook ResolvedActions []framework.BackupItemResolvedActionV2 ResolvedItemSnapshotters []framework.ItemSnapshotterResolvedAction diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index a599816bad..af38f895dd 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 1d7a8f53fc..c09f98e64b 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -82,26 +82,29 @@ 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 - ItemOperationTimeout time.Duration - - client veleroclient.Interface + 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 + ItemOperationTimeout time.Duration + client veleroclient.Interface } func NewCreateOptions() *CreateOptions { @@ -117,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 deployments.apps(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 deployments.apps(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.") @@ -131,7 +138,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") @@ -162,7 +169,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) @@ -170,6 +177,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 filter parameters.\n" + + "They cannot be used together") + } + if o.StorageLocation != "" { location := &velerov1api.BackupStorageLocation{} if err := client.Get(context.Background(), kbclient.ObjectKey{ @@ -300,13 +313,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]) @@ -334,6 +347,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). @@ -362,3 +379,15 @@ 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 := len(o.IncludeResources) > 0 || + len(o.ExcludeResources) > 0 || + o.IncludeClusterResources.Value != nil + haveNewResourceFilterParameters := len(o.IncludeClusterScopeResources) > 0 || + (len(o.ExcludeClusterScopeResources) > 0) || + (len(o.IncludeNamespacedResources) > 0) || + (len(o.ExcludeNamespacedResources) > 0) + + return haveOldResourceFilterParameters && haveNewResourceFilterParameters +} diff --git a/pkg/cmd/cli/schedule/create.go b/pkg/cmd/cli/schedule/create.go index 8aac85b8a5..609e74470e 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -133,20 +133,24 @@ 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}, - ItemOperationTimeout: metav1.Duration{Duration: o.BackupOptions.ItemOperationTimeout}, + 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}, + ItemOperationTimeout: metav1.Duration{Duration: o.BackupOptions.ItemOperationTimeout}, }, 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 4c2e9d271c..36fc96706d 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -36,6 +36,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/features" clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -135,21 +136,48 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) { d.Printf("\tExcluded:\t%s\n", s) d.Println() - d.Printf("Resources:\n") - if len(spec.IncludedResources) == 0 { - s = "*" - } else { - s = strings.Join(spec.IncludedResources, ", ") - } - d.Printf("\tIncluded:\t%s\n", s) - if len(spec.ExcludedResources) == 0 { - s = emptyDisplay + if collections.UseOldResourceFilters(spec) { + d.Printf("Resources:\n") + if len(spec.IncludedResources) == 0 { + s = "*" + } else { + s = strings.Join(spec.IncludedResources, ", ") + } + d.Printf("\tIncluded:\t%s\n", s) + if len(spec.ExcludedResources) == 0 { + s = emptyDisplay + } else { + s = strings.Join(spec.ExcludedResources, ", ") + } + d.Printf("\tExcluded:\t%s\n", s) + d.Printf("\tCluster-scoped:\t%s\n", BoolPointerString(spec.IncludeClusterResources, "excluded", "included", "auto")) } else { - s = strings.Join(spec.ExcludedResources, ", ") - } - d.Printf("\tExcluded:\t%s\n", s) + 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) - d.Printf("\tCluster-scoped:\t%s\n", BoolPointerString(spec.IncludeClusterResources, "excluded", "included", "auto")) + 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 diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index e6a09f1f50..fafc3a0e29 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -426,11 +426,30 @@ func (b *backupReconciler) 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 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)) @@ -1082,3 +1101,15 @@ func (b *backupReconciler) recreateVolumeSnapshotContent(vsc snapshotv1api.Volum return nil } + +func oldAndNewFilterParametersUsedTogether(backupSpec velerov1api.BackupSpec) bool { + haveOldResourceFilterParameters := len(backupSpec.IncludedResources) > 0 || + (len(backupSpec.ExcludedResources) > 0) || + (backupSpec.IncludeClusterResources != nil) + haveNewResourceFilterParameters := len(backupSpec.IncludedClusterScopeResources) > 0 || + (len(backupSpec.ExcludedClusterScopeResources) > 0) || + (len(backupSpec.IncludedNamespacedResources) > 0) || + (len(backupSpec.ExcludedNamespacedResources) > 0) + + return haveOldResourceFilterParameters && haveNewResourceFilterParameters +} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 566add5f23..89532e9384 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -181,6 +181,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 filter parameters.\nThey cannot be used together"}, + }, } for _, test := range tests { diff --git a/pkg/test/api_server.go b/pkg/test/api_server.go index 06439bca18..75cc545d77 100644 --- a/pkg/test/api_server.go +++ b/pkg/test/api_server.go @@ -56,6 +56,7 @@ func NewAPIServer(t *testing.T) *APIServer { {Group: "apps", Version: "v1", Resource: "deployments"}: "DeploymentsList", {Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}: "CRDList", {Group: "velero.io", Version: "v1", Resource: "volumesnapshotlocations"}: "VSLList", + {Group: "velero.io", Version: "v1", Resource: "backups"}: "BackupList", {Group: "extensions", Version: "v1", Resource: "deployments"}: "ExtDeploymentsList", {Group: "velero.io", Version: "v1", Resource: "deployments"}: "VeleroDeploymentsList", }) diff --git a/pkg/test/resources.go b/pkg/test/resources.go index 9e09fccdf1..dfe22278d5 100644 --- a/pkg/test/resources.go +++ b/pkg/test/resources.go @@ -163,6 +163,16 @@ func VSLs(items ...metav1.Object) *APIResource { } } +func Backups(items ...metav1.Object) *APIResource { + return &APIResource{ + Group: "velero.io", + Version: "v1", + Name: "backups", + Namespaced: true, + Items: items, + } +} + func Services(items ...metav1.Object) *APIResource { return &APIResource{ Group: "", diff --git a/pkg/util/collections/includes_excludes.go b/pkg/util/collections/includes_excludes.go index 231689be04..0f027a42a5 100644 --- a/pkg/util/collections/includes_excludes.go +++ b/pkg/util/collections/includes_excludes.go @@ -21,11 +21,15 @@ import ( "github.com/gobwas/glob" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/discovery" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/util/boolptr" ) type globStringSet struct { @@ -101,6 +105,160 @@ func (ie *IncludesExcludes) ShouldInclude(s string) bool { return ie.includes.Len() == 0 || ie.includes.Has("*") || ie.includes.match(s) } +// IncludesExcludesInterface is used as polymorphic IncludesExcludes for Global and scope +// resources Include/Exclude. +type IncludesExcludesInterface interface { + // Check whether the type name passed in by parameter should be included. + // typeName should be k8s.io/apimachinery/pkg/runtime/schema GroupResource's String() result. + ShouldInclude(typeName string) bool + + // Check whether the type name passed in by parameter should be excluded. + // typeName should be k8s.io/apimachinery/pkg/runtime/schema GroupResource's String() result. + ShouldExclude(typeName string) bool +} + +type GlobalIncludesExcludes struct { + resourceFilter IncludesExcludes + includeClusterResources *bool + namespaceFilter IncludesExcludes + + helper discovery.Helper + logger logrus.FieldLogger +} + +// ShouldInclude returns whether the specified item should be +// included or not. Everything in the includes list except those +// items in the excludes list should be included. +// It has some exceptional cases. When IncludeClusterResources is set to false, +// no need to check the filter, all cluster resources are excluded. +func (ie *GlobalIncludesExcludes) ShouldInclude(typeName string) bool { + _, resource, err := ie.helper.ResourceFor(schema.ParseGroupResource(typeName).WithVersion("")) + if err != nil { + ie.logger.Errorf("fail to get resource %s. %s", typeName, err.Error()) + return false + } + + if resource.Namespaced == false && boolptr.IsSetToFalse(ie.includeClusterResources) { + ie.logger.Info("Skipping resource %s, because it's cluster-scoped, and IncludeClusterResources is set to false.", typeName) + return false + } + + // when IncludeClusterResources == nil (auto), only directly + // back up cluster-scoped resources if we're doing a full-cluster + // (all namespaces and all namespace scope types) 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. + if typeName != kuberesource.Namespaces.String() && resource.Namespaced == false && + ie.includeClusterResources == nil && !ie.namespaceFilter.IncludeEverything() { + ie.logger.Infof("Skipping resource %s, because it's cluster-scoped and only specific namespaces or namespace scope types are included in the backup.", typeName) + return false + } + + return ie.resourceFilter.ShouldInclude(typeName) +} + +// ShouldExclude returns whether the resource type should be excluded or not. +func (ie *GlobalIncludesExcludes) ShouldExclude(typeName string) bool { + // if the type name is specified in excluded list, it's excluded. + if ie.resourceFilter.excludes.match(typeName) { + return true + } + + _, resource, err := ie.helper.ResourceFor(schema.ParseGroupResource(typeName).WithVersion("")) + if err != nil { + ie.logger.Errorf("fail to get resource %s. %s", typeName, err.Error()) + return true + } + + // the resource type is cluster scope + if !resource.Namespaced { + // if includeClusterResources is set to false, cluster resource should be excluded. + if boolptr.IsSetToFalse(ie.includeClusterResources) { + return true + } + // if includeClusterResources is set to nil, check whether it's included by resource + // filter. + if ie.includeClusterResources == nil && !ie.resourceFilter.ShouldInclude(typeName) { + return true + } + } + + return false +} + +type ScopeIncludesExcludes struct { + namespaceResourceFilter IncludesExcludes // namespace scope resource filter + clusterResourceFilter IncludesExcludes // cluster scope resource filter + namespaceFilter IncludesExcludes // namespace filter + + helper discovery.Helper + logger logrus.FieldLogger +} + +// ShouldInclude returns whether the specified resource should be included or not. +// The function will check whether the resource is namespaced resource first. +// For namespaced resource, except resources listed in excludes, other things should be included. +// For cluster resource, except resources listed in excludes, only include the resource specified by the included. +// It also has some exceptional checks. For namespace, as long as it's not excluded, it is involved. +// If all namespace resources are included, all cluster resource are returned to get a full backup. +func (ie *ScopeIncludesExcludes) ShouldInclude(typeName string) bool { + _, resource, err := ie.helper.ResourceFor(schema.ParseGroupResource(typeName).WithVersion("")) + if err != nil { + ie.logger.Errorf("fail to get resource %s. %s", typeName, err.Error()) + return false + } + + if resource.Namespaced { + if ie.namespaceResourceFilter.excludes.Has("*") || ie.namespaceResourceFilter.excludes.match(typeName) { + return false + } + + // len=0 means include everything + return ie.namespaceResourceFilter.includes.Len() == 0 || ie.namespaceResourceFilter.includes.Has("*") || ie.namespaceResourceFilter.includes.match(typeName) + } + + if ie.clusterResourceFilter.excludes.Has("*") || ie.clusterResourceFilter.excludes.match(typeName) { + return false + } + + // when IncludedClusterScopeResources and ExcludedClusterScopeResources are not specified, + // only directly back up cluster-scoped resources if we're doing a full-cluster + // (all namespaces and all namespace scope types) backup. + if len(ie.clusterResourceFilter.includes.List()) == 0 && + len(ie.clusterResourceFilter.excludes.List()) == 0 && + ie.namespaceFilter.IncludeEverything() && + ie.namespaceResourceFilter.IncludeEverything() { + return true + } + + // Also include namespace resource by default. + return ie.clusterResourceFilter.includes.Has("*") || ie.clusterResourceFilter.includes.match(typeName) || typeName == kuberesource.Namespaces.String() +} + +// ShouldExclude returns whether the resource type should be excluded or not. +// For ScopeIncludesExcludes, if the resource type is specified in the exclude +// list, it should be excluded. +func (ie *ScopeIncludesExcludes) ShouldExclude(typeName string) bool { + _, resource, err := ie.helper.ResourceFor(schema.ParseGroupResource(typeName).WithVersion("")) + if err != nil { + ie.logger.Errorf("fail to get resource %s. %s", typeName, err.Error()) + return true + } + + if resource.Namespaced { + if ie.namespaceResourceFilter.excludes.match(typeName) { + return true + } + } else { + if ie.clusterResourceFilter.excludes.match(typeName) { + return true + } + } + return false +} + // IncludesString returns a string containing all of the includes, separated by commas, or * if the // list is empty. func (ie *IncludesExcludes) IncludesString() string { @@ -126,6 +284,24 @@ func (ie *IncludesExcludes) IncludeEverything() bool { return ie.excludes.Len() == 0 && (ie.includes.Len() == 0 || (ie.includes.Len() == 1 && ie.includes.Has("*"))) } +func newScopeIncludesExcludes(nsIncludesExcludes IncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes { + ret := &ScopeIncludesExcludes{ + namespaceResourceFilter: IncludesExcludes{ + includes: newGlobStringSet(), + excludes: newGlobStringSet(), + }, + clusterResourceFilter: IncludesExcludes{ + includes: newGlobStringSet(), + excludes: newGlobStringSet(), + }, + namespaceFilter: nsIncludesExcludes, + helper: helper, + logger: logger, + } + + return ret +} + // ValidateIncludesExcludes checks provided lists of included and excluded // items to ensure they are a valid set of IncludesExcludes data. func ValidateIncludesExcludes(includesList, excludesList []string) []error { @@ -177,6 +353,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 @@ -200,11 +405,11 @@ func validateNamespaceName(ns string) []error { return errs } -// GenerateIncludesExcludes constructs an IncludesExcludes struct by taking the provided +// generateIncludesExcludes constructs an IncludesExcludes struct by taking the provided // include/exclude slices, applying the specified mapping function to each item in them, // and adding the output of the function to the new struct. If the mapping function returns // an empty string for an item, it is omitted from the result. -func GenerateIncludesExcludes(includes, excludes []string, mapFunc func(string) string) *IncludesExcludes { +func generateIncludesExcludes(includes, excludes []string, mapFunc func(string) string) *IncludesExcludes { res := NewIncludesExcludes() for _, item := range includes { @@ -237,11 +442,39 @@ func GenerateIncludesExcludes(includes, excludes []string, mapFunc func(string) return res } +// generateScopedIncludesExcludes's function is similar with generateIncludesExcludes, +// but it's used for scoped Includes/Excludes. +func generateScopedIncludesExcludes(namespacedIncludes, namespacedExcludes, clusterIncludes, clusterExcludes []string, mapFunc func(string, bool) string, nsIncludesExcludes IncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes { + res := newScopeIncludesExcludes(nsIncludesExcludes, helper, logger) + + generateFilter(res.namespaceResourceFilter.includes, namespacedIncludes, mapFunc, true) + generateFilter(res.namespaceResourceFilter.excludes, namespacedExcludes, mapFunc, true) + generateFilter(res.clusterResourceFilter.includes, clusterIncludes, mapFunc, false) + generateFilter(res.clusterResourceFilter.excludes, clusterExcludes, mapFunc, false) + + return res +} + +func generateFilter(filter globStringSet, resources []string, mapFunc func(string, bool) string, namespaced bool) { + for _, item := range resources { + if item == "*" { + filter.Insert(item) + continue + } + + key := mapFunc(item, namespaced) + if key == "" { + continue + } + filter.Insert(key) + } +} + // 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. func GetResourceIncludesExcludes(helper discovery.Helper, includes, excludes []string) *IncludesExcludes { - resources := GenerateIncludesExcludes( + resources := generateIncludesExcludes( includes, excludes, func(item string) string { @@ -260,3 +493,74 @@ func GetResourceIncludesExcludes(helper discovery.Helper, includes, excludes []s return resources } + +func GetGlobalResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, includes, excludes []string, includeClusterResources *bool, nsIncludesExcludes IncludesExcludes) *GlobalIncludesExcludes { + ret := &GlobalIncludesExcludes{ + resourceFilter: *GetResourceIncludesExcludes(helper, includes, excludes), + includeClusterResources: includeClusterResources, + namespaceFilter: nsIncludesExcludes, + helper: helper, + logger: logger, + } + + logger.Infof("Including resources: %s", ret.resourceFilter.IncludesString()) + logger.Infof("Excluding resources: %s", ret.resourceFilter.ExcludesString()) + return ret +} + +// GetScopeResourceIncludesExcludes's function is similar with GetResourceIncludesExcludes, +// but it's used for scoped Includes/Excludes, and can handle both cluster and namespace resources. +func GetScopeResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, namespaceIncludes, namespaceExcludes, clusterIncludes, clusterExcludes []string, nsIncludesExcludes IncludesExcludes) *ScopeIncludesExcludes { + ret := generateScopedIncludesExcludes( + namespaceIncludes, + namespaceExcludes, + clusterIncludes, + clusterExcludes, + 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() + }, + nsIncludesExcludes, + helper, + logger, + ) + logger.Infof("Including namespace scope resources: %s", ret.namespaceResourceFilter.IncludesString()) + logger.Infof("Excluding namespace scope resources: %s", ret.namespaceResourceFilter.ExcludesString()) + logger.Infof("Including cluster scope resources: %s", ret.clusterResourceFilter.GetIncludes()) + logger.Infof("Excluding cluster scope resources: %s", ret.clusterResourceFilter.ExcludesString()) + + return ret +} + +// UseOldResourceFilters checks whether to use old resource filters (IncludeClusterResources, +// IncludedResources and ExcludedResources), depending the backup's filters setting. +// New filters are IncludeClusterScopedResources, ExcludeClusterScopedResources, +// IncludeNamespacedResources and ExcludeNamespacedResources. +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/util/collections/includes_excludes_test.go b/pkg/util/collections/includes_excludes_test.go index 672a309d23..e3e23b83cc 100644 --- a/pkg/util/collections/includes_excludes_test.go +++ b/pkg/util/collections/includes_excludes_test.go @@ -20,8 +20,15 @@ import ( "testing" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/test" ) func TestShouldInclude(t *testing.T) { @@ -295,3 +302,626 @@ 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: "exclude everything not allowed with other excludes", + 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 excludes 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 TestNamespaceScopeShouldInclude(t *testing.T) { + tests := []struct { + name string + namespaceIncludes []string + namespaceExcludes []string + item string + want bool + apiResources []*test.APIResource + }{ + { + name: "empty string should include every item", + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "include * should include every item", + namespaceIncludes: []string{"*"}, + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "item in includes list should include item", + namespaceIncludes: []string{"foo", "bar", "pods"}, + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "item not in includes list should not include item", + namespaceIncludes: []string{"foo", "baz"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "include *, excluded item should not include item", + namespaceIncludes: []string{"*"}, + namespaceExcludes: []string{"pods"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "include *, exclude foo, bar should be included", + namespaceIncludes: []string{"*"}, + namespaceExcludes: []string{"foo"}, + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "an item both included and excluded should not be included", + namespaceIncludes: []string{"pods"}, + namespaceExcludes: []string{"pods"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "wildcard should include item", + namespaceIncludes: []string{"*s"}, + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "wildcard mismatch should not include item", + namespaceIncludes: []string{"*.bar"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "exclude * should include nothing", + namespaceExcludes: []string{"*"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "wildcard exclude should not include item", + namespaceIncludes: []string{"*"}, + namespaceExcludes: []string{"*s"}, + item: "pods", + want: false, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "wildcard exclude mismatch should include item", + namespaceExcludes: []string{"*.bar"}, + item: "pods", + want: true, + apiResources: []*test.APIResource{ + test.Pods(), + }, + }, + { + name: "resource cannot be found by discovery client should not be include", + item: "pods", + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + discoveryHelper := setupDiscoveryClientWithResources(tc.apiResources) + logger := logrus.StandardLogger() + scopeIncludesExcludes := GetScopeResourceIncludesExcludes(discoveryHelper, logger, tc.namespaceIncludes, tc.namespaceExcludes, []string{}, []string{}, *NewIncludesExcludes()) + + if got := scopeIncludesExcludes.ShouldInclude((tc.item)); got != tc.want { + t.Errorf("want %t, got %t", tc.want, got) + } + }) + } +} + +func TestClusterScopedShouldInclude(t *testing.T) { + tests := []struct { + name string + clusterIncludes []string + clusterExcludes []string + nsIncludes []string + item string + want bool + apiResources []*test.APIResource + }{ + { + name: "empty string should include nothing", + nsIncludes: []string{"default"}, + item: "persistentvolumes", + want: false, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + { + name: "include * should include every item", + clusterIncludes: []string{"*"}, + item: "persistentvolumes", + want: true, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + { + name: "item in includes list should include item", + clusterIncludes: []string{"namespaces", "bar", "baz"}, + item: "namespaces", + want: true, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "item not in includes list should not include item", + clusterIncludes: []string{"foo", "baz"}, + nsIncludes: []string{"default"}, + item: "persistentvolumes", + want: false, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + { + name: "include *, excluded item should not include item", + clusterIncludes: []string{"*"}, + clusterExcludes: []string{"namespaces"}, + item: "namespaces", + want: false, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "include *, exclude foo, bar should be included", + clusterIncludes: []string{"*"}, + clusterExcludes: []string{"foo"}, + item: "namespaces", + want: true, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "an item both included and excluded should not be included", + clusterIncludes: []string{"namespaces"}, + clusterExcludes: []string{"namespaces"}, + item: "namespaces", + want: false, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "wildcard should include item", + clusterIncludes: []string{"*spaces"}, + item: "namespaces", + want: true, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "wildcard mismatch should not include item", + clusterIncludes: []string{"*.bar"}, + nsIncludes: []string{"default"}, + item: "persistentvolumes", + want: false, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + { + name: "exclude * should include nothing", + clusterExcludes: []string{"*"}, + item: "namespaces", + want: false, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "wildcard exclude should not include item", + clusterIncludes: []string{"*"}, + clusterExcludes: []string{"*spaces"}, + item: "namespaces", + want: false, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "wildcard exclude mismatch should not include item", + clusterExcludes: []string{"*spaces"}, + item: "namespaces", + want: false, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "resource cannot be found by discovery client should not be include", + item: "namespaces", + want: false, + }, + { + name: "even namespaces is not in the include list, it should also be involved.", + clusterIncludes: []string{"foo", "baz"}, + item: "namespaces", + want: true, + apiResources: []*test.APIResource{ + test.Namespaces(), + }, + }, + { + name: "When all namespaces and namespace scope resources are included, cluster resource should be included.", + clusterIncludes: []string{}, + nsIncludes: []string{"*"}, + item: "persistentvolumes", + want: true, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + { + name: "When all namespaces and namespace scope resources are included, but cluster resource is excluded.", + clusterIncludes: []string{}, + clusterExcludes: []string{"persistentvolumes"}, + nsIncludes: []string{"*"}, + item: "persistentvolumes", + want: false, + apiResources: []*test.APIResource{ + test.PVs(), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + discoveryHelper := setupDiscoveryClientWithResources(tc.apiResources) + logger := logrus.StandardLogger() + nsIncludeExclude := NewIncludesExcludes().Includes(tc.nsIncludes...) + scopeIncludesExcludes := GetScopeResourceIncludesExcludes(discoveryHelper, logger, []string{}, []string{}, tc.clusterIncludes, tc.clusterExcludes, *nsIncludeExclude) + + if got := scopeIncludesExcludes.ShouldInclude((tc.item)); got != tc.want { + t.Errorf("want %t, got %t", tc.want, got) + } + }) + } +} + +func TestGetScopedResourceIncludesExcludes(t *testing.T) { + tests := []struct { + name string + namespaceIncludes []string + namespaceExcludes []string + clusterIncludes []string + clusterExcludes []string + expectedNamespaceIncludes []string + expectedNamespaceExcludes []string + expectedClusterIncludes []string + expectedClusterExcludes []string + apiResources []*test.APIResource + }{ + { + name: "only include namespace resources in IncludesExcludes, when namespaced is set to true", + namespaceIncludes: []string{"deployments.apps", "persistentvolumes"}, + namespaceExcludes: []string{"pods", "persistentvolumes"}, + expectedNamespaceIncludes: []string{"deployments.apps"}, + expectedNamespaceExcludes: []string{"pods"}, + expectedClusterIncludes: []string{}, + expectedClusterExcludes: []string{}, + apiResources: []*test.APIResource{ + test.Deployments(), + test.PVs(), + test.Pods(), + }, + }, + { + name: "only include cluster-scoped resources in IncludesExcludes, when namespaced is set to false", + clusterIncludes: []string{"deployments.apps", "persistentvolumes"}, + clusterExcludes: []string{"pods", "persistentvolumes"}, + expectedNamespaceIncludes: []string{}, + expectedNamespaceExcludes: []string{}, + expectedClusterIncludes: []string{"persistentvolumes"}, + expectedClusterExcludes: []string{"persistentvolumes"}, + apiResources: []*test.APIResource{ + test.Deployments(), + test.PVs(), + test.Pods(), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + logger := logrus.StandardLogger() + nsIncludeExclude := NewIncludesExcludes() + resources := GetScopeResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, tc.namespaceIncludes, tc.namespaceExcludes, tc.clusterIncludes, tc.clusterExcludes, *nsIncludeExclude) + + assert.Equal(t, tc.expectedNamespaceIncludes, resources.namespaceResourceFilter.includes.List()) + assert.Equal(t, tc.expectedNamespaceExcludes, resources.namespaceResourceFilter.excludes.List()) + assert.Equal(t, tc.expectedClusterIncludes, resources.clusterResourceFilter.includes.List()) + assert.Equal(t, tc.expectedClusterExcludes, resources.clusterResourceFilter.excludes.List()) + }) + } +} + +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)) + }) + } +} + +func defaultBackup() *builder.BackupBuilder { + return builder.ForBackup(velerov1api.DefaultNamespace, "backup-1").DefaultVolumesToFsBackup(false) +} + +func TestShouldExcluded(t *testing.T) { + falseBoolean := false + trueBoolean := true + tests := []struct { + name string + clusterIncludes []string + clusterExcludes []string + includeClusterResources *bool + filterType string + resourceName string + apiResources []*test.APIResource + resourceIsExcluded bool + }{ + { + name: "GlobalResourceIncludesExcludes: filters are all default", + clusterIncludes: []string{}, + clusterExcludes: []string{}, + includeClusterResources: nil, + filterType: "global", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + { + name: "GlobalResourceIncludesExcludes: IncludeClusterResources is set to true", + clusterIncludes: []string{}, + clusterExcludes: []string{}, + includeClusterResources: &trueBoolean, + filterType: "global", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + { + name: "GlobalResourceIncludesExcludes: IncludeClusterResources is set to false", + clusterIncludes: []string{"persistentvolumes"}, + clusterExcludes: []string{}, + includeClusterResources: &falseBoolean, + filterType: "global", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: true, + }, + { + name: "GlobalResourceIncludesExcludes: resource is in the include list", + clusterIncludes: []string{"persistentvolumes"}, + clusterExcludes: []string{}, + includeClusterResources: nil, + filterType: "global", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + { + name: "ScopeResourceIncludesExcludes: resource is in the include list", + clusterIncludes: []string{"persistentvolumes"}, + clusterExcludes: []string{}, + filterType: "scope", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + { + name: "ScopeResourceIncludesExcludes: filters are all default", + clusterIncludes: []string{}, + clusterExcludes: []string{}, + filterType: "scope", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + { + name: "ScopeResourceIncludesExcludes: resource is not in the exclude list", + clusterIncludes: []string{}, + clusterExcludes: []string{"namespaces"}, + filterType: "scope", + resourceName: "persistentvolumes", + apiResources: []*test.APIResource{ + test.PVs(), + }, + resourceIsExcluded: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := logrus.StandardLogger() + + var ie IncludesExcludesInterface + if tc.filterType == "global" { + ie = GetGlobalResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, tc.clusterIncludes, tc.clusterExcludes, tc.includeClusterResources, *NewIncludesExcludes()) + } else if tc.filterType == "scope" { + ie = GetScopeResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, []string{}, []string{}, tc.clusterIncludes, tc.clusterExcludes, *NewIncludesExcludes()) + } + assert.Equal(t, tc.resourceIsExcluded, ie.ShouldExclude(tc.resourceName)) + }) + } +} + +func setupDiscoveryClientWithResources(APIResources []*test.APIResource) *test.FakeDiscoveryHelper { + resourcesMap := make(map[schema.GroupVersionResource]schema.GroupVersionResource) + resourceList := make([]*metav1.APIResourceList, 0) + + for _, resource := range APIResources { + gvr := schema.GroupVersionResource{ + Group: resource.Group, + Version: resource.Version, + Resource: resource.Name, + } + resourcesMap[gvr] = gvr + + resourceList = append(resourceList, + &metav1.APIResourceList{ + GroupVersion: gvr.GroupVersion().String(), + APIResources: []metav1.APIResource{ + { + Name: resource.Name, + Kind: resource.Name, + Namespaced: resource.Namespaced, + }, + }, + }, + ) + } + + discoveryHelper := test.NewFakeDiscoveryHelper(false, resourcesMap) + discoveryHelper.ResourceList = resourceList + return discoveryHelper +} diff --git a/site/content/docs/main/api-types/backup.md b/site/content/docs/main/api-types/backup.md index 69c515c440..3393769cc7 100644 --- a/site/content/docs/main/api-types/backup.md +++ b/site/content/docs/main/api-types/backup.md @@ -68,6 +68,26 @@ spec: # PersistentVolumeClaim is included in the backup, its associated PersistentVolume (which is # cluster-scoped) would also be backed up. includeClusterResources: null + # Array of cluster-scoped resources to exclude from the backup. Resources may be shortcuts + # (for example 'sc' for 'storageclasses'), or fully-qualified. If unspecified, + # no additional cluster-scoped resources are excluded. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + excludedClusterScopeResources: {} + # Array of cluster-scoped resources to include from the backup. Resources may be shortcuts + # (for example 'sc' for 'storageclasses'), or fully-qualified. If unspecified, + # no additional cluster-scoped resources are included. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + includedClusterScopeResources: {} + # Array of namespace resources to exclude from the backup. Resources may be shortcuts + # (for example 'cm' for 'configmaps'), or fully-qualified. If unspecified, + # no namespace resources are excluded. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + excludedNamespacedResources: {} + # Array of namespace resources to include from the backup. Resources may be shortcuts + # (for example 'cm' for 'configmaps'), or fully-qualified. If unspecified, + # all namespace resources are included. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + includedNamespacedResources: {} # Individual objects must match this label selector to be included in the backup. Optional. labelSelector: matchLabels: diff --git a/site/content/docs/main/api-types/schedule.md b/site/content/docs/main/api-types/schedule.md index 7ae6d9e9d3..0d5c2aabcf 100644 --- a/site/content/docs/main/api-types/schedule.md +++ b/site/content/docs/main/api-types/schedule.md @@ -69,6 +69,26 @@ spec: # PersistentVolumeClaim is included in the backup, its associated PersistentVolume (which is # cluster-scoped) would also be backed up. includeClusterResources: null + # Array of cluster-scoped resources to exclude from the backup. Resources may be shortcuts + # (for example 'sc' for 'storageclasses'), or fully-qualified. If unspecified, + # no additional cluster-scoped resources are excluded. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + excludedClusterScopeResources: {} + # Array of cluster-scoped resources to include from the backup. Resources may be shortcuts + # (for example 'sc' for 'storageclasses'), or fully-qualified. If unspecified, + # no additional cluster-scoped resources are included. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + includedClusterScopeResources: {} + # Array of namespace resources to exclude from the backup. Resources may be shortcuts + # (for example 'cm' for 'configmaps'), or fully-qualified. If unspecified, + # no namespace resources are excluded. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + excludedNamespacedResources: {} + # Array of namespace resources to include from the backup. Resources may be shortcuts + # (for example 'cm' for 'configmaps'), or fully-qualified. If unspecified, + # all namespace resources are included. Optional. + # Cannot work with include-resources, exclude-resources and include-cluster-resources. + includedNamespacedResources: {} # Individual objects must match this label selector to be included in the scheduled backup. Optional. labelSelector: matchLabels: diff --git a/site/content/docs/main/manual-testing.md b/site/content/docs/main/manual-testing.md index 136b490bb2..935bc8792c 100644 --- a/site/content/docs/main/manual-testing.md +++ b/site/content/docs/main/manual-testing.md @@ -90,3 +90,9 @@ The following are test cases that are not currently performed as part of a Veler - `--exclude-namespaces` - `--exclude-resources` - `velero.io/exclude-from-backup=true` label + +- Since v1.11, new resource filters are added. The new filters only work for backup, and cannot work with old filters (`--include-resources`, `--exclude-resources` and `--include-cluster-resources`). Need to verify backups correctly apply the following new resource filters: + - `--exclude-cluster-scope-resources` + - `--include-cluster-scope-resources` + - `--exclude-namespaced-resources` + - `--include-namespaced-resources` diff --git a/site/content/docs/main/resource-filtering.md b/site/content/docs/main/resource-filtering.md index 02ae3d68d0..9a34fd2281 100644 --- a/site/content/docs/main/resource-filtering.md +++ b/site/content/docs/main/resource-filtering.md @@ -31,7 +31,7 @@ Namespaces to include. Default is `*`, all namespaces. ### --include-resources -Kubernetes resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use `*` for all resources). +Kubernetes 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`. * Backup all deployments in the cluster. @@ -53,7 +53,7 @@ Kubernetes resources to include in the backup, formatted as resource.group, such ### --include-cluster-resources -Includes cluster-scoped resources. This option can have three possible values: +Includes cluster-scoped resources. Cannot work with `--include-cluster-scope-resources`, `--exclude-cluster-scope-resources`, `--include-namespaced-resources` and `--exclude-namespaced-resources`. This option can have three possible values: * `true`: all cluster-scoped resources are included. @@ -99,6 +99,36 @@ Includes cluster-scoped resources. This option can have three possible values: For more information read the [Kubernetes label selector documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) +### --include-cluster-scope-resources +Kubernetes 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`. This parameter only works for backup, not for restore. + +* Backup all StorageClasses and ClusterRoles in the cluster. + + ```bash + velero backup create --include-cluster-scope-resources="storageclasses,clusterroles" + ``` + +* Backup all cluster-scoped resources in the cluster. + + ```bash + velero backup create --include-cluster-scope-resources="*" + ``` + + +### --include-namespaced-resources +Kubernetes namespace resources to include in the backup, formatted as resource.group, such as `deployments.apps`(use '*' for all resources). Cannot work with `--include-resources`, `--exclude-resources` and `--include-cluster-resources`. This parameter only works for backup, not for restore. + +* Backup all Deployments and ConfigMaps in the cluster. + + ```bash + velero backup create --include-namespaced-resources="deployments.apps,configmaps" + ``` + +* Backup all namespace resources in the cluster. + + ```bash + velero backup create --include-namespaced-resources="*" + ``` ## Excludes @@ -124,7 +154,7 @@ Namespaces to exclude. ### --exclude-resources -Kubernetes resources to exclude, formatted as resource.group, such as storageclasses.storage.k8s.io. +Kubernetes resources to exclude, 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`. * Exclude secrets from the backup. @@ -141,3 +171,33 @@ Kubernetes resources to exclude, formatted as resource.group, such as storagecla ### velero.io/exclude-from-backup=true * Resources with the label `velero.io/exclude-from-backup=true` are not included in backup, even if it contains a matching selector label. + +### --exclude-cluster-scope-resources +Kubernetes 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`. This parameter only works for backup, not for restore. + +* Exclude StorageClasses and ClusterRoles from the backup. + + ```bash + velero backup create --exclude-cluster-scope-resources="storageclasses,clusterroles" + ``` + +* Exclude all cluster-scoped resources from the backup. + + ```bash + velero backup create --exclude-cluster-scope-resources="*" + ``` + +### --exclude-namespaced-resources +Kubernetes namespace resources to exclude from the backup, formatted as resource.group, such as `deployments.apps`(use '*' for all resources). Cannot work with `--include-resources`, `--exclude-resources` and `--include-cluster-resources`. This parameter only works for backup, not for restore. + +* Exclude all Deployments and ConfigMaps from the backup. + + ```bash + velero backup create --exclude-namespaced-resources="deployments.apps,configmaps" + ``` + +* Exclude all namespace resources from the backup. + + ```bash + velero backup create --exclude-namespaced-resources="*" + ```