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