diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index df5fe1fc7..8167e0383 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days. If a Velero team member has requested log or more information, please provide the output of the shared commands." diff --git a/changelogs/unreleased/7000-qiuming-best b/changelogs/unreleased/7000-qiuming-best new file mode 100644 index 000000000..61b3ade5d --- /dev/null +++ b/changelogs/unreleased/7000-qiuming-best @@ -0,0 +1 @@ +Make Kopia file parallelism configurable diff --git a/changelogs/unreleased/7100-blackpiglet b/changelogs/unreleased/7100-blackpiglet new file mode 100644 index 000000000..1084a29d1 --- /dev/null +++ b/changelogs/unreleased/7100-blackpiglet @@ -0,0 +1 @@ +Generate VolumeInfo for backup. \ No newline at end of file diff --git a/changelogs/unreleased/7115-reasonerjt b/changelogs/unreleased/7115-reasonerjt new file mode 100644 index 000000000..5824427b0 --- /dev/null +++ b/changelogs/unreleased/7115-reasonerjt @@ -0,0 +1 @@ +Include plugin name in the error message by operations \ 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 5314b862c..9099eb13f 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -477,6 +477,15 @@ spec: description: TTL is a time.Duration-parseable string describing how long the Backup should be retained for. type: string + uploaderConfig: + description: UploaderConfig specifies the configuration for the uploader. + nullable: true + properties: + parallelFilesUpload: + description: ParallelFilesUpload is the number of files parallel + uploads to perform when using the uploader. + type: integer + type: object volumeSnapshotLocations: description: VolumeSnapshotLocations is a list containing names of VolumeSnapshotLocations associated with this backup. diff --git a/config/crd/v1/bases/velero.io_podvolumebackups.yaml b/config/crd/v1/bases/velero.io_podvolumebackups.yaml index 3922fdc07..a642f3949 100644 --- a/config/crd/v1/bases/velero.io_podvolumebackups.yaml +++ b/config/crd/v1/bases/velero.io_podvolumebackups.yaml @@ -121,6 +121,14 @@ spec: description: Tags are a map of key-value pairs that should be applied to the volume backup as tags. type: object + uploaderConfig: + description: UploaderConfig specifies the configuration for the uploader. + properties: + parallelFilesUpload: + description: ParallelFilesUpload is the number of files parallel + uploads to perform when using the uploader. + type: integer + type: object uploaderType: description: UploaderType is the type of the uploader to handle the data transfer. diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index a334ee6ce..0cf3cbe52 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -514,6 +514,16 @@ spec: description: TTL is a time.Duration-parseable string describing how long the Backup should be retained for. type: string + uploaderConfig: + description: UploaderConfig specifies the configuration for the + uploader. + nullable: true + properties: + parallelFilesUpload: + description: ParallelFilesUpload is the number of files parallel + uploads to perform when using the uploader. + type: integer + type: object volumeSnapshotLocations: description: VolumeSnapshotLocations is a list containing names of VolumeSnapshotLocations associated with this backup. diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index f0814a70a..fff600dc1 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\xb4VAo\xe46\x0f\xbdϯ \xf6;\xec\xe5\xb3g\xb7\xbd\x14\xbem\xd3\x16\b\x9a\x04A\x12\xe4Nۜ\x19mdI\x95\xa8I\xa7E\xff{A\xc9\xcexl'\xb3Y\xa0\xbaY\xa2\x1e\xc9G>ZEQ\xacЩG\xf2AYS\x01:E\x7f2\x19\xf9\n\xe5\xd3O\xa1Tv\xbd\xff\xbczR\xa6\xad\xe0\"\x06\xb6\xdd\x1d\x05\x1b}C\xbf\xd0F\x19\xc5ʚUG\x8c-2V+\x004\xc62\xcav\x90O\x80\xc6\x1a\xf6Vk\xf2ŖL\xf9\x14k\xaa\xa3\xd2-\xf9\x04>\xb8\xde\x7f*?\xffP~Z\x01\x18쨂\x1a\x9b\xa7\xe8<9\x1b\x14[\xaf(\x94{\xd2\xe4m\xa9\xec*8j\x04}\xebmt\x15\x1c\x0f\xf2\xed\xdes\x8e\xfa\xe7\x04t7\x00\x1dґV\x81\x7f_<\xbeR\x81\x93\x89\xd3ѣ^\n$\x1d\ae\xb6Q\xa3\x9f\x19\x88\x83\xd0XG\x15\xdcH,\x0e\x1bjW\x00}\xa6)\xb6\x02\xb0m\x13w\xa8o\xbd2L\xfe\xc2\xea\xd8\r\x9c\x15\xf05Xs\x8b\xbc\xab\xa0\x1c\xd8-\x1bO\x89\xd8\a\xd5Q`\xec\\\xb2\x1d\b\xfb\xb2\xa5\xfe\x9b\x0f\xe2\xbcE\xa69\x980W\x1ec}88:A9\x12\x01\xa3\xb3\x8c\x18\xd8+\xb3]\x1d\x8d\xf7\x9f3\x15͎:\xacz[\xeb\xc8|\xb9\xbd|\xfc\xf1\xfed\x1b\xc0y\xebȳ\x1aʓר\xfdF\xbb\x00-\x85\xc6+ǩ9>\n`\xb6\x82V\xfa\x8e\x02\xf0\x8e\x06N\xa9\xedc\x00\xbb\x01ީ\x00\x9e\x9c\xa7@&w\xe2\t0\x88\x11\x1a\xb0\xf5Wj\xb8\x84{\xf2\x02\x03ag\xa3n\xa5]\xf7\xe4\x19<5vk\xd4_/\xd8\x01\xd8&\xa7\x1a\x99\xfa\x1e9\xaeTC\x83\x1a\xf6\xa8#\xfd\x1fд\xd0\xe1\x01<\x89\x17\x88f\x84\x97LB\t\xd7\xd6\x13(\xb3\xb1\x15\xec\x98]\xa8\xd6\xeb\xad\xe2Av\x8d\xed\xbah\x14\x1f\xd6IA\xaa\x8el}X\xb7\xb4'\xbd\x0ej[\xa0ov\x8a\xa9\xe1\xe8i\x8dN\x15)t\x93\xa4Wv\xed\xff|/\xd4\xf0\xf1$\xd6Y-\xf3Jby\xa3\x02\xa2\x16P\x01\xb0\xbf\x9a\xb38\x12-[\xc2\xceݯ\xf7\x0f0\xb8NŘ\xb2\x9fx?^\f\xc7\x12\ba\xcal\xc8\xe7\"n\xbc\xed\x12&\x99\xd6Ye8}4Z\x91\x99\xd2\x1fb\xdd)\x96\xba\xff\x11)\xb0Ԫ\x84\x8b4\x8b\xa0&\x88N\xd4Жpi\xe0\x02;\xd2\x17\x18\xe8?/\x800\x1d\n!\xf6\xdbJ0\x1e\xa3S\xe3\xcc\xda\xe8`\x18\x81\xaf\xd4k:\xd6\xee\x1d5R>aP\xae\xaa\x8dj\x926`c=\xe0̾<\x81^\x96\xae\xac<\xfc\xee\xd9z\xdcҕ͘S\xa3\xc5\xd8&w\x86\xe0d\xb2d\x19Ӳ\xe1\f\x1b\x80w\xc8#\xfd2*\xf32\x06\x16\xf3y\xa3\b\xa9\x10(r6h\x1a\xfa-u\x94i\x0egr\xba^\xb8\")\xed\xec3\xd8\r\x93\x19\x83\xf6\xb1.dR\x13\xf8h\xde\x15\xec\xe90?\x13\xe6݉1(\xd3J\x1b\xf4\xd3T\x9c\f\xd4K]ɴ\xe0O\xff\x9b\xe3E&vsw\x05\xfdy\xf3\xf6_7\x7f~E\x88\xa0\x05ܒ\x1d͞\xaaRoN\xc0A\xc9\r\x93\xaft\t\x99\x05yP\xb2*oI\xf3\x83\xeb\xe2\x87s\xa8\xfe\x05{\xe3\vδ\xf9\xa9\xf5\xf2g\xa6\r\xfeP\xf2JQ^\x8f\x84\xef4\x13\x87\x8aS\x15\u07be\"Dg\xb2\x84[\xf2\xc9\x0eQ\xd2\f\xf2W\x84x\xacqȵG\xf8\xf4\xd6AȎPP\x87\v!\xb2\x04\xf1\xee~\xfb\xe5\xdf\x1e:\xaf\t\xc9Ag\x8a\x95\x06\xe7\xee\x10#L\x13J\xbeഈ\xf2T&\xe6H\rQP*\xd0 \x8c&\xe6\b$\xa3\xa5\xa9\x14\x10\xb9'?U;P\x02\f\xe8\x1a4!\x19\xaf\xb4\x01E\xb4\xa1\x06\b5\x84\x92R2a\b\x13İ\x02\xc8\x1f\xde\xddo\x89\xdc\xfd\x06\x99ф\x8a\x9cP\xadeƨ\x81\x9c\x9c$\xaf\np}\xff\xb8\xa9\xa1\x96J\x96\xa0\f\vtvOKxZo{\xd3{m)\xe0Z\x91\xdcJ\r\xb8ix*B\xee\x89f\xe7c\x8eL7\xd3E9\xea\x00&\xb6\x11\x15\x1e\xf9\ry\x00e\xc1\x10}\x94\x15ϭ\xb0\x9d@Y\x82e\xf2 \xd8?jؚ\x18\x89\x83rj\xc0\v@\xf30a@\t\xcaɉ\xf2\nn\x90$\x05=\x13\x05v\x14R\x89\x16\x93\x96r\x88\xfe؛G\xc3H+\xa98\xf31ԭ#зh᱈Z\x0f\x17-g\xceN,\xaf(G#JE\xe6\xe6Ck\xbcb\xc6x\x82\xc9\x03\x9c\x9d\x89\x0e\x98[Nt\xc2\x11)\xc0\xba\xa0\x85\x8d\xc1\x86McF\xc6=c\xd3\xdeQ\xebgH'\xa2\xaa\xe2\xa0\xfdPαkt\xc0\xcd(\xe8\x9a#.~\xe7t\a\x9ch\xe0\x90\x19\xa9\xe2\xe4\x98c\xb2{R\xf4\xda\b\x15#\x1a\xae\x1b\x104\x13\x9b\x00I0\xa8:\xb2\xec\xe8\xdc4+A\b\x87\xe4\x124\xaerZ\x96\x055\xd9\xf1\xc3W\xeby\xe9&\x97\x93H\x97~g\xe7\xbf\x06\x7f\xbek\x98g\xe0\x12\xdc\x14f\n\n\xb7\xd9\xfc\x88\xd4lޠG\xf5\xee\xd3\xfb\xd8nV\xf7I\x90\xbc\xc1D\xde\xf5\x90m\x0f\xed\x9d\xf2\xd4ixק\x8eo\\\x1a\xe1\x86P\xf2\x04g\xe7\xb1PA,s\xa8\x1dh$\xd2\x19\x12\a\xf3\x19(dOpF0>A1\xdb;U\x14\xdc\xf3\x04\xe7\x94f=\x02Z\x9c\x98\xf6\x89\x17KI\xfb\x02\t\x81\xfb\xd9\xe9\xc4#\x98l\n\xbah~r$]\x91\x84'\xd0\xfe\x82i\xd6lk%ꐱ\xaf\xb5c\x91]\x05GV&NԚ9\xdcJ\x90\xfb:\xdd\xf4\x85r\x96\xd7\x039\xb9ߊqo\xb8\xfb|\x92f+nȇ\xafL\xfb\x8c\xdf{\t\xfa\x934\xf8\xe6E\xc8\xe9\x10\xbf\x80\x98\xae#./\xe1Զ\xa5C;o\x95 \xdc\xeeٺ\b\xaff\x0f\xd3d+l\xdc\xe2\xe9\x81YH7ܴ}\xe8>E\xa511%\xa4X\xbb\xad\x97\xd8H\x8e؉ \xa5\xeapd\x88Z=\xe8\xc8^O\xfcy\xb4\x96\xc4\xf5wyUN3\xc8C^\x05\xb3\x81\xd4\xc0\x81e\xa4\x00u\x982\x1c\xed\xa7\xb4\xfa=\r\x85D\xad랅\x12\x96f\xda\xc3\xe3Uwt\xf3\xbb\xfb\xac\xed\xcaMh\x15\x98=\xdbt$\t8\xdet~Fhb\xd1\xff\x98\xa5.\xcds,Ӡ\xfc~\x81\xc6_\xc0\x8b\xa1\xedw\x889\vYPLN\xfc\xb75s(\xd0\xffCJ\xcaT\xc2\x1a~\x87\xe5\x18\x1c:}\xfd.V{\x18;\x02\xd3\xc4\xf2\xf7D\xf90\xbd\x1c\x99\x9c\xb4\xba\x05\xb83\xe4r?\xf0Xn\xc8\xf3QjgS1)2\v\x92i\xb2z\x82\xb3OƵ\xf5\xc0j+V\xce\xc0/V7\xb5\xb7 \x05?\x93\x15\xf6]}\x8b\x13\x94(\x89\x89;\xae\x9f\xea\xf2\x93uA˵\x97^#\v\x96\x8d\xf6\xc3r\x99T\x17\xdbƠ\xc1\x83\xb0\x1d\xeb\x1a\x11\xeb\x1eO\xcd6I~K\xa9#\x99\xef\x11T\xee\xa56nG\xb2\xe3\xce.\xd9\xfd\"N\xf6\xfc\xae\x17\xa1{W\xa5#U\xa8\xbf\xb0겷Qk\xb9\xad\xa75\xb3\xcb\x19\xf8\x9d4\a\xd4\x06d\xabf\xe5;=\xbcr9\v\x1c\x84f\xe8\x94\xcc\xc2-\x95\xcc@G\xb3\xc5͓\xa0\xe5g6\x17\xeb\x8dE\xea\x02\x1fW\xdc0\xbd\x99\x19\x9etG\xd6\x12ia\b\xf0\xe1kk\xd7\xd3*\r\xfb\xf7\x9c\xf0-ŋ\xe0Z/\nگ\xe2IB\xf1\xce\xf5\f\xcb\xc4\x03r!\x85:T\xa8\"\xd2=O/H߃y/\x98\xd8\xe2\x00\xe4\xed\xd5݁Z\xb9\xc6j9bO\x8f\xe4\xbeoC\xf4\xfa\x85\x18)\xe6\x88=\xa5\xc4\x1d\x7f\x05\x1d\xce\r\xf7ǭ\x83\x99\bRH\xd3ކ\xb0pK\x99\xbf\xd6dϔ6mDS\x85\"^+\x12{\x96F\\\xe2\x83R\x17\x05\\\xbf\xba\x9e\xad\r\xb0\xa3|\x0e\xb5P\xa3\xc5\x13\xb1\a\x93I@؞0C@d\xb2\x12\xb8mc\x97:\x0e\xe1X\xe0\x14t2\xc9\xd2\x14\x84}@TE\x1a\x01\xd6(uLL\xeeﴛ\x7f\xa4,\x96\x81\x1e>\v\xd9f\xc6J\xc6bO\x87m\xa1v\xac]\xd4VЯ\xac\xa8\nB\vK\xfa\xd4pi\xef*\xce:\x1c\xaf\xeb\xce\x10.\x9a\x11#\xed\xa2*9\x98\xd4\x15\xe9*\xcc\xec2\xd1,\x87\xda0{)\x90\x82P\xb2\xa7\x8c\x8f\x94\xbb\f\x9fE\xb4]\x12\xa3xeq\xbd\xe0#m\xf05\x92\"a\x037\xd1ɜ\xd6֥Jw\x15\xef\x15\xa4\xb9gs\x9b\xd9\xc1=+\x15\x93X\xa7we\x0f͋\x18\x15\xe7\x1f.\xda\xe0\xf9\xe1\xa2\xcd3ś\xdfP/\x1f\x06\xbd\xa8^~;\xd9\xf9J\xf5\xf2\x1eþ\xd7}\xa5j\xf90\xffe\xd5\xf27\xbeT\xa3\x00\x1a\xb6\xe7].>\x1f\x1b\xb27\xda\x00\xf0\xef\\\x7f;\xa8\x0f\xbb\x8c\xf1/^m?\xc2\xfc\xc4\xc2\xf8՟V\xdf\x1f\xa5\x17\xd3v\x94\x9a\x032E&\x15\x8e\xf9ڸ\xb2]\xdc\xd5-\xa4\xfb>\x85s\xa94\xa6V\xccO\xd1k\xa8eZ\x04\xfb^\x17\xb3\x81\xe2\xd7\xd2ۊ\xb4\x13\x9d\xdbH\x97\xb93\x9d\x91\xf9\xe0&\x80>\x8b쨤\x90\x95\xf6\xfb\x06\x16\xfa;ܾ\xf0\xa9P,\x03KT\xb0o\xc9QV\x91\x8a\xed\t\xda\xcd\xd4\xef\x8dW\xed\xf9\x1c5\x18zz\xbb\xe9\xfeb\xa4\xaf\xe1#\xcf\xcc\x1c#x>\x1fA`v]\x1c\xda\x05\xf9a\xc1\xf9\x03\xe7}A\"R\x11\xc1\xf8\x98\xc1\xaaO\xe5wLӯ\xa5\xdb$Zl\xf9\xa778Ҫ\xfc.\xae\xed\xeb\xd6\xee\x8d8\x81K\x93\xd9\xe9G\x18ҫ\xf7\xa6\xcb\xed\x96\xd4\xec\xf5+\xf2F\x81\xceW\xea\xa5\xecM\xcdT\xe5]P\x8b\x97X\x87\xfdͩ\xf7\x94j\xbb\x8bj\xecfK\x95\x13+\xeb\xba5s\xd3 \x17\xd4\xd3%\x11g\xbevnqŜ\xafP\x9b\x9cGr\x9d\\\xa4\x02n\x12\xf0hu\xdcT\xdd\xdb̾\xf7\xb0&.\xbd\xdam\x124V\xc2\xcd\u05f8]\xaf\x92\xfd\x1aQ\xf6\xb8\xaa\x99\xadS\x9b\x8d§\xf1\x9b\xadD[R\x7f6K\xb1\vk\xcd\xeaZ\xb2\x91q\x97V\x98u+\xc8F\x80\xa6ԕ\x8dԍ\x8d@\x9c\xac&K\xad\x16\x1b\x81=cv'\xa5d\xf2\xc7%Ub\xf1KTȬ5俗\xfc]J\x06\xa9:\xce\xe5\\@\xf3k\xaf\xb9\xe5|𱦝\u0558\x9f\xca\xccq\xb9\xb3ZTܰ\x92cz\xf1\xc4\xf2h\xccn\x8ep\xae/\x86\xf8M\xe2qMw\x99\t\xf9\xf5s-̛\x9e\xcbM5y\x06\xce\t\x8d\x89\xe2`晻\a(\x93k\xb0\x16\xc2.O\x7f兿.\xe8\xc6\xc9;\x9eH\x8de`\xcc\x11\n\ve\xfcړQU>\xedN:\xcf\x17\xdf\xfd\xbd\x02u&x=K\xed_̜\x87r\xcbR\xdbX((\n\xafm\xdc\xedS=7\xbbY\x9e\xe4\x9dp\x06/\n\xb6\x87#±\x1a\x82\u05fc\xb6\xca\xd0F\r#M\xe3\x1b\xb1\xb2\xee\x1d\xf9}\xceSM=L\xf4\xb2\x81\xc6\xf2Pc\xd6ȿH\xb8qy\xc01\x012\xf5pPZB|\xf60\xd0K\x05\x1es\xa1G\xb2ϕv\xd8\xe7%\x0e\xf9,8ܳ \x04Y\x16\x84$\x93)\xe5\x10ϋ\x84\"/\x18\x8c\xbcD8rY@2\x03\xb2w8'\xe5\xd8MR\xb1Gr\xbe3\xa5Xc>%9}\x9c&\xe1\x18MB\xb2r\x0eӄ\xe32ˎ\xc9$\xd0\xf0\x85B\x95\x17\nV^\"\\yـe6d\x99\x95\x9c\x99\x9f\x97\x1do\xb9x\xf3^\xaa\x1c\xd4d\xae#U4'\x85\xb2\x17_t\xc7\xec\xed\xfc\x87;\xe5l\xab\x8e+\x1b۰\xaeO\xbdg\xe4'&|\x1e\xd5\na\xcb\xeew\x120\x8d#\x12\xdf\xffo\xbc<\x7fۨ\xcb\xdah(\xa9\xc2\f\xeb\xee\xecJ+\xf4\x86|\xa0ٱ\a\xfd\x18\x8d+\xf6R\x15ԐU\x9d\xf2z\xe3\x80ۿW\x1bB>\xca:i߾IF\xb3\xa2\xe4g\x1b7D`\xae\xda .\x13\x88\xa8\xf0\x85\xf1\xef%gY\xc4ӊ^.\xe4\x1a\x0f\xae\x84\xc0+\x8f\xb2v껴\r\xe3\x8e\x16:e\xdd\xeb\x15\xf7\x92s\xf9\xbc0\x1c\xa7%\xfb\x0f\xbc\xa5y~\x0f\xe7\xdd\xfd\x16\x9b\x06I\xc1\u06dd\xeb\n\xa1\x1a\xe9\x1dX\x8b\xd9Lgl\xc5o\xf7\x1d\x88\x91J\xbb\xfaO\x94\xd6\xdab\xb3\xb1[\x97\\՟\xd54\xf7[\x87\xdd\x06\x85\x85\x8a3\x91X\xeba\x8eL\xe5\xeb\x92*svu\x0575\x0e\xe3\xfb8\xc1nN\xed\xb6\x8c\x9a\x97\xe1u\xbfQچ[\x7f1\x99w.\xbb\xa9\xd0>E/\xc1c\xfc(\xdf\xec!\xbe+\xe21\ue0ac\x91R\x91\xd7Ѣ\xa4\xab\xedbi\x7f\xb5\xed/\xf2\x04\uf8fbY\x1d\xf2<\xf4\x9aGʉ\x02Dw\xb9\xeb\xd4\xed\xa0x9\xe7e\xba(^\x1f\x14\x86\xf6\xd7w&\xceŷ\x8eL%\xdc\\\x1a\xe0\xea\xf8\xae\x8d]^\xf7_0\xb4\xaaU\x98wv|\xf0\x14\xb6\xae\xfa\x17\xdc\xfd\xe5\xfa5R\xdaHE\x0f\xf0\xb3tW/\xcfѠۺs\xef\xb6wyB\xcdbX\r\xb1P\xc0_\x02\xdd\x03֔\"\x0f\xae\xc1\xb5X.\xbc\xd5\xd7\x18>3\x99\xc7ǟ\xdd\x04\f+`\xf3\xber\xb9|\xab\xed4Xj\x86\x89\xb9N;\xfb\xdfc\xc4^\x10\xbcO\xb6ş\x16\xde\n\xb0\xdc\x19\xcb\xde\x16a\x7f\xea\\$\x1dH4'\xa2_\xe2\xbdZ\xfbK-&9\xd7#*\xa1cpZw\xe9\xe3\xce+\x1eW\xbe\ueb4bc\xce\xe4\xd8m\xe3x\xc3\xf6\xfc}\xe3\xee\"n\xffu\x01_4_)\xbc2\xd1_ҍW\f^t\xe5\xf8\xae\xae\v\xa9\xabN\xf4;cl\xa0\x1c\xd3\xdc\x11\xf4F\xfa\xd6\x06N\x1aʉ\xa8\x8a\x1d\xfa\xac1\x95Rw\xc1\x8a\x95\xc9R\x15\xe7\x80L0Α\x9a\t\x03\x87\xc1\xa6{l\xaew\xbe\xc6\xf9\x92\xb9\xd6}\xd3窫,\x03\xad\xf7\x15\xe7纾z\xc9\xc4c\xd6\xe5J\xa4\xf8H\x19\xbf\x88\x0e\xae\xe3\b\x11\xdc\xdcF\xf5h\x12\x9b}Q'\x88<,ށ)\xb0\x0f\x9e:XF\a\xcf\x02_k\xa5\r-\xe6nN\xbf\x1b\xf6\xc0\xcfZ\xa8\xbcU\x9dU_\xff\xfdLu\xc3\xe6\x98Oـs=\xd1\x05\xb5\xd0 'p\x02A\xa4\xc0\xaay\xbc\x8b\xd3}z\xa5\xdf'\x02\xb5\rŗ\xe5W%\x974\x0f\x06.D\x92\xfes\x1d\x8fh\xbe\xd5\t\xd4k=\x01\xb3\xbe\xd0=B\x84\xa1d\xba\xd0\xee\xd6\xfaF\xb0\x8e\x02M2\xfdQ]\x9bi\xd6\xd5\xf3\xc9J\xeb\xeea;\xd6sT\x82C\x83\x18\xff\x06\x1fN\xf8F%5\x9cY\xaa\x8a\x1a\xcelNAu\xd4Qdr\x8d\x82\xba\xfa4q\xad\xce\u07b8\x8c\x8d\x9c\a\x80Nj\xc2u\xfa\xee|Q\x01Z\xd3C\xb8j\xf9\xd9:`\a\x10\x80\x9b\x11\x91\xd9\xf8\xed\xdd\xe6\x027\xaf,R\xa4D\r\xa1C\x99\x86\xa9\x94h\xa5r\xea\xf3b\r\xba4{\x1a\xc5ԧ\xa2\xc3G\x0f\xdf\xf8\x8b\xd3\xd7{%\x8b\xb5\xe7\x05VW\xdc\xf8\xf4\x8ab\xd2\x06\xec\xe6\x18%9q\xdfT\xf27\x14\xa3\x18\x94%\bB\xb5\xc7'\xe1b\x89i\xb6N\xec\xa6jC\x95I\x8d\x83\x1e:\x8dgB \x84\x1c\xc7\xf7\xc1\xa7\x8f\xdc\x05\x1bw\xfe\x93b5\xe0\x1b\xa2\x99\b\xdf[t\xc9)'\n\xdaFF\nps-Z83\x88i:\x11L\x17\xfd\xdf7x9\xd56\xf1C\x8a\x17\xfc\xa5\u05fcw\x88\n?\xaeU7\xf1\x9ek\x84\x1e\x7f`{W˓Y\xac\xff\xf8\x7f~8\xea\x94\xe4e\xbd\x9et\xb0\xd0w\xaa=\xa5\x99Oi\xdds\xb0\x9e\x8f\x06\xe8\xfan\xaf\x179\xe9\xa7\xcb\xc2\xcekƜ\xe1S\xa0\u05c9\xc4N\x97E\x9b/\x16j^wv\xcf\x14??8\xb7\xc6\xfe\xe6\x9bEbM\x0f!\x12mF\xa6Qǟ\xb3\xd1f+\xd8\f8\x8e|-\xa8\x17\x80^)܌ځ\xc1KT\xa0ykm\xfb\x91\xfc\x9b\xff\r\x00\x00\xff\xff\x9a\xfbL\xe1\xa9x\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=M\x93\xdb:rw\xff\n\x94r\xf0\xee\xd6H~N.)\u07fcc;Q\xbd\xf7\xec)\xcf<\xef%\x17\x88lIx\x03\x02\\\x00\xd4X\x9b\xca\x7fO\xa1\x01\xf0\x13$AY\xf3\xe2M\x99\x17{(\xa0\xd1\xe8n\xf4\x17\x1a\xe0z\xbd~AK\xf6\x05\x94fR\xbc!\xb4d\xf0Հ\xb0\x7f\xe9\xcd\xe3\xbf\xeb\r\x93\xafN\xaf_<2\x91\xbf!\xb7\x956\xb2\xf8\fZV*\x83w\xb0g\x82\x19&ŋ\x02\fͩ\xa1o^\x10B\x85\x90\x86\xda\xd7\xda\xfeIH&\x85Q\x92sP\xeb\x03\x88\xcdc\xb5\x83]\xc5x\x0e\n\x81\x87\xa1O?m^\xff\xeb\xe6\xa7\x17\x84\bZ\xc0\x1b\xb2\xa3\xd9cU\xea\xcd\t8(\xb9a\xf2\x85.!\xb3 \x0fJV\xe5\x1b\xd2\xfc\xe0\xba\xf8\xe1\x1c\xaa\x7f\xc5\xde\xf8\x823m~n\xbd\xfc\x85i\x83?\x94\xbcR\x94\xd7#\xe1;\xcdġ\xe2T\x85\xb7/\bљ,\xe1\r\xf9h\x87(i\x06\xf9\vB<\xd68\xe4\xda#|z\xed dG(\xa8Å\x10Y\x82x{\xb7\xfd\xf2o\xf7\x9dׄ\xe4\xa03\xc5J\x83sw\x88\x11\xa6\t%_pZDy*\x13s\xa4\x86((\x15h\x10F\x13s\x04\x92\xd1\xd2T\n\x88ܓ\x9f\xab\x1d(\x01\x06t\r\x9a\x90\x8cWڀ\"\xdaP\x03\x84\x1aBI)\x990\x84\tbX\x01\xe4Oo\xef\xb6D\xee~\x87\xcchBEN\xa8\xd62c\xd4@NN\x92W\x05\xb8\xbe\x7f\xde\xd4PK%KP\x86\x05:\xbb\xa7%<\xad\xb7\xbd齴\x14p\xadHn\xa5\x06\xdc4<\x15!\xf7D\xb3\xf31G\xa6\x9b\xe9\xa2\x1cu\x00\x13ۈ\n\x8f\xfc\x86܃\xb2`\x88>ʊ\xe7V\xd8N\xa0,\xc12y\x10\xec\x1f5lM\x8c\xc4A95\xe0\x05\xa0y\x980\xa0\x04\xe5\xe4Dy\x057H\x92\x82\x9e\x89\x02;\n\xa9D\v\x1e6\xd1\x1b\xf2\xabT@\x98\xd8\xcb7\xe4hL\xa9\u07fczu`&,\x9aL\x16E%\x989\xbfB\xf9g\xbb\xcaH\xa5_\xe5p\x02\xfeJ\xb3Ú\xaa\xec\xc8\fd\x96\x91\xafh\xc9ֈ\xba\xc0\x85\xb3)\xf2\x7f\t\x02\xa0_vp5g+\x8c\xda(&\x0e\xad\x1fP\xea'8`\x17\x80\x93/\xd7\xd5͢!\xb4}e\xa9\xf3\xf9\xfd\xfdC[\xf6\x98\xeeS\x1f\xe9\xde\x12Ȇ\x05\x96`L\xecA9&\xee\x95,\x10&\x88\xdcI\x1f\x8a.g \xfa\xe4\xd7ծ`\xc6\xf2\xfd\xef\x15h+\xe4rCnQ\x93\x90\x1d\x90\xaa̭dn\xc8V\x90[Z\x00\xbf\xa5\x1a\x9e\x9d\x01\x96\xd2zm\t\x9bƂ\xb6\x12\xec7vTk\xfd\x10t\xd9\b\xbf\x9cB\xb8/!\xeb,\x18ۋ\xedY\x86˂\xec\xa5j\xf4\x85SW\x9b\x0e\xc8\xf8\x92\xb5O\xa6ٽ\xa0\xa5>J\xf3\xc0\n\x90\x95\xe9\xb7\xe8!t{\xbf\xedu\b\xc8x\xd4P\xadT\x1ar\xbbΞ(3\x16\xbd\x01Lb\x01\x91/\xa8a\x02<\xd44\x95&\xa6R\x02W\xe9g\xa0\xf9\xf9A\xfe\xa6\x81\xe4\x15\nk\xa6\x00\xa7|Cv\xb0\x97\n\"p\x15\xd8\xfe\xb61(e\t\xa3\x11%Y\x99\ry8\x82%#\xad\xb8\xf1r\xcf4y\xfd\x13)\x98\xa8\fl\x06\xd0F\x18\x8cD\xa1\x86\x16\xf2\x04j\x86^﨡\xbf\xdav=2\xd9\xfe\x04\x01ؙ\xee<\xc9vg\xfbcdV\x9e\xabd\xbboAd\x9a\xacVD*\xb2r&pu\x83\xa0\xadQ5k&ZcD >1\xceø\xcbf\xee\b\xe8x\xa7\x1f\xe4\a\xed\x84t\x8e\x10#\xddZty:\x829\x82\"\xa5\f\xc6'\x82\xf7\x9eq \xfa\xac\r\x14\x9e*A\xe5\a\"\xe2r\xe0܃Ж\xa8\x1e\xe7\xe1\t\x927\x98\xc8\xdb\x1e\xb2\xed\xa1\xbdS\x9e:\r\xef\xfa\xd4\xf1\x8d\xdbF\xb8!\x94<\xc2\xd9y,T\x10\xcb\x1cj\a\x1a\x89t\x86\xc4\xc1\xfd\f\x14\xb2G8#\x18\xbfA1\xdb;U\x14\xdc\xf3\b\xe7\x94f=\x02Z\x9c\x98\xf6\x1b/\x96\x92\xf6\x05\x12\x02\xf3\xd9\xe9\xc4#\xb8\xd9\x14t\xd1\xfc\xe4H\xba\"\tO\xa0\xfd\x05Ӭ\xd9\xd6ڨCƾԎEv\x15\x1cY\x998Qk\xe60\x95 \xf7\xf5v\xd3\x17\xcaY^\x0f\xe4\xe4~+ƽ\xe1\xee\xf3Q\x9a\xad\xb8!\xef\xbf2\xedw\xfc\xdeI\xd0\x1f\xa5\xc17\xcfBN\x87\xf8\x05\xc4t\x1dqy\t\xa7\xb6-\x1d\xda\xfbV\t\xc2\ud7ad\x8b\xf0j\xf60M\xb6\xc2\xc6-\x9e\x1e\xb8\v醛\xb6\x0fݧ\xa84nL\t)\xd6.\xf5\x12\x1b\xc9\x11;\x11\xa4T\x1d\x8e\fQ\xab\a\x1d\xc9\xf5ğ\akI\\\x7f\xb7\xaf\xcai\x06y\xd8W\xc1\xdd@j\xe0\xc02R\x80:L\x19\x8e\xf6SZ\xfd\x9e\x86B\xa2\xd6u\xcfB\tK3\xed\xe1\xf1\xaa;\x9a\xfc\xee>k\xbbr\x13Z\x05f\xcf6\x1d\xd9\x04\x1co:?#4\xb1\xe8\x7f\xccR\x97\xe69\x96iP~\xb7@\xe3/\xe0\xc5\xd0\xf6;Ĝ\x85,(nN\xfc\xb75s(\xd0\xffCJ\xcaT\xc2\x1a~\x8b\xe5\x18\x1c:}}\x16\xab=\x8c\x1d\x81ib\xf9{\xa2|\xb8\xbd\x1c\x99\x9c\xb4\xba\x05\xb83\xe4r?\xf0Xn\xc8\xd3QjgSqSd\x16$\xd3d\xf5\bg\xbf\x19\xd7\xd6\x03\xab\xadX9\x03\xbfX\xdd\xd4ނ\x14\xfcLV\xd8w\xf5-NP\xa2$&6\xfb\xba~\xac\xcbO\xd6\x05-\xd7^z\x8d,X6\xda\x0f\xcbeR]l\x1b\x83\x06\x0f\xc2v\xackD\xac{<5\xdb$\xf9-\xa5\x8e\xec|\x8f\xa0r'\xb5q\x19Ɏ;\xbb$\xfbE\x9c\xec\xf9\xac\x17\xa1{W\xa5#U\xa8\xbf\xb0겗\xa8\xb5\xdc\xd6Ӛ\xd9\xed\x19\xf8L\x9a\x03j\x03\xb2U\xb3\xf2\x9d\x1e^\xb9=\v\x1c\x84f\xe8\x94\xcc\xc2-\x95\xcc@Gw\x8b\x9b'A\xcb\xcf$\x17\xeb\xc4\"u\x81\x8f+n\x98Nf\x86'ݑ\xb5DZ\x18\x02\xbc\xff\xda\xcazZ\xa5a\xff\x9e\x13\xbe\xa5x\x11\\\xebEA\xfbU\v\xd9f\xc6J\xc6bO\x87m\xa1v\xac]\xd4VЯ\xac\xa8\nB\vK\xfa\xd4pi\xef*\xce:\x1c\xaf\xeb\xce\x10.\x9a\x11#\xed\xa2*9\x98\xd4\x15\xe9*\xcc\xec2\xd1,\x87\xda0{)\x90\x82P\xb2\xa7\x8c\x8f\x94\xbb\f\x9fE\xb4]\x12\xa3xeq\xbd\xe0#m\xf05\x92\"!\x81\x9b\xe8dNk\xebR\xa5\xbb\x8aw\n\xd2ܳ\xb9dvp\xcfJ\xc5$\xd6\xe9]\xd9C\xf3\"F\xc5\xf9\x87\x8b6x~\xb8h3\xcf\x0f\x17m\xf4\xf9\xe1\xa2\xcd??\\4\xff\xfcp\xd1\xc2\xf3\xc3E\xfb\xe1\xa2M5\x9b\xd2\xd6s\x18\xb9\xd3q#?\xceb\x91\xb0\xad=\x85\xe2\x04|_\x85\xe1\xeb\xbcS+3\xb7\xf1^\x91:\xfe\xe4\xdap\xdd2%u\xa9\xa6] A\xbc\xdda\x9f\x99\xe2\xcdo\xa8\x97\x0f\x83^T/\xbf\x9d\xec|\xa5zy\x8fa\xdf\xeb\xbeR\xb5|\x98\xff\xb2j\xf9\x1b_\xaaQ\x00\r\xe9y\xb7\x17\x9f\x8f\r\xd9\x1bm\x00\xf8\x0f\xae\xbf\x1dԇ]\xc6\xf8g\xaf\xb6\x1fa~ba\xfc\xea/\xab\xef\x8fҋi;J\xcd\x01\x99\"\x93\n\xc7|m\\\xd9.\xee\xea\x16\xd2}\x9f¹T\x1aS+\xe6\xa7\xe85\xd42-\x82}\xaf\x8b\xd9@\xf1\xa9\xf4\xb6\"\xedD\xe76\xd2e\xeeLgd>\x98\x04\xd0g\x91\x1d\x95\x14\xb2\xd2>o`\xa1\xbf\xc5\xf4\x85\xdf\n\xc52\xb0D\x05\xfb\x9a\x1ce\x15\xa9؞\xa0\xddL\xfd\xdex՞ߣ\x06CO\xaf7\xdd_\x8c\xf45|䉙c\x04ϧ#\b\xdc]\x17\x87vA~Xp\xfe\xc0y_\x90\x88TD0>f\xb0\xeaS\xf9\x1d\xd3\xf4\xa9tI\xa2Ŗ\x7f:\xc1\x91V\xe5wqm_\xb7vo\xc4\t\\\xba\x99\x9d~\x84!\xbdzo\xba\xdcnI\xcd^\xbf\"o\x14\xe8|\xa5^Jnj\xa6*\xef\x82Z\xbc\xc4:\xeco\xdezO\xa9\xb6\xbb\xa8\xc6n\xb6T9\xb1\xb2\xae[37\rrA=]\x12q\xe6k\xe7\x16W\xcc\xf9\n\xb5\xc9y$\xd7\xc9E*\xe0&\x01\x8fV\xc7Mս\xcd佇5q\xe9\xd5n\x93\xa0\xb1\x12n\xbe\xc6\xedz\x95\xec\u05c8\xb2\xc7U\xcdl\x9d\xdal\x14>\x8d\xdfl%ڒ\xfa\xb3Y\x8a]XkVג\x8d\x8c\xbb\xb4¬[A6\x024\xa5\xael\xa4nl\x04\xe2d5Yj\xb5\xd8\b\xec\x19\xb3;)%\x93?.\xa9\x12\x8b_\xa2Bf\xad!\xff\xa3\xe4\xefR2H\xd5q.\xe7\x02\x9aO\xbd\xe6\x96\xf3\xc1ǚvVc~*3\xc7\xe5\xcejQq\xc3J\x8eۋ'\x96Gcvs\x84s}1\xc4\xef\x12\x8fk\xba\xcbLȧϵ0oz.7\xd5\xe4\t8'4&\x8a\x83\x99g\xee\x1e\xa0L\xae\xc1Z\b\xbb<\xfd\x95\x17\xfe\xba\xa0\x1b'\xefx\"5\xb6\x03c\x8ePX(\xe3מ\x8c\xaa\xf2iw\xd2y\xbe\xf8\xee\xef\x15\xa83\xc1\xebYj\xffb\xe6<\x94[\x96\xda\xc6BAQxm\xe3n\x9f\xea\xb9\xd9\xcd\xf2$o\x853xQ\xb0=\x1c\x11\x8e\xd5\x10\xbc\xe6\xb5U\x866j\x18i\x1aO\xc4ʺw\xe4\xf79O5\xf50\xd1\xf3\x06\x1a\xcbC\x8dY#\xff,\xe1\xc6\xe5\x01\xc7\x04\xc8\xd4\xc3Ai\x1bⳇ\x81\x9e+\xf0\x98\v=\x92}\xae\xb4\xc3>\xcfq\xc8g\xc1\xe1\x9e\x05!Ȳ $\x99L)\x87x\x9e%\x14y\xc6`\xe49\u0091\xcb\x02\x92\x19\x90\xbd\xc39)\xc7n\x92\x8a=\x92\xf7;S\x8a5\xe6\xb7$\xa7\x8f\xd3$\x1c\xa3Iج\x9c\xc34\xe1\xb8̲c2\t4|\xa6P噂\x95\xe7\bW\x9e7`\x99\rYf%g\xe6\xe7e\xc7[.N\xdeK\x95\x83\x9a\xdc\xebH\x15\xcdI\xa1\xec\xc5\x17\xdd1{\x99\xffp\xa7\x9cm\xd5qec\t\xeb\xfa\xd4{F~f\xc2\xef\xa3Z!l\xd9\xfd\xce\x06L\xe3\x88\xc4\xf3\xff\x8d\x97\xe7o\x1bu\xbb6\x1aJ\xaap\x87uwv\xa5\x15zC\xde\xd3\xec\u0603~\x8c\xc6\x15{\xa9\njȪ\xde\xf2z\xe5\x80ۿW\x1bB>\xc8zӾ}\x93\x8cfE\xc9\xcf6n\x88\xc0\\\xb5A\\&\x10Q\xe1\v\xe3\xdfIβ\x88\xa7\x15\xbd\\\xc85\x1e\\\t\x81W\x1ee\xed\xad\xef\xd26\x8c;Z\xe8\x94u\xafW\xdcK\xce\xe5\xd3\xc2p\x9c\x96\xec?\xf0\x96\xe6\xf9\x1c\xceۻ-6\r\x92\x82\xb7;\xd7\x15B5\xd2;\xb0\x16\xb3\x99\xce؊\xdf\xee;\x10#\x95v\xf5\x9f(\xad\xb5\xc5fc\xb7.\xb9\xaa?\xabi\xee\xb6\x0e\xbb\r\n\v\x15g\"\xb1\xd6\xc3\x1c\x99\xca\xd7%U\xe6\xec\xea\nnj\x1c\xc6\xf38\xc1nNe[F\xcd\xcb\xf0\xba\xdf(mí\xbf\xb8\x99w.\xbb[\xa1}\x8a^\x82\xc7\xf8Q\xbe\xd9C|W\xc4c\xdc\x05Y#\xa5\"\xaf\xa3EIW\xcbbi\x7f\xb5\xed\xaf\xf2\x04\xef\xa2٬\x0ey\xee{\xcd#\xe5D\x01\xa2\xbb\xdcu\xeavP\xbc\x9c\xf32]\x14\xaf\x0f\nC\xfb\xeb;\x13\xe7\xe2[G\xa6\x12n.\rpu\x01\xfe\x95wݭ\x1b\xc39\xf0\x0f\x8c\x83vh%(ѻa\xafZ\xa7V\xc5\xce9j{\xfbc=\xc0\x88\xf5q\xd3\u009co\t\xca:F.;\\\xe9 \x96\xe3\x13o8\u0084\x81C$\xdf<\xa1EO\x9d\x8b\xbf\x83Hϩ\x94/\xf1^\xad|`kQ9W1\xaaQ\xc6ാ}\x80\x99r<^~\xdd[2ǜ\xff\xb1\xdb\xe1\xf1F\xf4\xf9\xfb\xe1\xdd\xc5\xe9\xfek\x10^\x90+\x85W\\\xfaK\xd5\xf1Jȋ\xae\x88\xdf\xd5uP\xc6/\xa2\x83\xeb8B\x047\xb7Q\xbb\x97\xc4f_\x84\v\"\x0f\x8bw`\xba탧D\x96\xd1\xc1\xb3\xc0\xd7\xc6iC\x8b\xb9\x9b\xeeo\x87=\xf03$*oU\xd3\xd5\u05f5?Qݰ9\xa6f\x1bp\xae'\x86\f\x16\x1a\xe4\x04N \x88\xb5S\x8e\xbe\xe1S9\xfd>\x11\xa8m(\xfe\x18\x85\xd3\xf5A\xf3\x87\xc8\xdf\x7f^\xe5\x01\xdd-u\x02\xf5RO\xc0\xac/\xe0\x8f\x10a(\x99.\x14\x7fc}YXG\x81&\xb9jQ]\x9bi\xd6\xd5\xf3\xc9J\xeb\xf6~;\xd6sT\x82C\x83\x18\xff\x06\x1f\xba\xf8F%5\x9cY\xaa\x8a\x1a\xcelNAu\xd4Qdr\x8d\x82\xba\xfa4q\xad\xceސ\x8d\x8d\x9c\a\x80\xc7\xc1\xc2\xe7\x0f\xdcy\xb0\x02\xb4\xa6\x87p5\xf6\x93u\x98\x0f \x00\x93G\x91\xd9\xf8t|s~\xa8{1\xb4\xdb7\xa4\x99\xa9\xa8\x1f Tg\xb6Z\xbd\x8c)`.\x0f\xee\xc3\x15,|\xb0(D\x12\vi\xf2\xb5d*%\xf2x_7\xb4\xb4A\xa7\x0e\x19\xd1|`\n8;0\xeb\xb6[&\x1d\xa8\xda\xd1\x03\xac3\xc99\xa0\xae\x1d\xe2\xf5\x9c\x8b՟\xd2\xfa\fT\xcfN\xedC\xbb\xad\xdf_r\xdcv\xf7*RW\xa8\x8c\xdf\x1b2LA\xf3\x01\xaf\x01B\x12\a^\x14i8*D?u5Ĵ\xdd6,0\xafW}\x16\xd2\x7f\xf9\xea\xc6Ǯ\xf1\xe4JA\x7f\x97\xea\x86\x14L\xd8\x7f\xa8\xc8\xdd\x06P\xe8\xbc\b\x7f\xbc\xf1|\x06\xef;ۦ>,\xdb\xf2#!,\x88\xb1\xc8:~@rM>\xc20\x10tg\x1e!\xc7-\xcf\xd8\xf7\xbdl\x93\xad\xb8S\xf2\xa0@\x0fW՚\xfc\x8d2\xc3\xc4\xe1\x83Tw\xbc:0\xd1\xf8\x1b\x8b\x1a\xdfQe\x18\xe5\xfc\xec\xf0\x89!\xca\x04\xe5\xec\x1f1\xee\xb4\x7f\x9c\aT\xab\xdb\xc8o\th\x8c\xfd\xf0\x0e\xac\xa9\x1d\r7\xe2\x82\xe0\xe9:'\v\xbeY\xb3EÄ\x93]<_\xb9\x93\x95\xe9(\xbfFy\xc6\xe2]\x0flC>J\x03a\xe7\x9fuaZs\x01ڬa\xbf\x97ʸ\x1d\xa1\xf5\x9a\xb0\xbd\x0f_b)}\xca8F\xb1\xee\x03a\x84\x99\xa6ԳYo\x98IR\xa86\xf0Fゞ]\xbe\x97f\x99\r\xf8\xe1\x956\x94G4\xf27\xc5\xf8\x18'\xda\xf5\x02\xf9o)i\xf0m\xbb\xfd0\xaeGp\x8erx\x16\xdaY\xa3\xa8m&\x98\a\x04A\x9e\x143\xc6Z\x80vi\x171V\xe7sN\xb4Ղ\x17\x05\xf8\xc4y\v\xdb\xf1\xad\xe6n\x02\xa9n<\xe6l\xf8\xc9\xe1\xf7\xb0vH\x82\xd1ą\xdfR\xf7}-+\xb3#\x15\a+TJV\x87c\x90\xcb\x11[>\x027\xaf\x00\x93!\xa8!t(\xab1\x95\x12\xad\xad\xb7\xfa|_\x83.\xcd\x1eG1\xf5\xa5\x03\xe1#\x95\xaf\xfcE\xf7뽒\xc5\xda\xf3\x02\xaban\xfcv\x98b\xd2\x06\xec\xe6\x18%9q\xdf\xc0\xf27J\xa3\x18\x94%\bB\xb5\xc7'\xe1\"\x90\x8b\xf36\xdaPeR\xe3\xa0\xfbN\xe3\x99\x10\b!\xc7\xf1\xbd\xf7\xdb}\xeeB\x94[\xff\t\xb8\x1a\xf0\r\xd1L\x84\xefc\xba\xcdD'\n\xdaFF\n0\x19\x1a-t\x1a\xc44\x9d\b\xa6\x8b\xfe\x1f\x1b\xbc\x9cj\x9b\xf8>\xc5\v\xfe\xd2k\xde;\xf4\x86\x1fC\xab\x9bx\xcf5B\x8f?\xb1\xbd\xab\xbd\xca,\xd6\x7f\xfe??\xccvJ\xf2\xb2^N:X\xe8;՞\xd2̧\xcf\xee8X\xcfG\x03t}\xb7\x97\x8b\x9c\xf4\xd3ea\xe75c\xce\xf0\xe9\xd6\xebDb\xa7ˢ\xcdg\v5\xaf;\xbb'\x8a\x9f\x8b\x9c[c\x7f\xf3\xcd\"\xb1\xa6\x87\x10\x896#Ө\xe3\xcf\xd9h\xb3\x15l\x06\x1cG\xbe\xee\xd4\v@\xaf\x14nF\xed\xc0\xe0%*м\xb5\xb6\xfdH\xfe\xcd\xff\x06\x00\x00\xff\xffw\xe0\xabZYz\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\xe3\xb8\x11\xbe\xebWt\xed\x1e|YQ3\x9bKJ\x97\x94FN\xaa\xa6\xe2\x89]#ǹ\xe4\xb0\x10Д\xb0\x06\x01\x06\x0fi\x94T\xfe{\xaa\x01\xf0!\x92\xb2\xe4\xa9$\x8b\x8bM\xb2\xd1\xe8\xfe\xfa\r\xcd\xe7\xf3\x19\xab\xe5\vZ'\x8d^\x02\xab%~\xf3\xa8\xe9\xc9\x15\xaf\xbfw\x854\x8b\xc3\xc7٫\xd4b\t\xeb༩\xbe\xa23\xc1r\xbc\xc7Rj\xe9\xa5ѳ\n=\x13̳\xe5\f\x80im<\xa3\u05ce\x1e\x01\xb8\xd1\xde\x1a\xa5\xd0\xcew\xa8\x8bװ\xc5m\x90J\xa0\x8d̛\xa3\x0f\x1f\x8a\x8f?\x17\x1ff\x00\x9aU\xb8\x84-㯡v\xdeX\xb6CexbY\x1cP\xa15\x8543W#\xa7\x13vքz\t݇\xc4!\x9f\x9e$\xff\x14\x99m\x12\xb3\x87\xcc,~W\xd2\xf9?_\xa6y\x90\xceG\xbaZ\x05\xcb\xd4%\xb1\"\x89\xdb\x1b\xeb\xff\xd2\x1d=\x87\xadS\xe9\x8bԻ\xa0\x98\xbd\xb0}\x06ษq\tqw\xcd8\x8a\x19@\x86&r\x9b\x03\x13\"\x82\xcdԓ\x95ڣ]\x1b\x15*ݞ%\xd0q+k\x1f\xc1L\xba@V\x06\x1am\xc0y\xe6\x83\x03\x17\xf8\x1e\x98\x83ՁIŶ\n\x17\x7fլ\xf9?\xf2\x03\xf8\xd5\x19\xfd\xc4\xfc~\tE\xdaU\xd4{暯\xc9FO\xbd7\xfeD\n8o\xa5\xdeM\x89\xf4\xc0\x9c\x7faJ\x8a(ɳ\xac\x10\xa4\x03\xbfGP\xccy\xf0\xf4\x82\x9e\x12B@\x10!4\b\xc1\x91\xb9|\x0e\xc0!q\x89\x18MK\xaaFg\x9d\x89M\xa2\xc0ˀK\x92\x9f\xded\xe9{l\x1b\xff.\xb8Ŗ\xa5\xf3\xac\xaa\xcf\xf8\xaevx\x89\xd9\x19\x14\xf7X\xb2\xa0|_U\xb2\x92\xea\xfb\xe5\xb9Z5\xf2B\xa4]g'ޟ\xbdK\xa7n\x8dQ\xc8\x12\x97Du\xf8\x98\xbc\x90\xef\xb1b\xcbLljԫ\xa7\xcf/\xbfۜ\xbd\x86)G\x1a\x04\x05\x19\x8e\xf5l\xb3G\x8b\xf0\x12\xe3/\xd9\xcde\xd5Z\x9e\x00f\xfb+r\xdf\x19\xb1\xb6\xa6F\xebe\x13,i\xf5rQ\xef\xed@\xa6;\x12;Q\x81\xa0$\x84ɏr\xbc\xa0Ț\x82)\xc1\xef\xa5\x03\x8b\xb5E\x87\xda\xf7\xe1m\x05+\x81\xe9,^\x01\x1b\xb4Ćb9(A\xb9\xeb\x80փEnvZ\xfe\xb3\xe5\xed\xc0\x9b\xec\xbc\x1e\x9d\x1f\xf0\x8c\xf1\xa9\x99\"W\r\xf8\x130-\xa0b'\xb0H\xa7@\xd0=~\x91\xc4\x15\xf0\x85\xfc]\xea\xd2,a\xef}햋\xc5N\xfa&\asSUAK\x7fZ\xc4t*\xb7\xc1\x1b\xeb\x16\x02\x0f\xa8\x16N\xee\xe6\xcc\xf2\xbd\xf4\xc8}\xb0\xb8`\xb5\x9cG\xd1uJ\x9a\x95\xf8\xd1\xe6\xac\xed\xee\xced\x1dEmZ1k\xbea\x01ʘ\xc9\v\xd2֤E\a4\xbd\"t\xbe\xfeq\xf3\f\xcd\xd1\xd1\x18C\xf4#\xee\xddFי\x80\x00\x93\xbaD\x9b\x8cXZSE\x9e\xa8Em\xa4\xf6\xf1\x81+\x89z\b\xbf\v\xdbJz\xb2\xfb?\x02:O\xb6*`\x1d\v\x13l\x11B\x1d㾀\xcf\x1a֬B\xb5f\x0e\xff\xe7\x06 \xa4ݜ\x80\xbd\xcd\x04\xfd\x9a:$N\xa8\xf5>4\xb5\xf0\x82\xbd&\xa3xS#?\x8b\x1f\x81NZ\xf2p\xcf<Ƹ\x18\xe0\x9aC\xfcr1m\xd6tp\xd3b\x9c\xa3s_\x8c\xc0ᗁȫ\x96\xf0L\xc6\x1am%],\x8bP\x1a;\xac\x18\xac\xcd\xc0\xfd\xd5d\xaab\xf4\ru\xa8Ƃ\xcc\xe1+2\xf1\xa8\xd5\xe9§\xbfY\xe9\xc7\a]0$\xad$\xe2\xe6\xa4\xf9\x13Zi\xc4\x15\xe5?\r\xc8[\b\xf6\xe6\betk\xedՉr\x90;i>ζ\xcdZ=}n2o\n\xa0\x1co\x19\xab\x02V9rM\t\x1f@HG\r\x80\x8bL\xc7`\xe9\xa0b\x83\xb0\x04oû\xd4\xe7F\x97r7V\xba\xdf\xd3\\\xf2\x98+\xac\aȭ\xe3I\x94\x9a\xc8;jk\x0eR\xa0\x9dS|\xc8R\xf2,I\xb0\xa9r\x95\x12\x95pcM/DYTŢ\xa0\xa8f\xea\x8a\r\xd7-a쀙\xd4Ƀ;\x061\xd9\xd8*\x97T\xedQ\x8b\xb6\x1b9\x93\xc6Ĭ\xe5P\xc0Q\xfa}J\x87j*\xee\xe0\xcdأ\xf5\x8a\xa7\xa9\xd7\x03ٟ\xf7H\x94\xa9\x80\"8\xe4\x16}\xf46T\xe4>\xe4J\x05\xc0\x97\xe0bB\x1d\xe6\x89f\xc5F\xad\xd9\xfd\x8a\xa71\xd0p\u0378\xb9\x85\xb9.\xf2\x1d\xb5\u038d\xc0\x16K\xb4\xa8\xfddR\xa7\x01\xc4j\xf4\x18\xf3\xba0\xdcQJ\xe7X{\xb70\a\xb4\a\x89\xc7\xc5\xd1\xd8W\xa9ws\x02|\x9e#h\x11NJŏ\xf1\xcf\x05\x95\x9f\x1f\xef\x1f\x97\xb0\x12\x02\x8cߣ%\xab\x95A5\x8e\xd6\xebo~\x8a5\xf6'\bR\xfc\xe1\xee{p1u\x8a\x9c\x1b\xb0\xd9D\xef?Q\xa3\x16\x85\"\x886\xc9*\xc6\x02UJ2v\x95\xad\x99r͔#Nu\x98\xfdE\x89\x89*\xc8TF}\xc5q2}#\xcc\x00\xbe\xcd;C\xcd+V\xcf\x135\xf3\xa6\x92|6\xd46\xb6\xc1W\"\xb2i\xbb\xa5\x16\x92S\xdbv\x1eI\xcd8\"κ\xf3\t\x18\x86\xfd\xfa\xa5\xfc1\rSR7W\xcf+\x12?\xf6i\xbb!.%\xb3\\\x11\x1dzj\xb7\x1ch\xa4\x8a\xc9\xec\x18\xe7\x98B\xb8њb\xd7\x1b`mb\xbcsÊ\xf0\xce|\xb2\r\xfc\x15'\x80\x1f\xa9\xf2)\x126\x18\xa7m$Kp\x18S\xf551\xe0zDp\xb6F{\x8b,\xeb\x15\x11\xb6E\x95\xc1z\x05۠\x85\xc2F\xa2\xe3\x1e5\xcd\x13\xb2\x14?\xfcf3\x93b\xce\xd3\b\x84\xe2+\x1e\xe4\xf8Nh\x8c\xee\xc3hG\x13\xf8m8\xd0\xc3/\xcdh\xbd\xb0\x99\xec\x97\t0J\xa9\xa8s\x9c\xc8\x13]\xc70\xbe\xbd\xfc\xb4y\xb8s\xb1\xe1G\xed\xa7\x9a\xc4#Z\x8c\xf3\x15\n\xea\xf9M\xbe\xc5\bΣ\x9dp\x80\xd6z\xd1栌\xde\r\x02'\xad|\xa7A\xfd\\r(cA\xa0\xa7Ҥw\xc0\xf7Lﰻ\xb3\xca\xf2\xbf-)\xb9\xcf\xc0g:\x0f\x91\xfa\x92{\xdcd\xd1g9\xd5ԏ\xee\x8b;\xe2\xe9\xbb\xe2F\xfaƲ\x17\x87\xa2+\xb8\x8f\xe8\x9b*M\xa0\xce}w\x7fܭ\xef\x1f\x86Ǘ\xd37 \xf1ޛ\xf37nA\xe0\xc8\\w\x87\xfe\xdb\xe1PQ\xb7z\xb5\x05\xfe\x92\xa8\xd2ec\xde\x02lk\x82\x7f+2\xef\xa6\x1c:\xff8\xf0\x1e\x19\xe3O\x1eך\f\xa2i,\u0083\xa5\xc1\xb3\xbbC\x8bIa\xaa\xb6\xdc~\x19\xb5\x1a\xfc2\xd3\xff6\xfe\xdd\xe6\x06\xbd&k\xed\xe8e\xaa\x97=\xbbf\x90\xfbo¶\xbdW^¿\xfe=\xfbO\x00\x00\x00\xff\xff\x80.\x12\xd3P\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96M\x93\xdb6\x0f\xc7\xef\xfe\x14\x98y\x0e\xb9<\x92\xb3\xed\xa5\xa3[\xb3\xc9a\xa7mƳ\x9bɝ&a\x8bY\x8ad\x01\xd0[\xb7\xd3\xef\xde!)\xf9E\xb67\xdbCy\x13\t\x02\x7f\xfe@\x80j\x9af\xa1\xa2\xfd\x8a\xc46\xf8\x0eT\xb4\xf8\x87\xa0\xcf_\xdc>\xffĭ\r\xcb\xdd\xdd\xe2\xd9z\xd3\xc1}b\t\xc3#rH\xa4\xf1#n\xac\xb7b\x83_\f(\xca(Q\xdd\x02@y\x1fD\xe5iΟ\x00:x\xa1\xe0\x1cR\xb3E\xdf>\xa75\xae\x93u\x06\xa98\x9fB\xef\u07b7w?\xb4\xef\x17\x00^\r\u0601A\x87\x82k\xa5\x9fS$\xfc=!\v\xb7;tH\xa1\xb5a\xc1\x11u\xf6\xbf\xa5\x90b\aDž\xba\x7f\x8c]u\x7f,\xae>\x14W\x8f\xd5UYu\x96\xe5\x97[\x16\xbf\xda\xd1*\xbaD\xca]\x17T\f\xd8\xfamr\x8a\xae\x9a,\x00X\x87\x88\x1d|β\xa2\xd2h\x16\x00㱋\xcc\x06\x941\x05\xa4r+\xb2^\x90\xee\x83K\xc3\x04\xb0\x01\x83\xac\xc9F)\xa0\xbe\xf4X\x8e\ba\x03\xd2#\xd4p \x01\xd68*0e\x1f\xc07\x0e~\xa5\xa4\xef\xa0ͼ\xdaj\x9a\x85\x8c\x06\x15\xf5\x87\xf9\xb4\xec\xb3`\x16\xb2~{K\x02\x8b\x92ē\x88\x12\xd7\x06\x0ft\xc2\xf7\\@\xb1oc\xaf\xf8<\xfaSY\xb8\x15\xb9\xda\xec\xee*i\xdd㠺\xd16D\xf4?\xaf\x1e\xbe\xfe\xf8t6\r\xe7Z\xaf\xa4\x16,\x83\x9a\x94fp\x95\x1a\x04\x8f\x10\b\x86@\x13Un\x0fN#\x85\x88$v\xbaZu\x9c\x14\xcf\xc9\xecL»\xac\xb2Z\x81\xc9U\x83\\\xa0\x8d\x97\x00\xcdx\xb0\n\xd32\x10FBF_\xeb\xe8\xcc1d#\xe5!\xac\xbf\xa1\x96\x16\x9e\x90\xb2\x1b\xe0>$gr\xb1\xed\x90\x04\bu\xd8z\xfb\xe7\xc17\xe7s\xe6\xa0N\xc91?\xd3(\x97\xce+\a;\xe5\x12\xfe\x1f\x9470\xa8=\x10\xe6(\x90\xfc\x89\xbfb\xc2-\xfc\x961Y\xbf\t\x1d\xf4\"\x91\xbb\xe5rkej\x1a:\fC\xf2V\xf6\xcbR\xffv\x9d$\x10/\r\xee\xd0-\xd9n\x1bE\xba\xb7\x82Z\x12\xe1RE\xdb\x14\xe9\xbe4\x8ev0\xff\xa3\xb1\xcd\xf0\xbb3\xad\x17\x17\xa4\x8eR\xe8\xafd \x97yM{\xddZOq\x04\x9d\xa72\x9d\xc7OO_`\n]\x921\xa7_\xb8\x1f7\xf21\x05\x19\x98\xf5\x1b\xa4\x9a\xc4\r\x85\xa1\xf8Dob\xb0^ʇv\x16\xfd\x1c?\xa7\xf5`\x85\xa7+\x99s\xd5\xc2}餹\xa8S4Jд\xf0\xe0\xe1^\r\xe8\xee\x15\xe3\x7f\x9e\x80L\x9a\x9b\f\xf6m)8}\x04\xe6ƕ\xda\xc9\xc2Ծo\xe4\xebJ\xd1>E\xd49\x83\x19b\xdem7V\x97\xf2\x80M x\xe9\xad\ue9e2\x9d\xd1=\x14x{\xb6p\xbd\xa0\xf38\xb6\xc9\xf9\xca\xcd\xc3Cɝ%\x9c\xdd\xc2\x06.z\xee\xeb\\J3\xfc\x97dj'\x1e\xd9\xe8D\x84^N\xfa\xb3\xba\xb6\xe9\xad,\x90(\xd0\xc5\xecLԧbT^ze=\x83\xf2\xfbq#H\xaf\x04^\x90r\x19\xe8\x90r\x9fA\x03&]\xf0\x1b\xb1\x9c\xbe%\x91\x82F\xe6\xf6\xc2\xce\n\x0eW4\xbd\x92\x9d<|rN\xad\x1dv \x94\xf0Ff\x15\x91\xda\xcf\xd6ʛ\xf5\x1d\x04\xabls-\a\x87w\xfa\xbbI(\xb8}\x1a.#5\xf0\x19_\xae\xcc>\xf8\x15\x85-!ϯ|^\\Uz\x87\x9f\x817P\xbaz)/&9\xf7;sB\x91%\x90ڞr\xe5\xb4>\xf4\xef\x0e\xfe\xfa{\xf1O\x00\x00\x00\xff\xff\x045\f\xc6i\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4VM\x93\xdb6\f\xbd\xfbW`\xa6\x87\xb43\x91\x9c\xb4\x97\x8eo\xad\x93\xc3N\xd24\xb3N\xf7NS\xb0\xc4.E\xb2\x04\xe8\xcd\xf6\xd7w@J\xfe\x94\xbd\xdeCu\x13\t\x82\x8f\x0f\x0f\x8f\xac\xaaj\xa6\x82y\xc0Hƻ\x05\xa8`\xf0;\xa3\x93?\xaa\x1f\x7f\xa5\xda\xf8\xf9\xf6\xfd\xecѸf\x01\xcbD\xec\xfb{$\x9f\xa2\xc6\x0f\xb81ΰ\xf1n\xd6#\xabF\xb1Z\xcc\x00\x94s\x9e\x95\f\x93\xfc\x02h\xef8zk1V-\xba\xfa1\xadq\x9d\x8cm0\xe6\xe4\xe3\xd6\xdbw\xf5\xfb\x9f\xebw3\x00\xa7z\\@㟜\xf5\xaa\x89\xf8OBb\xaa\xb7h1\xfa\xda\xf8\x19\x05Ԓ\xbb\x8d>\x85\x05\xec'\xca\xdaa߂\xf9Ð澤\xc93\xd6\x10\x7f\x9a\x9a\xfdl\x86\x88`ST\xf6\x1cD\x9e$\xe3\xdadU<\x9b\x9e\x01\x90\xf6\x01\x17\xf0E`\x04\xa5\xb1\x99\x01\fG̰\xaa\xe1t\xdb\xf7%\x95\xee\xb0W\x05/\x80\x0f\xe8~\xfbz\xf7\xf0\xcb\xeah\x18\xa0A\xd2\xd1\x04\xceD\x9d`\x06C\xa0`@\x00\xecw\xa0@9P\x91\xcdFi\x86M\xf4=\xac\x95~La\x97\x15\xc0\xaf\xffF\xcd@\xec\xa3j\xf1-P\xd2\x1d(\xc9WB\xc1\xfa\x166\xc6b\xbd[\x14\xa2\x0f\x18ٌ,\x97\xef@C\a\xa3'\xc0\xdf\xc8\xd9J\x144\"\x1e$\xe0\x0eG~\xb0\x19\xe8\x00\xbf\x01\xee\fA\xc4\x10\x91\xd0\x159\x1d%\x06\tRn8A\r+\x8c\x92\x06\xa8\xf3\xc96\xa2\xb9-F\x86\x88ڷ\xce\xfc\xbb\xcbM\u0090lj\x15\x8fr\xd8\x7f\xc61F\xa7,l\x95M\xf8\x16\x94k\xa0W\xcf\x101\xf3\x94\xdcA\xbe\x1cB5\xfc\xe1#\x82q\x1b\xbf\x80\x8e9\xd0b>o\r\x8f\xbd\xa3}\xdf'g\xf8y\x9e\xdb\xc0\xac\x13\xfbH\xf3\x06\xb7h\xe7d\xdaJE\xdd\x19F\xcd)\xe2\\\x05Se\xe8.\xf7O\xdd7?ġ\xdb\xe8\xcd\x11V~\x16\x99\x11G\xe3ڃ\x89\xac\xf9+\x15\x10\xd5\x17\xc1\x94\xa5\xe5\x14{\xa2eHع\xff\xb8\xfa\x06\xe3ֹ\x18\xa7\xec\x17\xe5\xec\x16Ҿ\x04B\x98q\x1b\x8c\xa5\x88Yy\x92\x13]\x13\xbcq\x9c\x7f\xb45\xe8N駴\xee\r\xd3(f\xa9U\r\xcbl(\xb0FH\xa1Q\x8cM\rw\x0e\x96\xaaG\xbbT\x84\xff{\x01\x84i\xaa\x84\xd8\xdbJp腧\xc1\x85\xb5\x83\x89\xd1\xc9.\xd4\xeb\xa4\xd5W\x01\xb5TO\b\x94\x95fctn\r\xd8\xf8\bj\xdf\xf9\x03\x81\xf5Q\xe6\xe9\xce\xcd\xe0Tl\x91OGO\xb0|\xcbA\xb2\xfdS\xa7\x8e\x8d\xe6G\xac\xdbZ\xbc\x82\x06 \xc5=~\xaa\xcf2^\xc6\x00\x93\xea\x9dD2\x8aXh\x10^\xc5\nĤ\x0e1\x9do-\x1f\xba\xd4OoP\xc1\xef\x19\xf3g\xdf^\x9d_z\xc7\"\xf7\xabA\x0fަ\x1eWN\x05\xea\xfc\v\xb1w\x8c\xfd\x9f\x01c\xb91\xaf\x86\x8e\x17\xef\ue5ba\x12\x98\xec\xc5}\xefQ\xfc\x1e/\x9ft\b\xb8)\xcb\r\x98\x86ț\x0e\xba\\ݽ\x86\xc2\v\xe1\xaf(ҝ\xdb\xf8\xe9\xb8\v\xed=~\xf9\x1a\x7fY\xab\xf2\x10\x18\xb5*K\xca݆\xf0)\xad1:d\xa4\xbd\xcd>\x19\xee&3\x02}S\xe6\x84H\xf9\x01\xa4\xd5\xe9\xd3K\xbe5B\x83\x16\x19\x1bX?\x97\x1b\xe9\x99\x18\xfbs\xdc\x1b\x1f{\xc5\v\x90˻b3!#\x97\xacUk\x8b\v\xe0\x98.\xa9l\xf2\xe0\xa1S4цGg\xfe*1S\xc2\xd85\xe3Ue\xc0\xc5{\xa3\x82/\xf841\xfa5z\x8dDx\xdeF\x17O2\xd9\x04g\x83$/\xac急\xe1\xe1>\x8c\xfc\x17\x00\x00\xff\xff\t\x15i;\xcd\r\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\x1c\xb7\x11\xbe\xf3Wt\xc9\a\xc6U\x9aY[I\xa5R{\x93\xa88\xc5ĦX\xa2\xa4\x8b\xcb\a\xec\xa0g\x06\xe6\f\x80\x00\x98%7.\xff\xf7T\xe3\xb1;\x0f\xec.Ɋ\x1c\\\xc8ţ\xf1\xa1\xdf\xddS\x14\xc5\x05\xd3\xe2\v\x1a+\x94\\\x03\xd3\x02\x1f\x1dJ\xfae\xcb\xfb\xbf\xd9R\xa8\xd5\xf6\xfb\x8b{!\xf9\x1a\xae\x06\xebT\xff\x11\xad\x1aL\x85\xef\xb1\x16R8\xa1\xe4E\x8f\x8eq\xe6\xd8\xfa\x02\x80I\xa9\x1c\xa3iK?\x01*%\x9dQ]\x87\xa6hP\x96\xf7\xc3\x067\x83\xe88\x1aO<]\xbd\xfd\xae\xfc\xfeM\xf9\xdd\x05\x80d=\xaeA+\xbeU\xdd\xd0\xe3\x86U\xf7\x83\xb6\xe5\x16;4\xaa\x14\xea\xc2j\xac\x88vcԠ\xd7pX\bg\xe3\xbd\x01\xf3\xad\xe2_<\x99w\x9e\x8c_\xe9\x84u\xffʭ\xfe(\xac\xf3;t7\x18\xd6-A\xf8E+d3t\xcc,\x96/\x00l\xa54\xae\xe1\x86`hV!\xbf\x00\x88O\xf4\xb0\n`\x9c{\xa6\xb1\xee\xd6\b\xe9\xd0\\\x11\x85Ĭ\x028\xda\xca\b\xed\x89\x1e\xe1\xa1E\t\xae\x15\x16\xc2k\xe1\x81Y\x82c\x9c\x7fe\xfeb\xbfNǭc\xbd\x9e \xb82\xc8\x0eG\x03\x04\xce\x1c\xe6\x00\xec\xf9\t\xaa\x06\xd7\"q\xde+\x16\x13R\xc8\xc6O\x05I\x80S\xb0A\x0f\x119\f:\x83LcUj\xc5K\x99\x88N`\xdd\xccf\xcf\xf1\x86\xf6\xff\xafQM\x00\xdd*\xfe\x02(Ϻ7l\x9e\xdc\xfae0\x96=\xff\xc6\xc40j/'X\x17\x8a\x11\x86\x0ff'$@\xe1\f\x84\x05\x16\x8f\x86W\x1c\x18\x9d\xdc\xd1ǿ\xdf}\x82t\xb5\x17Ɯ\xfb\x9e\uf1c3\xf6 \x02b\x98\x905\x995\t\xb16\xaa\xf74Qr\xad\x84t\xfeG\xd5\t\x94s\xf6\xdba\xd3\vGr\xff\xf7\x80֑\xacJ\xb8\xf2\x99\x02\xb9\xa7A\x93\xe6\xf2\x12\xae%\\\xb1\x1e\xbb+f\xf1\xab\v\x808m\vb\xec\xd3D0Nr\xe6\x9b\x03\xd7F\v)E9\"\xafY\xdeq\xa7\xb1\"\xe9\x11\x03館E\xf4P\xb52\xc0\xe6\xdb\xcb\t\xe1\xbc\xe1\xd2\xc8z\xa7\xf9\xa6\x19\xb2w\xb93\t\x9b\x1c\xf9\xd4\xe40\xc3\xce\x05Q\x80n\xeee\xf7g\fje\x85SfG\x84\x83\x83-\x17\x14\x8e\x88\x81\x86T\x1cϼ\xe3Fq\xcc\xc1\xa6\xa3\xe0Z\x16\xb4\x95\xf2+\xf2G\x83\x94\xcb[h(\xf9,`Z\xf13\xb8\xe2\x8d\f\f\xd6hPV\x98\x1cש\xe4!\x83l\x1c֗\x18\x8f+\x05\x9c\xf0\xeaY\xc4oo\xaf\x93'OL\x8c\xd8\xdd\xf2\xde3\xfc\xa1Q\v\xec\xb8\x0ft\xe7ᄐ\xae\xc3eާ9\x05\f\xb4\xc0\x90\x06\xee\x83\x04\bi\x1d2\x0e\xaa\xceR\xa4\x9a\x04\xc8\xf0\r\xc6\x13\xaf\x83\a\x8b\xae\xf2\x10Z\x88\xf7\xc0\xc8w\n\x0e\xff\xbc\xfbp\xb3\xfaG\x8e\xf5\xfbW\x00\xab*\xb4>\vvأt\xaf\xf7\x899G+\frJ\xb3\xb1\xec\x99\x145ZW\xc6;\xd0؟\xdf\xfc\x92\xe7\x1e\xc0\x0f\xca\x00>\xb2^w\xf8\x1aD\xe0\xf8\xde-'\xa5\x116\xb0cO\x11\x1e\x84k\xc5<\x98\xee9@\xea\x15\x9f\xfd\xe0\x9f\xeb\xd8=\x82\x8a\xcf\x1d\x10:q\x8fkx\xe5Ӛ\x03\xcc\xdf\xc8v~\x7fu\x84Ꟃi\xbf\xa2M\xaf\x02\xb8}\x1c\x1e\x1b\xdd\x01d\xb0<#\x9a\x06\x0fY\xd5|\xf8\xa0B\xae\xfa[P\x868 Ո\x84'L\xd2\v\x8e\x12\xf9\x02\xf4\xcfo~9\x8ax\xca/\x10\x92\xe3#\xbc\x01\x11K\x1b\xad\xf8\xb7%|\xf2ڱ\x93\x8e=\xd2MU\xab,\x1e㬒\xdd.\xe4\xb9[\x04\xab\xa8P®+B\x1e\xc4\xe1\x81\xed\x88\vIp\xa4o\f43\ue936\xa6\xec\xe7Ӈ\xf7\x1f\xd6\x01\x19)T\xe3=1E\xcdZP6CiL\x88\xc5^\x1b\x17\xc1<\r;\x04\xf5q\n\xaa\x96\xc9\x06\xc3{\x11ꁢcy\xf9\x12;^\xa6$idR\x93\xb9\xe3\xf8\xbf\x05\xf7'>\xceg\xd0Oxܸ\xca8\xf9\xb8\xfba\x83F\xa2C\xff>\xae*KO\xabP;\xbbR[4[\x81\x0f\xab\ae\xee\x85l\nR\xcd\"\xe8\x80]\xf92u\xf5\x8d\xff\xf3\xe2\xb7\xf8\x8a\xf6\xa9\x0f\x9aT\xda_\xf3Ut\x8f]\xbd\xe8Q)\x87}z\x1c\xbb\xbc\x8b\x99\xd5\xfc,\x99\xc5C+\xaa6\x15'\xd1\xc7\x1e1&A\x990\x0f\xae\x99\xc9\xddWWeb\xe8`\bѮ\x88\xbd\xb4\x82IN\xff[a\x1dͿ\x88\x83\x83x\x92\xf9~\xbe~\xff\xc7(\xf8 ^d\xabG\x12\xf00\x1e\x8b\x03\xac\xa2g\xba\b\xbb\x99S\xbd\xa8f\xbb)+\xbd\xe6\xc4\xf8Z\xa09\x93\xc6}\x9clN\x89f&\xbf\xdd\xefyV\x1e\xe9X\x93I\xdcƭ\xc3S\xe9\xddI~M\x1b7\xac\xb1\xc0\f\x02\x83\x9ei\x92\xf3=\ue290\x10h&(\x9aS\xc0\xdewE\x80i݉l\xe0\x8ea?\xa6\xac\x91\x13T\x96\xb3\xc6\x1e{{Vj\xe3.\xd0\x19)|\x1emM28Ӈrmή'ݩ%Z\x94C\xbf\x84R\xc0\xbd҂e\xe6\rZ\xb7\xd0/Zx\xb5\xccKN\b+\xf0\xf2\f\x0fb{8S\xeaDQ\x84\xbcp_\xee\xf8\x8e`\xae\x9e8^L\x1c\x85H\xf5a\x89=Z˚s\xa6\xf8S\xd8\x15\xea\xfbx\x04\xd8F\rn_\xe0O\xdc㥍:\xf5\xbc\x1eC\xb6t\x9e\xaa3\xa3\xd2\xc6\xc6\x14\xbf\xeb\xfc\x99\xb1#8|\x93\xf3\xa86\x98O\x11^\xe2\x13\x00\xfcǦs\biO\xce\xc0\xf6\xde뤅\xc1\t\xa7|\x83\x0f\x99\xd9\xc5G\xb2\xf1\xe2U2\x99\xcc\xda\x0f\xde\x1a\x9e\xf5\xfex\xd19\x16\xc4mЪ.\x19\xb3r\xac\x039\xf4\x1b4ć\xcdΡ\x9d\xba\xf3\\7\xc7W\x81\a6\x8e\xce'\xf9\x05J\xb1\xb0\xad\x98\xf4]W\xb2.\xa7\x80\v\xab;\xb6\xcb\x10N\x0f\xf1\x99\x1e\x19\x17\xb9\x80\x83>'\xa3\xd6h\xfc\xd2s\xbbP\x1e\xd3{%\x8f\xd4%ɞ\x85t\x7f\xfdˉ\xbcPH\x87\xcd,8\xc4ub\xe7;\xba\xe5\xeb\xdcp\"\x89\xb1\x92i\xdb*w\xfd\xfe\x8c\x16\xdc\xed7&k8\xa4\x8c\xde\xf7\xf9\x9ep\xdc\x14U!'\xaa\xbdoy\x96\xa9N?Ϟ\x83:\xd9|&\n\xc5\x0fù\x18t\x87\x9a\x19\xb2t\xff\x05\xe1j\xfe\x89\xeb5X\xe1ۢ\x94y\x86T44-,\x05'J\xad\x94\xc1\x8c˄eX\x99\x04\x91)\xfc?2~d\xf5d1\xe9\x91\xf3\x11\xed\xd8Z\x1f\xcf\f\x9b\xfdg\xa35\xfc\xf6\xfb\xc5\x7f\x03\x00\x00\xff\xffY\xc0\xfaX\xc0!\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\xdc6\xf2\xbf\xebSt9\a\xfdSerb\xff\xb7\xb6\xb6\xe6f\xcb\xeb-\xed&\xb2ʒ}I\xe5\x80!\x9a$\"\x12@\x00p\xa4\xd9T\xbe\xfbV\x03\x04\x87\x0f̌F\xb5\xce\xf2\"\r\x1e\xdd?4\xfa\x8d,\xcb.\x98\x16_\xd1X\xa1\xe4\x1a\x98\x16\xf8\xe4P\xd2/\x9b?\xfc\xcd\xe6B\xad\xb6o.\x1e\x84\xe4k\xb8\xea\xacS\xedg\xb4\xaa3\x05~\xc0RHᄒ\x17-:ƙc\xeb\v\x00&\xa5r\x8c\x86-\xfd\x04(\x94tF5\r\x9a\xacB\x99?t\x1b\xdct\xa2\xe1h<\xf1\xc8z\xfbC\xfe\xe6m\xfe\xc3\x05\x80d-\xaeA+\xbeUM\xd7\xe2\x86\x15\x0f\x9d\xb6\xf9\x16\x1b4*\x17\xea\xc2j,\x88veT\xa7װ\x9f\b{{\xbe\x01\xf3\xad\xe2_=\x99\xf7\x9e\x8c\x9fi\x84u\xffJ\xcd\xfe(\xac\xf3+t\xd3\x19\xd6,A\xf8I+d\xd55\xcc,\xa6/\x00l\xa14\xae\xe1\x86`hV \xbf\x00\xe8\x8f\xe8ae\xc08\xf7Bcͭ\x11ҡ\xb9\"\nQX\x19p\xb4\x85\x11\xday\xa1\xdc*\x0e\x01 \x04\x84`\x1ds\x9d\x05\xdb\x1550\v7\xf8\xb8\xba\x96\xb7FU\x06m\x80\a\xf0\xabU\xf2\x96\xb9z\ryX\x9e\xeb\x9aY\xecg\x83x\xef\xfcD?\xe4v\x04\xda:#d\x95\x82q/Z\x84\xc7\x1a%\xb8ZX\b\xa7\x85Gf\t\x8eq\xfe\x94i\xc6~\x9e\xb6[\xc7Z=Ape\x90\xed\xb7\x06\b\x9c9L\x01\x18\xe4\t\xaa\x04W#I\xde+\x16\x13R\xc8\xca\x0f\x85\x9b\x00\xa7`\x83\x1e\"r\xe8t\x02\x99\xc6\"\u05ca\xe72\x12\x9d\xc0\xba\x99\x8d\x9e\x92\r\xad\xffo\xa3\x9a\x00\xbaU\xfc\x05P\xce\xe2\x1b\x16O\xb8~\x1d\x0f\x9dԏ\x1a\xfd\x9aȼӍb\x1c\r\xb1\xaf\x99\xe4\r\xd2\xcd2p\x86I[\xa29\x00#n\xbb\xdf\xe9)\x98/\x91\xdeh\xe6\x1ca\xf4\xb6s\xe7\x94a\x15\u008f\xaa\xf0\x0e\x8aT\xda\xe0D\xa7m\xad\xba\x86\xc3&r\x01\xb0N\x99\xa4\x82\x13Ⱛ\xa7\x1b\xc9\xce\xecl\xca\xf30\xfa\x11\xed\xe8O\xf3\x82lD(\x99\xb6\xa0w\x15\xa6\xad'Lo\xdf\x04wU\xd4زu\xbfRi\x94\xefn\xaf\xbf\xfe\xff\xddd\x18@\x1b\xa5\xd18\x11\xddg\xf8F\xc1a4\nSQ_\x12\xc1\xb0\n8E\x05\xb4A\a\xc3\x18\xf2\x1eC\xb8\x0ea\xc1\xa06hQ\xba\xb1H\xe2\xa7J`\x12\xd4\xe6W,\\\x0ewh\x88L\xbc\x98B\xc9-\x1a\a\x06\vUI\xf1\uf076%]#\xa6\rs\xd8{\xf1\xfd\xe7\x1d\xadd\rlY\xd3\xe1k`\x92C\xcbv`\x90\xb8@'G\xf4\xfc\x12\x9b\xc3O\xca \bY\xaa5\xd4\xcei\xbb^\xad*\xe1bP,T\xdbvR\xb8\xdd\xca\xc77\xb1\xe9\x9c2v\xc5q\x8b\xcdʊ*c\xa6\xa8\x85\xc3\xc2u\x06WL\x8b\xccC\x97>0\xe6-\xff\xce\xf4a\xd4^N\xb0.\x14#|>\x98\x1d\xb9\x01\ng ,\xb0~k8\xc5^\xd0\xd1\x1d}\xfe\xfb\xdd=D\xd6\xfe2\xe6\xd2\xf7r\xdfo\xb4\xfb+ \x81\tY\x92Y\xd3%\x96F\xb5\x9e&J\xae\x95\x90\xce\xff(\x1a\x81r.~\xdbmZ\xe1\xe8\xde\x7f\xeb\xd0:\xba\xab\x1c\xae|\xa6@\xee\xa9Ӥ\xb9<\x87k\tW\xac\xc5\xe6\x8aY\xfc\xe6\x17@\x92\xb6\x19\t\xf6yW0Nr拃\xd4F\x131E9p_\xb3\xbc\xe3NcA\xb7G\x02\xa4\x9d\xa2\x14\xbd\x87*\x95\x016_\x9eO\b\xa7\r\x97\xbe\xa4w\x9a/\x9a!{\x9f\xda\x13\xb1ɑO\x8d\x0e3\xac\\\x10\x05h\xe6^v\xd8cP++\x9c2;\"\x1c\x1cl\xbe\xa0p\xe0\x1a蓊\xe3\x89s\xdc(\x8e)ش\x15\\͂\xb6R~E\xfe\xa8\x93rɅ>%\xcf\x02\xa6\x15?\x81\xab\xe7\xc8\xc0`\x89\x06e\x81\xd1q\x1dK\x1e\x12\xc8\xc6a}\x89\xf1\xb0R\xc0\x11\xaf\x9eD\xfc\xee\xf6:z\xf2(\xc4\x1e\xbb[\xf2=!\x1f\xfaJ\x81\r\xf7\x81\xee4\xef\xcb\xeb20\xf3>\xcd)`\xa0\x05\x864p\b\x12 \xa4u\xc88\xa82I\x91j\x12 \xc37\xd8\xefx\x1d\xad\x032R\xa8\xca{b\x8a\x9a\xa5\xa0l\x86Ҙ\x10\x8b\xbd6.\x82y\xfcl\x17\xd4\xc7)(j&+\f\xe7E(;\x8a\x8e\xf9\xe5K\xecx\x99\x92\xc4/\x91\x9a\xcc\x1d\xc7\xff,\xb8?\xf3p>\x83~\xc6\xe1\xc6U\xc6\xd1\xc3=t\x1b4\x12\x1d\xfa\xf3qUX:Z\x81\xdaٕڢ\xd9\n|\\=*\xf3 d\x95\x91jfA\a\xecʗ\xa9\xab\xef\xfc\x9f\x17\x9f\xc5W\xb4\xcf=Ф\xd2\xfe\x96\xa7\">v\xf5\xa2C\xc5\x1c\xf6\xf9q\xec\xf2\xaeϬ\xe6{\xc9,\x1ekQԱ8\xe9}\xec\x01c\x12\x94\t\xf3\xe0\x9a\x99\xdc}sU&\x81v\x86\x10\xed\xb2\xbe\x97\x961\xc9\xe9\x7f+\xac\xa3\xf1\x17I\xb0\x13\xcf2\xdf/\xd7\x1f\xfe\x1c\x05\xefċl\xf5@\x02\x1e\xbe\xa7l\x0f+k\x99\xce\xc2j\xe6T+\x8a\xd9j\xcaJ\xaf9\t\xbe\x14hN\xa4q\x9f'\x8bc\xa2\x99\xc8o\x875g呎U\x89\xc4m\xdc:<\x96\xde\x1d\x95״q\xc3*\v\xcc 0h\x99\xa6{~\xc0]\x16\x12\x02\xcd\x04Es\n\xd8CW\x04\x98֍H\x06\xee>\xec\xf7)k/\t*\xcbYe\x0f\x9d=yk\xb1\vt\xa5d)\xaa\x13\xf7\xf0e\xb2x\xc8\tlL;JQuf_J\x8d\x9bS\xe7&͚\x19\xd64\xd8|\x14\r\xda\xc0\xf7\x19\xf6s\xbb\xdc5\x94%]\xbbACB/ir`p\xc0\xef\x04\xdc>\xbf\xd2hJe\xdaВ\xedl,\x17\x0e\x9fl/r!\x1dVC\a\xee\xac\v\xb9\xa7%ϻ\x0eZ\x1a\x8fy\xa21\xe8ꔣ\x9d\xb4\v\x97\xe7AٵK(\x19<(-Xbܠu\v\x83\xa7\x89W\xcbD\xf1\x88\xf5\x04\xe5>!\x83\xbe_\x9f\xa8={\xdb\b\x89\xfaP\x7f\xfa\x16m\xaa\xc0;\\\xdd\x1d\x84h\xf0\xb7\x8eʎ)\xc4,\xdd\t\x98\xad\xa1\xcax6\xa4\x15\xbf\x98\vr\xec\xf7f\x93\x936\xf2\x18\xe9\xb2=\xe2_\a\xceh\x90\x84W\x8f^\xa6!\x1c\xba\xf8\x16B\xb5\xe0K[$\x85\xa22k\xd2b=q\xbdW\xcb\x1d\xbe\x1bix\xaf\xee\xa2%w:z#\xe9y\xa4z\x1c0\"\x17v\xfa\x04\x84\xa8!\xf75\x10\x95h%\x13\rr\x88\x0fa\xf3=\t\xaac*\x1b,)Z\aӋ\xae\xa2\x877\xd4\x195\x82\xf5m\xbeK{\x84fg\x91{?\x9a\x10²\xf6 \x17\xc5\\hKgI\xa2\xb2k\x1a\xb6ip\r\xcet\xcb\xe9#\x96آ\xb5\xac:e\x8a?\x85U\xa1\xe1\xd2o\x01\xb6Q\x9d\x1b:.\x93xui{\x9d:\xaf\xe9\x93\xece̢\x00՚\xb6\xaf\xb9\x9a\xc6\xef\x19;\x82\xfd#\xa9G\xb5\xc1t\xce\xf6\x12\x9f\x00\xe0_\xffN!\xa45)\x03\x1b\xbc\xd7Q\v\x83#N\xf9\x06\x1f\x13\xa3\x8bW\xcb\xf1\xe4U4\x99\xc4\xdcGo\rg\x9d\xbfgtJ\x04\xfd2\xa8U\x13\x8dY9\u058c\x02\xf5f\xe7\xd0N\xddy\xaa\xbd\xe6\xcb\xf2\xbd\x18G\xfb\xe3\xfd\x05J}\xa7\xa1`ҷ\xc1ɺ\x9c\x02.\xacn\xd8.\x95\x82D\x84\x94z\x93q\x91\v\xd8\xebs4j\x8d!\xe597\xc3\xf1\x98>(y\xa0P\x8c\xf6,\xa4\xfb\xeb_^\x90b@\x10\xe7{\xe2\xf2m8\x1cIb\xacd\xda\xd6\xca]\x7f8\xa1\x05w\xc3\xc2h\r\xfb\x1c~\xc8!#\xb5^\x15RW5\xf8\x96\xb3Lu\xfa^~\n\xead\xf1\x89(Կԧb\xd0\x1dR\xea\xe90<\xe9\\\xcd\xdf\x1c_\x83\x15\xbeOM\xa5@\xa8\rB\x17\xc9Rp\xa2\xd4J\x19L\xb8LX\x86\x95I\x10\x99\xc2\xff3\xe3GRO\x16\x83\x1e9\x1f\xd1\xee\xdf:\xc6#\xddfx\xc7[\xc3\xef\x7f\\\xfc'\x00\x00\xff\xffo\x05\xb2\xa4Q#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Y_\x93۶\x11\x7fק\xd8q\x1e\xae\x991\xa9\xd8\xedt:z\xb3\xef\x9aε\xc9Yc\x9d\xfd\x92\xc9\x03D\xacHD$\x80\x02\xa0tj&߽\xb3\x00A\xf1\x9f\xa4\xd3M.\xe1\x8b}\xc0b\xf1\xc3\x0f\xfb\x0f\xab$IfL\x8b\xafh\xacPr\x01L\v|r(\xe9/\x9bn\xffaS\xa1\xe6\xbbw\xb3\xad\x90|\x01\xb7\xb5u\xaa\xfa\x8cV\xd5&\xc3;\xdc\b)\x9cPrV\xa1c\x9c9\xb6\x98\x010)\x95c4l\xe9O\x80LIgTY\xa2Ir\x94\xe9\xb6^\xe3\xba\x16%G\xe3\x95ǭwߥ\xefާ\xdf\xcd\x00$\xabp\x01Z\xf1\x9d*\xeb\n\rZ\xa7\f\xdat\x87%\x1a\x95\n5\xb3\x1a3R\x9e\x1bU\xeb\x05\x1c'\xc2\xe2f\xe3\x00z\xa9\xf8W\xaf\xe7s\xd0\xe3\xa7Ja\xdd\x7f&\xa7\x7f\x10\xd6y\x11]ֆ\x95\x138\xfc\xac\x152\xafKf\xc6\xf33\x00\x9b)\x8d\vx (\x9ae\xc8g\x00\xcd9=\xb4\x04\x18\xe7\x9e9V.\x8d\x90\x0e\xcd-\xa9\x88\x8c%\xc0\xd1fFh\xe7\x99i\xf5\x80ڀ+\x90\xb6\xf4\xac2!\x85\xcc\xfdP\x80\x00N\xc1\x1a\xa1A½2\x80_\xac\x92K\xe6\x8a\x05\xa4D\\\xaa\x15Oe\xd4\xd9\xc8\x04\xce\x1f\x06\xa3\xee@\xe7\xb0\xce\b\x99\x9fB\xf6;\x83\xea\xe1Y*\xfeL$\x8f\x05z\x99\x88\xa6֥b\x1c\rm^0\xc9K\x042Pp\x86I\xbbAs\x02E\\\xf6x\xd0}$_\xa2\xbe\xce\xcc5\xec\\CE\x90\xedm\xff\xb5;tiߥ\xe2\xcd\x02h\x8c\x1a\xacc\xae\xb6`\xeb\xac\x00f\xe1\x01\xf7\xf3{\xb94*7h\xed\x04\f/\x9e\xea\x82\xd9>\x8e\x95\x9fx]\x1c\x1be*\xe6\x16 \xa4\xfb\xfb\xdfNck\x16\xa5N9V~<8\xb4=\xa4\x8f\xc3ဖ\x9c-o\xae\xffO\x81\xbb&HwJ\xf6y\xfd8\x18\x9d\x02\xdbQ\x1a\xe3m\x9a\x19\xf4\xa1\xf6QTh\x1d\xabtO뇼\xaf\x8f3\x17\x06\xc2\xf4\xee]\beY\x81\x15[4\x92J\xa3\xfc\xb0\xbc\xff\xfa\xd7Uo\x18@\x1b\xa5\xd18\x11\xa3k\xf8:ɣ3\n}foHa\x90\x02NY\x03mp\x8a0\x86\xbc\xc1\x10\x9cEX0\xa8\rZ\x94!\x8f\xf4\x14\x03\t1\tj\xfd\vf.\x85\x15\x1aR\x03\xb6Pu\xe9#\xd0\x0e\x8d\x03\x83\x99ʥ\xf8_\xabے\xefѦ%s\u0604\xf8\xe3\xe7c\xb0d%\xecXY\xe3[`\x92C\xc5\x0e`\x90v\x81Zv\xf4y\x11\x9b\u008fd!Bn\xd4\x02\n\xe7\xb4]\xcc\xe7\xb9p1if\xaa\xaaj)\xdca\xee\xf3\x9fX\xd7N\x19;\xe7\xb8\xc3rnE\x9e0\x93\x15\xc2a\xe6j\x83s\xa6E\xe2\xa1K\x9f8ӊ\x7fc\x9a4kozXGN\x17>\x9f\xeb\xce\xdc\x00%;\x10\x16X\xb34\x9c\xe2Ht\fٟ\xff\xb9z\x84\xb8\xb5\xbf\x8c!\xfb\x9e\xf7\xe3B{\xbc\x02\"L\xc8\r\x05]\xbačQ\x95\u05c9\x92k%\xa4\xf3\x7fd\xa5@9\xa4\xdf\xd6\xebJ8\xba\xf7\xff\xd6h\x1d\xddU\n\xb7\xbe\x92\xa0xYk\xb2\\\x9e½\x84[Vay\xcb,\xbe\xfa\x05\x10\xd36!b\x9fw\x05\xdd\"h(\x1cX\xebL\xc4\n\xe6\xc4}\r\xab\x92\x95ƌ\xae\x8f\x18\xa4\xa5b#2\xef\x1b\x14~\x80\x8d\xe4Ӟ\xeaiץoͲm\xadWN\x19\x96\xe3\x0f*\xe8\x1c\n\r\xb0}\x9cZ\x13\xc1\xc9N\xce\v\xca\xc1\x06ɑR\x802.\xde\x17h\xb0\xbbƠVV8e\x0e\xa48d\xcbt\xa4\xe1\xc4E\xf8#+~\xe1\x18\x14\xee\xbdC\x18ܠA\x99a\x8c\x10\xe7*\x99\x89St\x12\xfa\x18\xe2i\xea\xe1L\xf4\x9c\x04\xfcay\x1f#fd\xb8\x81\xee\xc6\xfb^\xa0\x87\xbe\x8d\xc0\x92\xfb\x84ry\xef\x9b\xfbM\xd8\xcc\xc7\x0e\xa7\x80\x81\x16\x18*\xd26\x18\x83\x90\xd6!\xe3\xa06\x93\x1a\xe9m\x00\xe4`\x06\x9b\x15oC\xa4hB\xd21\x84\x13\xf5\xc0(F\t\x0e\xff^}z\x98\xffk\x8a\xf9\xf6\x14\xc0\xb2\f\xad\xf5\xf9\x1a+\x94\xeem\x9b\xb39Za\x90S\xe1\x82iŤؠui\xb3\a\x1a\xfb\xd3\xfb\x9f\xa7\xd9\x03\xf8^\x19\xc0'V\xe9\x12߂\b\x8c\xb7\xe1/ڌ\xb0\x81\x8eV#\xec\x85+\xc40i\xb5\f\x90u5\xc7\xde\xfb\xe3:\xb6EP\xcdqk\x84Rlq\x01o|%x\x84\xf9+9\xd6ooNh\xfdKp\xa07$\xf4&\x80k\xf3]\xd7#\x8f ]\xc1\x1c8#\xf2\x1c\x8f\x85\xe8\xf0\xf3\xc1\x9bBⷠ\f1 UG\x85WL\xb7\x17\xe2\x11\xf2\x11\xe8\x9f\xde\xff|\x12q\x9f/\x10\x92\xe3\x13\xbc\a!\x037Z\xf1oSx\xf4\xd6q\x90\x8e=\xd1NY\xa1,\x9ebV\xc9\xf2\x10\xaa\xfd\x1d\x82U\x15\xc2\x1e\xcb2\t\xf5\x06\x87=;\x10\v\xf1\xe2\xc8\xde\x18hf\xdcYk\x8dU\xc6㧻O\x8b\x80\x8c\f*\xf7\xf1\x8e\xb2\xd3FP\xd5@\xe5B\xc8y\xde\x1aGI3~\xb6\x0e\xe6\xe3\x14d\x05\x939\x86\xf3\"lj\xcaB\xe9\xcdK\xfcx\x9c\xfa\xe37Q\x02\f\x03ǟ\x96D\x9fy8_\xa9>\xe3pݷ\xd6\xd9\xc3m\xeb5\x1a\x89\x0e\xfd\xf9\xb8\xca,\x1d-C\xed\xec\\\xed\xd0\xec\x04\xee\xe7{e\xb6B\xe6\t\x99f\x12l\xc0\xce\xfd\x93y\xfe\x8d\xff\xe7\xc5g\xf1\xaf\xeb\xe7\x1e\xa8\xf7\xe8\x7f\xcdS\xd1>v\xfe\xa2C\xc5Z\xf1\xf9y\xecf\xd5\x140õ\xe4\x16\xfbBdE|\x0441\xf6\x843\t\xaa8y\b\xcdL\x1e^ݔ\x89\xd0\xda\x10\xa2C\xd2\xf4\xb4\x12&9\xfd\xdf\n\xebh\xfcE\f\xd6\xe2Y\xee\xfb\xe5\xfe\xee\x8f1\xf0Z\xbc\xc8WO\x14\xba\xe1{J\x8e\xb0\x92\x8a\xe9$H3\xa7*\x91\r\xa4\xa9\xf6\xbb\xe7D\xfcF\xa0\xb9P\xc5}\xee\t\xc7*t\xa2\x8ale\xae*#\xadd\xda\x16\xca\xdd\xdf]\xc0\xb1j\x05#\x86\xe3u5\xc5c\xd45h\x02]\x87\xc7\xfb\xcb\xc3\xe9@\xd2\a\u0557\x8eȔ\x11\xb9O[\xad\xef\xfbW\x84d\x15\xeb6\xff\xba_Ŵ\x162\xbf\nk\xb7\x97v\x01藎hDy\xa1\x9b\xe7\x8a)\x9c\xbd\x1e\xdf\x18-ʺ\x1aCI`\xab\xb4`\x13\xe3tG#\xfb\xa4\x897\xe3\xba\xe6\f\x13\xc1\x00.pд\x9e&\xdeQ\x8d\xfd\x84\xbaҏ\xd0\xdb\xc5[\xd1t@\xbe֮\xe8\xd9MEr\x1fa2\xfd:\x1c\xc8h\xc5gCҺ.9\x98<:\xd4p\xa2o\xab\x83\xd9^K\xb4{\x9a\xf1\xc3\xda\xf7ۮyZ\x87\x1e_\xc3{\x88\xf0.v\xfe\xe8y\xf3\xe2\xc7u\xa6\xe8\xe9\xd0k\xcf]\xb0\x81\xdb\xf1\n\xdf\xc92\xbc\xf1\tQ\xa1\x7f\xb1\x86\xf6\xe4\x9eٸ\xc9\xd4}CG_X\xea\xb3*\xa9C\xee\v{zwl\x98(\x91C\xfb+\x8bo\xa5[\xdfҹ\x99\xaac\xa3\xa2\xda\"\xf7qc\x02\xf4x]\xec\x92r\xe60!\x15#\tY\x97%[\x97\xb8\x00g\xea\xf1\xf4\x19\xf7\xaa\xd0Z\x96_\xf2\xaf\x1f\x83Tx\xf37K\x80\xadU\xed\xdaG\x7f\xe3h\r\x157\xb6\xb1\x82\xeb\x1a\x0f\x05\xb3\x97\xa0,If\xca\xe2Z\x97?orp&\x94=\xe0~btԵ\xeeN\xdeF\x13\x9a\x98\xfb\xde[\xc7U\x044\x1b]\xe2\xa0\x11\x83B\x95Ѻ\x95\xa3\xa4TWk4D\x84o\x95GFb\xe0\x98\xea\xa2\xf8\xd7בɣ\x86\x18\v\x83\xaa\xe6=\x991雊d\xbfN\x01\x17V\x97\xec0\xa17\x9e\xc4\x17Xd\xbe\xe4GG\x8b\x89^H\xee\xef\xe7\xae\xed\xfe\xb4?\x05L\x97\x7fS?,L\xddB\xf7W\x82\xc1|\xfb\x1b\xc8\xeb\xecp\xa6䳎\x19\xf7ܰ\xb7\xea\t_\x8ax^\xf5t\xbc놮q\xa0\xeao\xf3GƨI\xa2F\x83\x1e9\xef\xe8n:\xa7ݑz\xdd\xfe.\xb0\x80_\x7f\x9b\xfd?\x00\x00\xff\xffg\b\x17r\xc1\x1f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xdc]O\x93\xdb:r\xbfϧ@9\a'U#y\x9d\\Rs\x9b\xf8\xd9Ye\xf7\xd9Sc\x97\xdf\x19\"[\x12vH\x80\x0f\x005VR\xf9\xee\xa9n\x80\xe0\x1f\x81$\xa8\x19\xbd\xf5\x06\xb7\xa1\x80\x06\xd0\xddht7~\xc0\xacV\xab\x1b^\x89\uf80dP\xf2\x8e\xf1J\xc0\x0f\v\x12\xff2\xeb\xa7\x7f7k\xa1\xde\x1d\xdf\xdf<\t\x99߱\x0f\xb5\xb1\xaa|\x04\xa3j\x9d\xc1/\xb0\x13RX\xa1\xe4M\t\x96\xe7\xdc\xf2\xbb\x1bƸ\x94\xcar\xfcl\xf0O\xc62%\xadVE\x01z\xb5\a\xb9~\xaa\xb7\xb0\xadE\x91\x83&\xe2M\xd7\xc7?\xad\xdf\xff\xeb\xfaO7\x8cI^\xc2\x1d\xd3`\xac\xd2`\xd6G(@\xab\xb5P7\xa6\x82\fi\ued6a\xab;\xd6\xfe\xe0\xda\xf8\xfe\xdcX\x1f]s\xfaR\bc\xff\xd2\xfd\xfaWa,\xfdR\x15\xb5\xe6E\xdb\x19}4B\xee\xeb\x82\xeb\xf0\xf9\x861\x93\xa9\n\xee\xd8g\xec\xa6\xe2\x19\xe47\x8c\xf9\xa1S\xb7+?\xea\xe3{G\";@\xc9\xddx\x18S\x15\xc8\xfb\x87\xcd\xf7\x7f\xfb\xda\xfb\xccX\x0e&Ӣ\xb2\xc4\x00?6&\f\xe3\xec;\xcd\r\a@\xbcf\xf6\xc0-\xd3Pi0 \xada\xf6\x00\x8cWU!2bu\xa0ȘڅV\x86\xed\xb4*[j[\x9e=\xd5\x15\xb3\x8aqf\xb9ރe\x7f\xa9\xb7\xa0%X0,+jcA\xaf\x03\xadJ\xab\n\xb4\x15\rc]\xe9\xa8K\xe7\xeb`.oq\xba\xae\x16\xcbQO\xc0\rٳ\fr\xcf!\x1c\xad=\b\xd3Nm8\x1d?%.\x99\xda\xfe\r2\xbbf_A#\x19f\x0e\xaa.rT\xaf#hdN\xa6\xf6R\xfcw\xa0mp\xa2\xd8i\xc1-xy\xb7EH\vZ\xf2\x82\x1dyQ\xc3-\xe32g%?1\r\xd8\v\xabe\x87\x1eU1k\xf6+\x89G\xee\xd4\x1d;X[\x99\xbbw\xef\xf6\xc26\xcb$SeYKaO\xefH\xe3Ŷ\xb6J\x9bw9\x1c\xa1xg\xc4~\xc5uv\x10\x162[kx\xc7+\xb1\xa2\xa1KZ*\xeb2\xff\xa7 \xb6\xb7\xbd\xb1\xda\x13j\x9e\xb1Z\xc8}\xe7\aR\xf3\t\t\xa0\xc2;]rM\xdd,ZF\xe3'\xe4\xce\xe3ǯߺz&̐\xfb\xc4\xf7\x8e\xf2\xb5\"@\x86\t\xb9\x03\xed\x84Hچ4A\xe6\x95\x12\xd2\xd2\x1fY!@\x0e\xd9o\xeam),\xca\xfd\xf7\x1a\f*\xb4Z\xb3\x0fd;\xd8\x16X]\xe5\xdcB\xbef\x1b\xc9>\xf0\x12\x8a\x0f\xdc\xc0\xd5\x05\x80\x9c6+dl\x9a\b\xbafoX\xd9q\xad\xf3Cc\xbcF\xe4\xe5W\xff\xd7\n\xb2ފ\xc1fb\xe7\x979\xdb)\xdd3\x0e\xd8d\xdd#\x1a_\xb4X\xdc\xeaG\v6\xfce0\x94\xff\b\x15Q\x7fp\x10\xb5\x14\xbf\xd7@&έX83)g$Y3>R\x8b\xf5\xd9\xef#<\xc5\x02?\xb2\xa2\xce!\x0f\xd6\xf6l.\x83\x11\x7f\xe7d\n?]I\xb1\x95#|\x8cX;\xfa\xb7\x13\x9b \xc9P͟\x0f\";8'\x00u\x93\xe8\xb0\\\x81!Á\x8e\xeail\x92lN\xf6\xbe\x93)\xd3і\x9955\xa4\x173'mI0\xb7m\x991\xbcg\x86\xc5\x7f\x8f\xee\x9cm\xf9\xff\xc9\xd8f'\xb9@i7gM_Wi)\xa8Bg\x7f\xb3cPV\xf6t˄m\xbe\xceQ\xe4E\xd1\xe9\xff\x1fX0\xcb5~3l\xf9\xaa\x1a?)\x959\x8a(\x95\xd0\xfd?\xa0Ph\xb3\xf8\xea\xf7\x8ad\x81\xfc\xb5\xdbꖉ]\x10H~\xcbv\xa2\xb0\xa0\a\x92y\xd1zy\rf\xa4\xecwXJn\xb3\xc3\xc7\x1f\xe8٘6ϔȗac\xe7\x1271B\x7fc\x9e\xa1\xcb(|\x15\x1aJ\x17\x16\x7f#n\xb6_(\x9e\xb8\xff\xfc\v\xe4S\xecai\x9aw6\x91\xfb\xc1`\xbb]{??u\x1a\xde\xf5\t1\x93Kx\xdc2Ξ\xe0\xe4<\x16.\x19\n\x87[\xf2w\xa3\xd1\xd39s(\xf3BJ\xf6\x04'\"\xe3S)\xb3\xadSU\xc1\x95'\x88\xb8\xfb\xb1\xd2c \x8e\xc9\a\xb8\x8e\x93\xf8\x81\x18A\x81w:\xf3\x18\xa5\xc5\x1a[4?9\x96nH\x9a\xd2\xf0\xfe\x82i\x06\xb1u҇$طƉ\bW\xc1AT\x89\x13\xa5\xec\xa1\x01Z-Mb\xec;/D\x1e:rz\xbf\x91\xe3\xdep\xbf|Vv#o]DfHK~Q`>+K_\xae\xc2N7\xf0\v\x98\xe9\x1a\xd2\xf2\x92\xcel#\x1f\xba\x19\xb6\x04\xe5ve\xe3\x12)A<°\x8d\xc4\xc0\xc5\xf3\x83\U000a5bbb\xe9\xfd\xa1_\xca\xdaP\nM*\xb9\xa2\xadr\x1d\xeb\xc91;\x91\xa4\xd2=\x89\x9c\x0f-t\xea:L$\xfb\rw\x12\xd7\xdee\x80\v\x9eA\xdeD\x9b\x94\xb7\xe4\x16\xf6\"c%\xe8\xfd\xd4\xc6\xd1-\x15\xda\xf7\xb4!$Z]W\x16jX\xda\xd6\xde\x14o\xba\xf3\xf9\xc1\xacp\xe5&\xd4j\x84=[u$]9^u~F\xb4Œ\xff1\xcb]\x9e\xe7t\x84ċ\x87\x05\x16\x7f\x81,\xce\xf7~70\xb7C\x96\xbc\xc2\xf5\xfb?\xb8͑B\xff/\xab\xb8\xd0\tk\xf8\x9e\x8e\x89\n\xe8\xb5\xf5\x89\xb1n7\u06030\f\xe5{\xe4\xc5y\"<29\x85\xb6\x05\n\xb7\x91\xabݙ\xc7r˞\x0fʸ=u'\xa0\x88\xa5l\xfaE\x18\xf6\xe6\tNon\xcf\xec\xc0\x9b\x8d|\xe36\xf8\xc5\xe6&x\vJ\x16'\xf6\x86ھy\x89\x13\x94\xa8\x89\x89\xd5~\xac\x9eBJnU\xf2j\xe5\xb5תRd\xa3\xedd4=ޖ\x9e:uS\xe4mnܻ\xc7S\xb3M\xd2\xdfJ\x19\xfb\xe7x\xa2od<\x0fM\x8b\xbeO\x1bɗ\xcd\xfa\xfa>\xf7\x15\x8c1z\x80;\v\xda'\xff\x9c\x81n\"\x87\x17\xc6Tsɽ\x90\xd8\xe3!!\x8b\f\x9e\xd1&wT\x922\xc4%\xde&\xf2e\xa1\x9f\xfe\xf1G'7\x89+\x1b\xff\xeeN䵽\xe1L\x95%\x1f\x1e\x0e&\r\xf5\x83k\xd9\xe8\xb4'䤯\xf75\xad\xe7t7\xb1\xd1!:\x16|\x16\xf6 $\xe3\x8d\xd9\x00\xed\x15\x8a\xb3J\xcd[0W\x0eܰ-\x80\f9\xf5\x9fa\x9f/\x85\xdcP\a\xec\xfd\xab\xfb\x05\xace\xd7E\xe2lX\x1d\x04\x1a>\xd0N\x95\xeaR\xa9\x9c=\x1f@CO+\xce\x13\xe5\xe8i&\x92\x94\xcav\xf3\x11H\xb7R\xf9[\xc3vB\x1b\xdb\x1dh\xaa\xc2\xd5&U\x1d\x16J\x18g\xf7M\x94\xa0j{\x81\f>\xb6\xad{\xe7\xba%\xff!ʺd\xbcTu\x82S\xe0\n\xee/\xa2\f\x87\xaf^\x02\xcf\\\xd8p\x0eE\x99\x19\xabPJU\x016U\xc4[ء9ʔ4\"\a݀\x03\x9cd\x85\u0085\xbb㢨c\xc7>\xb1\xb24\xbc\x95\x1f\xb5\xbe(\xba\xfd\xe2Zv\xb2\x8d\a\xf5\xdcgP2\v\x0e\xfc\bL옰\fd\x86r\x01\xedL6u\xe1\x99A\xacIV\xcb4\x03\x8f\x05d]\xa61`E+[\xc8\xc9dZ\xb7\xfa'.\x8ak\x88\r5\xef\x93ҏ\xc0\xf3K\x120\xbfu\x9a3\x90\xa6\xd6tp\xef\xcc˳(\xd2ƌ\x92c\x05\xafev\x00\xb2S\xb2g>\x98#/\xa4\xb1\xc0Su\x01\xbd\xa6ZJ!\xf7i\xb2KNq\xb6űz\xabT\x01|\bx\x8a\x15\xe4\xf5\xe5f跶\xf5\x1fb\x86\x82\x04\xd2݅-xQy[ĭ\x85\xb2r\xebM1]\xcb\xee\xees\x05+\xb4$\x06\xf7\xa3x\xcd\xe0ZH\x91 \xd8\xc1\x99\x8b\xb0]\xcf\x12I\\ճ\xc4\x0e\x82SqI\xfal\xd3#\x80\xab\xb3\tRh\xecAk\x16x\x99[`<\xcf!w\x89ItU|\xcc\xe2\xe0e#P\x85\xe8얻\x89I\x92mJ/\"\xa5T\xac>ª\x96OR=\xcb\x15E\xf2f\xb1\x01IO\r\xbej\xf7\xf6bK\xf4GZ\xa1\xbe\xbe\xa6\xebT\xe3<]\xc1\xca$\xeb͢lȔ\x16\xcc\xd95\a]\x1e\xf9qv\x14S\xfdO4\xf6\a\xcd\x1f\x1c\xe68\x15϶\x89\xb7\xea8\x7f\xcf\a\xb0\a\xd0\r\x98yE\xb8혝nϣ\xdb8&\x00\xdcP\x7f\x1aW\xd8\x01/\a\x90\xb7x\xa0\x83^\xc0-*6\xaf\v\xeb\xe0Ǻ\x8e(Q\x12\xf0+\xee\x19\xa4 '\xe6\xf0\x12}\f`\xc0+4 @\xd5t\x12\x99\xa1\x93\xa5C\xfav\x0f\xe3\xfb\xc0\aJ\xf95#\xfd\xbb\xc3\x03\x130\r3H\x86i\xd0\xe4\x14\xbf\xceզ˱V\a}=\x8f\xa6\xfd\xb9\xd8g\xa1\xfcR\xf9u0\xea\x80\xf69\x18i2\x80\x83\x90\xe5Ɛ\x9d\x80\x05\\Č\v\xaeB\x9f\x0eD\x8a\xf7\x19\xadD\xd5\x106\x94j\xf6\xabͣۅa\xef\xd9A\xd5\x11H\xdd\x04wf\x00\x16\xe3\xb0\n\x7f\x88\x00\x96\x1f߯\xfb\xbfX\xe5A\x16\x94\xf9\x8a̎\x02\x956\x9b*d.\x8e\"\xafy\xd1[d\x1d\xb5h\xb5\x87)ͤ(b竨VM\xfb\x9e\x1a\xb1/\x95;gYl\x8e\xa6]\xc44,\xc6\xc5\b\x8c>\xc2bd\x93Zz\xe4\x90\x0e5M\xc7XL\x83\"\x96 +\x86\xb8\x89Q\xa2\xf3x\x8a\x14\xef~\x06;q\x01b\"\x11-\xf7\xe2\x03\x92\x14L\xc4EH\x88Y@Y\"\xfe\xa1\x8fl\x98&\xb9\x00\xf5\x90Ĝy\x84\xc3b\\\x83\xc7\x11L\xce#\x19\xcd\x10\xc1)L\x12\x1e\xc50L\xa1\x13\xa6Y\x1eA.\xa4c\x12&I\x13^a\x1e\x89\xf0zx\xc3\u05c8\x02\xc6M\xcd,\x9a\xe0EQB\x02^`\tJ`\x96c\x17\"\x02\u0089\xffH\xbfKq\x00\xfds\xfe\x11\xa2)\xa7\xff#\xa7\xfb#\x14'\xcf\xfcS\xcf\xf4Gh\xcfl\xbb\x93Z2\xf9㒳\xfc\x10\x86\xfcʫJ\xc8\xfd\xb9\x9e\xa4jӤ&\x9d\x01\x01\xba}\xf6T\xa9\x1b-\xf4\xe2\xacX\x97\xeeVn$&k\xd2zBZ\xb5f\xf7\xf2tF\x97\xae\x04Dc\x90\xfe\xb5-\x1cֳ(\x8a\xee\xdd$\"\xdb%\xe5o\xf9\x99xf\x00+\x8ey\xd8Q\x11*\xdd\xf3\x8e\xe7B\xb0/\x83\xea\xddDᴷ\x1ds\xb4\x85=\\\xe8m\x97uaE\x15]\xf2\x95VGAi\xc7\x03\x9c\x02?\xff\xa6\xe8V\xd0\xf6D\x94\xbe<\x86ո\x1e\x04\x0e<\xb6\x86\x9e\xa1(\x187\xe7\xd3\xcf\xdc\xc5\xd8L\xad\xe8\xae\x1bJ\xb2\xd1\a\x7f\x81\xf6\x96Vl,b\x97͕\xcd\x12\xc9\xd0\xe5Z\x13Ɉ\x8c\xeeE\xd3\xfe\xb0s\xdd\xe9\xdb\xef5\xe8\x13SG:\xd3\xf7\x0e\xd2\f\xec\xde\xd9\x15\x83\xf1[c鼹tױ\aqBk_ؽt;v\x94\xec`\x8cD\aM\\\x1b\x1b\xa15ǰg\xa4j\x94\xaaT\xa1u\\\x1f&7\xa6T\xcc\xfau#\xa5\xe5\xb1Ҭ\x97r\x95x\xe9\xf2\x88i\x82d*\x06=\xedLd\x16s~\xad\xc8i.vJv\x1a\xd30\xe5\xd7\xc0\x92/\xc0\x90/\x88\xa1\x96EQ\xc9lJ\xc1\x8a_%\x96\xbab4u\x8dx겈j\x86\xe4\x00\x03\x9e\x82\xeeN:\xc6K>\xb3I9e\x9b?9\x9eFm'\xa0\xb5\x13N\x83\xe6F\x9a\x80\xca^\x86\xc6N\xe0\xe1\x95b\xad+E[\u05c8\xb7\xae\x1bq\xcd\xc6\\\xb3\x9a3\xf3\xf32\x14\xf5Ň\f\xcdq\xf4g\x95Ã\xd2v.@x\x18֏\x1c\x01v\x82&U\xe4L6Uc'\r\xe8\xfb{\xbf\xff\xb2I\xc5O\xeb\x1a\xf7\xf7W\x95\xe3\xd8\xe6\xce\x16\x1e\a\xd5Ϯ\xd0\xee@\x83t\x0fK\xfc\xd7\xd7/\x9f\x03\xfd\x98?\xea\x9d\xde\xc1\x9b\x06\xce\xc1\xc8=s\xfc\xe9\x93\a\xdc8n\xd1\x1e\xfeʇ\x04\xbc\x12\xffIov\xcd'd\xee\x1f6T\xb5\xf1\x96譯p\xa0\x1f\xce\u07b6\x80\xbbG\xe0Ȩ\xf6ov=\x8a\x11\xd8i\xf8\x93ыI\xcd\xee%\xc60Y\x0e\x84\x84\xab\xeea\xe3F\xb7f\x9f\xd0u\x93'\xa6\x9c\xe2\x1d\x84\xceW\x15\xd7\xf6D\xdaan\xc3\x18Ɠ2\xcd\x1e2\x95:\x195\xb5\xe7oAEy\xdb<\tE\ap\xa7\xaa\x7f\x9a9\xe4\xe8%\xe3\x18\xbf=1{o\xe2\x15\xc71\xbe\x1d\xaf\x88S\x91\xcfQ\x04ī\xa5\xa4\xbc\x19z\xf8>g\xd6\x1eC\xc5i{\x86\x91l\x93։\xf0\aۓI3\x92W\xe6\x10\xc9\n\xbd̦\xd1CU\x96\xdb:q>\xaenoJ\";t\f\xd034&Jw\x9e\xed\x1b\x8c\tת#D\xdb0\xb9\xd0R\x14\xb7\x9d\xc0\xfc\x8f9\xf2L|\x16\xe4\xe2\aA\x1c{FL\x05e\x9aЌ5\xba\xd0\xf2\xe5\x82\xc3\xceY\x17.\x01\xd8:\xedw&\xbe(q\xf1[\x12\xf3̊0j\xec\x19\x89\x94\xa7\"\xfe\xae\xfc\x9c0I&;@^\x17\x90\xf0\xc0\xdb\xd7N\xd5\xf9'\xde\x1a±5\xa9\xfa\x8f\xbc!_;\xdb+:\xbc\xfd\xc7\xe4<\xd3=\xe5\x11\x88w\x97\xa4\xf3\xecݫS\x19\xfa\xf1\xa6\xce20fW\x17\rZ&\xd3\xc0-\xe4M\xf5(2\xbf\x99\xc3\x02XH|\x17Yu\x9eѻI\x90\x8c\x89\x98\xc9\t\x13\x99\xf1\xca\x12\n\x9e\xbc\x8cZk\x9a\xb2\xfbM\xedΞ\xfe\xeb\x91\x1d7Z\x1e\xce\xe8\xc18\xc6\xf22\xe2\x89\r/\x82\r[\xd0\x03\x9b:\xef\xc0w\xba/\xa4\x05TN,\xad\xcdM@T\xe6\xeb\x0emG\x86\x9c\x1f$\r9\x83#H\xa6$\xdd5\x81|\n\xbe\xfb\x8d\xd2f\xfa\b\xfa\xad\tt\bP\x84\xbe\xe2W˵\rC?\u05c8\x9d\xd2%\xb7w,\xe7\x16V\xd8\xfa\xb2\x1d2\xfez\xa1\xd6\xf3'\x1ctiŇ\xc1tӄ\xc4[\x14\xfe\xaaI\t\xc6\xf0}\xe3\xbe?\x83\x06\xb6\a\x89,\x9ez\xa0\xad\xbd\xad\xe3Wp\xc0\x9d!\xb7xfk\xee;p;e8\xfb\x89\x9d\x1b\xb8W?)\"؏\xae\x1b!-\xec\xcfN]\xfcM\xa1G\xe0f\xf8J\xec\x19#>u\xeb\xfa\x9c\x99\xe3\x81{\x92\x84;\x90\x18=*jE\x88RF\xac\x11\xf6\xbc\b\xfaU\x1d\xb8\x993\x97\x0fX'\\\xa1\xeb,\xca`)\x1fG\xc6\x14\xbfҳb\x9f\xe19\xf2\xf5\x13)=\xe5A\xe3Ki\xc56\xf2A\xab\xbd\x06s\xae\xd2+\xba\xe3!\xe4\xfe\x93\xd2\x0fE\xbd\x172@\xf0\x96U~\xe0\xda\n^\x14'7\x9eH\xdb\x0f\xcdb\x8e\xfc6\xdfz\xe4\x87)!\xf99\xcf&\x05\\\xb56\xa7\"\xa4[\xe8t\x81m\xabj\xdb]\x15oM\xbb`b\x01\xb4\xa7\xb6f\x9f\x95\x85&W/\xfaD\x05\x06\xcfƮ`\xb7Sں\x1c\xcej\xc5\xc4\xce\x1b\xeaX\xae\x81\x8b\x82|\r\xf7\xc6-: \x01]\x12v>\x1fOjZ\x15䤔\xfc\xe4\xc2R\x9ee5ځw\xc6\xf2؆\xf6\"ז\x9c\x1b\xaf\xcd)\x11\xe5\xa6[?\x84tu\xb9\x05M\x97:\xf0g\xc7:\xba[\xe7LP\xf4\x9c\x92\xd1=\xae\xce\xd5^fp9\xc7\xd3jSƇ~W\x96\x17\x9bqG\xad\x7f!!T\x0e\xb11~9\x9fF\xef5\xcfq,\xa20MS\x94Yv\xe0r\x8f\xea\xa3U\xbd?4*8f\xa9\xc7Ҩ5\xe5|*Z\xa9\xa69\U000f2d56\x9d\x94\xad?\x05\xcb\xdb\xe1N\x11\x9dfᄟ\xa9[Dnk3\xee\xdd]\xad\x98\xceļ\x9d\x91\xc6#\xfc\x8f%\x94B\x13nN2\x9b\x86\t\xbb䑘\xb8\r4Ō\xe8|\x83\x05\xbcd\xbe\xa1q\xfa|[\xaf\xb78\xb5\xbeԒɏ\xfbٯ\xc0\x0eg\xd2/\xe1\x85k9\xb6\xf0h~\x91\x91/\x12\xb7\xcf6\x80D\a\x93\xc0 \xd1\xfb\x96\xe4t,\xe3\x85\xe9y\x99sAW\xaf\xf2˼i\xea\x18}\xe9\x9f\xd7\v>\x067\xe6c\x8a?\xfc}P}p\xe7\x02=㖢\xf7a#\xcc\xf9g\xb1k\xfe-¶\x80\x7f9\xab\xf1\aߝx\xe6Z\n\xb9\x9f\x9b\xfco\xbeZ$\x1c\xf0\x14\"\x01Ad\x12!DX\x14\x104\x83\x1cy\xf9;\x04\t/\b\t\xa2\xdb\xc9\xd9GR\xe4\xbc\xc3dߓ\xff\xf2\x7f\x01\x00\x00\xff\xffT\xf5\x7f\x80\xacd\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Mw$)rw\xfd\n\x9e|\x18\xdbOU=m_\xfctk\xab{\xecz;ӭ\xd7\xd2\xf6\xc9\x17*3J\xc5(\x13r\x81,uy\xdf\xfew\xbf\b \xbf*\xc9$K\x92gfW\\\xba\x95\x05A\x10\x11\xc4\a\x04\xb0Z\xad.x%\xbe\x816B\xc9k\xc6+\x01\xdf-H\xfcˬ\x1f\xffì\x85zwx\x7f\xf1(d~\xcdnjcU\xf9\x15\x8c\xaau\x06\x1fa'\xa4\xb0Bɋ\x12,Ϲ\xe5\xd7\x17\x8cq)\x95\xe5\xf8\xd9\xe0\x9f\x8ceJZ\xad\x8a\x02\xf4\xea\x01\xe4\xfa\xb1\xde¶\x16E\x0e\x9a\x80\x87\xae\x0f?\xae\xdf\xff\xdb\xfa\xc7\v\xc6$/ᚙl\x0fy]\x80Y\x1f\xa0\x00\xad\xd6B]\x98\n2\x04\xfa\xa0U]]\xb3\xf6\a\xd7\xc8w落\xf3\xed\xe9S!\x8c\xfdS\xef\xf3\xcf\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5\b\xf9P\x17\\\xb7\xdf/\x183\x99\xaa\xe0\x9a}Ʈ*\x9eA~\xc1\x98ǟ\xba^1\x9e\xe7D\x11^\xdcj!-\xe8\x1bU\xd4e\xa0Ċ\xe5`2-*K#\xbe\xb3\xdcֆ\xa9\x1d\xb3{\xe8\xf6\x83\xe5W\xa3\xe4-\xb7\xfbk\xb66To]\xed\xb9\t\xbf:\x129\x00\xfe\x93=\"n\xc6j!\x1f\xc6z\xfb\xc0n\xb4\x92\f\xbeW\x1a\f\xa2\xccrb\xa0|`O{\x90\xcc*\xa6kI\xa8\xfc'\xcf\x1e\xebj\x04\x91\n\xb2\xf5\x00O\x8fI\xff\xe3\x1c.\xf7{`\x057\x96YQ\x02\xe3\xbeC\xf6\xc4\r\xe1\xb0S\x9aٽ0\xf34A =l\x1d:?\x0f?;\x84rn\xc1\xa3\xd3\x01\x15\x84w\x9di \xb9\xbd\x17%\x18\xcb\xcb>\xcc\x0f\x0f\x90\x00\x8cHT\xf1ڐp\xb4\xado\xbb\x9f\x1c\x80\xadR\x05py\xd1V:\xbcw\xb2\x97\xed\xa1\xe4\u05fe\xb2\xaa@~\xb8\xdd|\xfb\xf7\xbb\xdeg6\x90%O)&\f\xe3\xec\x1bM\f\xa6\xfdLev\xcf-Ӏ\x9c\ai\xb1F\xa5a\x15\xa8\x9b7 \x19S\x9aU\xa0\x85\xcaE\x16\xb8B\x8d\xcd^\xd5Eζ\x80\fZ7\r*\xad*\xd0V\x84\xa9\xe7JG\xa3t\xbe\x0e0\xfe\x01\a\xe5j9I\x04C\xc2\xe7'\x14\xe4\x9e\x0en~\b\xd3\xe2OL\xea\x01fX\x89K\xa6\xb6\xbfBf\xd7\xec\x0e4\x82\tXgJ\x1e@#\x052\xf5 \xc5\xff6\xb0\rJ\xbd%a\xb4\xe0\xf5A[h\x02K^\xb0\x03/j\xb8b\\\xe6\xac\xe4G\xa6\x01{a\xb5\xec\xc0\xa3*f\xcd~Q\x1a\x98\x90;u\xcd\xf6\xd6V\xe6\xfaݻ\aa\x83&\xcdTY\xd6R\xd8\xe3;R\x8ab[[\xa5ͻ\x1c\x0eP\xbc3\xe2a\xc5u\xb6\x17\x162[kx\xc7+\xb1\"\xd4%i\xd3u\x99\xffS\xe0\xa8\xf9\xa1\x87\xeb\xc9|s\x85\x14\xe1\x04\aP#:\x81qM\xdd(ZB\xe3'\xa4\xce\xd7Ow\xf7]a\x12fH}\xa2{G\xc2Z\x16 \xc1\x84܁\x9f\xd1;\xadJ\x82\t2\xaf\x94\x90\x96\xfe\xc8\n\x01rH~SoKa\x91\xef\x7f\xa9\xc1X\xe4՚ݐyA9\xac+\x9c\x81\xf9\x9am$\xbb\xe1%\x147\xdc\xc0\xab3\x00)mVH\xd84\x16t-㰲\xa3Z\xe7\x87`\xde\"\xfc\ns\xfc\xae\x82\xac7e\xb0\x9d؉\x8c&\x06i\xcfF\x05\f4\xa8+㳖~!55\xfc:\xc0\xc3\xe9\xb2\xd0+\x18\xb4\x1fvO\x1cn\xcd\x18ʕ\x83\x86:E\xaa!wǴ`\x87\x12\x1e\xca\f&}\xad\x97j\xdfN`2\xaf\xea\xd6\x11\x1cO\xb8J?AY\xa1ژA\xf1\xdeWC\x14\x91>y\xe35\x05\xc3\x1fԬ\xf2ڕ\x9d(7\xean\x0fȷ\x83Ƚ\xf6:\xe1*\x9b\xe4,\x96̈;\xc9+\xb3W\x16m\x9c\xaa\xedX\xad\xc1\x00n\xee6\x83F\x1d\xce#VdÉ\xd1V\xb1'.N9\xed\n\xca\xe5\xcd݆}C\x97\b\x02L\xe6,9\xb3\xb5\x96\xa4\x8e\xbf\x02Ϗ\xf7\xea\xcf\x06X^\x93V\nv\xf9*\x02x\v;\x9c\xf4\x1a\x10\x066\x00\xadq\x0e\x18BM\xd5vM\x0eG\x0e;^\x17\xd6+9a\xd8\xfb\x1fY)dm\xe1\x94\xefl\x9a\xf7D$ny\xa9\x0e\xa0\x13h\xf8\x91[\xfe\v\xd6\x1d\x90\x0ea0\x02\xe2\xd9Od\xdc\x1e#\x03\xc5&['\xa9l\xb3\xeb@\x15\x86]^\xe2<\xbbt.\xf1啫[\x8b®\x84\xa4~\"0]\xefO\xa2(B\xff\xe7Q\xc3\x11\xd7\xf1\xd6ܫ\x9f\x8c\x13\xeb\x14\xe2D\x9a\x8e(\x98J\xe5\xec@\xf5b2&\n`\xe6h,\x94\x9eR\x1dυ\x88Kڱ(<\x18öǀ\xfb\xf8\xb8e]\x14|[\xc05\xb3\xba\x1e\xefvJ\x91\x8d\xd1\xe6+\x18+\xb2\x04\xca\\\x0eI\xe3Z\x8e\x10F\xd3\x0f\x11\xa2\f(\x80.\x0f\x7fD\xb7\xdbS\b}\xa7\xa2\xe8\x10w\x9e*\x8c\xfd\x8fd\x1f\xd1\xdcgh\x84\xaf\xbdq\x17P\x90C!\x15+\x94|\x00\xedzD\xc7)H\x98\x06\x94\xb8<\x02\x15-\xad\x86\x02]\x06\xb6\xab\xd1\b\xaf\x19j\x82\xa8\x8c\bi,\xf0|}\xf9Z̃\xefYQ\xe7\x90\xdf\x14\xb5\xb1\xa0\xef0\x04\xccC\b<\xaae\aL\xfc4\t\xc0\xbb_\x85\xc8\x00\xf9\x90\xb9J+\x8a4cDj=\xb1c\x05.\xf0E\xa6zL[\x17\xab\xa3*\fX\xacr\xf9\xaf\x971%\x8a\x12\xd0\xef\xbdߏa\\CC\x8d\x9eF\x8d@l\xf4,\x94\x95=\x8eˑ\xb0PF\x888\xabr\x16\xb0\x97k\xcdǔj\x18N\x13џ\xcf\xde\x18\x88\x01\x83e\xa8\xf6\x1b\xb1x\xd8\xff?\"\x93\xcfb\xab\xa1u,.$\xb2\xb3\x10\xc6\xf6\xb89\f\x88\x1a\xcc0vF\x9ab\xd0\"\xa4\x83\x89ʭü\xdf3\xcdΙ\t1\xd1o$͋\xf3\x9eDŽ\xea\x0fH\xb0\xbdR\x8f)D\xfao\xac\xd7\x06\xca,\xa3%U\xb6\x85=?\b\xa5\xcdp\xb5\x05\xbeCVۨ\x9e\xe0\x96\xe5b\xb7\x03\x8d\xb0h\x81\xb0YO\x9c\"\xd6t\x98\xc0:\n(Za0\xae\x96\xe9\xc8<\xa2Fl(\x14\x8eE\xa12B\x1c\xbdx\xb2\xee\xb98\x88\xbc\xe6\x05\x19z.37>\xde\xe0\x17sOf\x04\xe2\x04\x7f\xe7N\x84Q \x97zQ\xb6\x92\x80\xeeu\xa9t\xcc\xf3t\xe5\x14L\x9c\f[N\xc1q,$m\x8b\xae\v0\x1e\x15\xe7\xc0\xb6z\xe7\xaa\xe5\x94[\xa0*\xf8\x16\nf\xa0\x80\xcc*\x1d'O\x8a\x10\xb8\x92\xaa?#\x94\x1dѤ\xfd hV\x89\xb6\x05\x03̽\xc8\xf6\xce\xddD)#X,W`Hc\xf0\xaa*\"V\xa8-\xb3\x92\xe1;\x9bS\x1amIP\x1fC\xb81EҖD\x1dܖ\x19mܧz#6oD\xef\xa1)\x9f%웓\xe6//\xecHn\x01\x86\x9c>\U000bab98\xb0\xe1k\nԞ\x1fh\xfe\xce\x18w\xdel\xd9\f[\xbf\xf8ly\x11\xae5h\xfc\x9d0\x8d\x8c՝\xb7U\x8b\x18\xf6s\xb7\xe5\x15\x13\xbb\x86a\xf9\x15ۉ\xc2\x02\xf9Rs\x88v\x1c\x9dYν$\x81Rm/\x96\x92\xdbl\xff\xa9Y\xd6Nh1\xa0\xd5\x10\x80\xf3\xcbC\fC\f<\x9d.\n4\xc0$\x90\x9dA\x91\x9b\xd6\xc4xn?\xef\x8aq\xf6\bG\xe7Y\x8d.\x0f\x8d\x15d-o@j\xa0\xcdER#\x8fp$P~\xb70\t\xde\x12Qq\xe5\x11\x8e\xa9U\aDE\xfc\xfc>\x85\xa3.~\xa0Q\xa4L\xa5\xb64D\xf5s\x87Y\x956X\xb6L)\x85\x12(~\xe6\xb0\x1b\x86\xf5\xb6\xc8\x1f\xe1\xf8\x83q\xec\xc3Y\xb3\x17\xd5\x02\n\xa0¦%\x19\xb5k\xf6\x86\xbf\xf1B\xe4Mg4O\x16@\xdc\xc8+\xf6YY\xfc\xe7\xd3wa\x10E\x99\xb3\x8f\n\xccge\xe9˫\x92\xd8\r\xe2L\x02\xbb\xc64-\xa53\vH\x97E\xfd\xb78\x90\tE\x11m\xd8&\f\xdbH\x8c\xcf\x1c}\x96\xb0i\x0f\x019\x87VY\x1b\xda]\x96J\xaeܒ\x96\xefm\x01\xd0.^\x9eUJ\xf78u\xb5\x10\xe2(\x8a\x1e\xbd{\xb4V\ue5d3}\xf9\xa9\xa2\xa1*x\x06y\xd8e\xa3$\x00n\xe1Ad\xac\x04\xfd\x00\xacB\xbb\x91.T\v4\xb9+gHa\xbak\x11\x8a7\v#{\xdace\x85\xb3>\xb1f`sR\xf5Ȏ\xfft\xf5\xb4Q\x92y'\x7f(\x89\xfa\xdd\x14\xb5e\x96e!\xbfN}\x10\x87\xa4s?JN\x1bO\x7fE\xf3J\xe2\xfd\xb74kȅ6k\xf6\x81\x12\xf4\n\xe8\xb6\x0f\xab\x84\x9d\xae\x92@\"&\xc20\x94\x93\x03/\xd0}@\xe5-\x19\x14ΙP\xbb\x13\x0f*M\xc5<\xed\x95q6\xbf\xd9\x18\xbb|\x84\xa3ߜ\xedj\x89ˍ\x8c\xae\xda\xf7\v\xea\xfc\x13\xa5\xd5x-J\x16GvI\xbf]\x92c\xb6d\x8a\x9c\xe1\xbc-\x90\xea\x05U\xbf\xaf\x1e\xeb-h\t\x16̪\xe4\xd5\xca\xcf\x06\xab\xca\xe8\x1e\xa7+\x94F\xb7$\x8c\xc08=x<ظI6C\xf7\x7f\x8e\x02\xc9\xf3\xa1R&\x92i\x11A\xebV\x19\xeb\x16\x0f{\xae\xfa\xc8\xeabJ\xe4\xe8W\x1c\x19\xdfY\xd0\xccX\xa5Cb\x17\xaa\xec\xc1\xe2:J\x8d\x99\x97\x1b\xb7O\xe4W2\x1d`\fP/[\xed\xe2\xec\xc1\xa5۫\xc2\xff\xcf\xc3\xcc\xc8\xd1\"ؕV\x19\x98h6B[\x12\xad\xce\xccbo\xb3\xd0\xcb]\xe0\xb7KR\xeb)\xcbС,s㑴g\x04E\x9f\xbew֬Q\x85\xe1\xdf)\xa2|\x0e\x8e\x8cr\xbb˒\x0f\x93\f\x93ѽq\xad\xc3\x04\xf4\xc0\\\xb0\xa5\x1fjRH\xcb|n/\x92\xbf7\xa7\xa5\x14rC\x1d\xb1\xf7\xaf\xe6\xe8\xb0`\x06b\x19Ice\xc0\x0e߾eH\xf3!5\xf6e!UM\xd1>\x8f\x86\x1egOwA\xd29\xc5\xd0\x11\x97\xcav\x17z|O?\x18\xb6\x13\xda\xd8\x16\xe1\x05P\x85\x99\xc8z\x1a\x1d\xde\x19\xf1\xa9\xfc\xa4\xf5\xd9\xe1\xe9\x17\u05fa\xb3$\xb9WO>\xc1sIP\x1e\x88\xbf\xe7\a`bDŽe 3UKZ,Cu\x81\xdd,\x80\xe8\x98\xe8\x8cI\xa2\xcd\xec4\x96u\x99N\x90\x15I\xa7\x90\xb3+k\xdd&?q\x91\xb6\xb2\xc5\xcec\xab\x9dJ\xa2\x1c+\xfd\xccP\x9fM\xd9\xcd\xe4-\xf9wQ\xd6%\xe3%\xb2eI̹sy\x98!\xed\xd7\xf1\xfa\x89\v\xebOS\xb8M\xd9e\xda4SeU\x80\x85\x90a\x99)iD\x0e\x8d\xfb\xe0\xf9?\x9a\xaf\x1a+\x9c\xed\xb8(j\xbd@G/\xe6\xccҘϫ\xa7\x97\x0f\xe4\xd2\x11Y\x111\x13\x17\xec\x178\xdc\xf3\xf6\xa3\xd2\xcb\\\xe6[\r/\xef\x9aVZ(ʁ\x9d\xf1Nga\x92\xf7\xda\xf7N\xbd\xf0ry\x8c\xb9\xa7\xb3P\t\x937\xf7\xb4)o\xee\xe9\x9b{\xfa\xe6\x9e\x0eʛ{\xfa枾\xb9\xa7\xe3\xe5\xcd=\xed\x947\xf74\xd9~\xa4`\xb8\xa2\x95ۉ\nIX%\xa6o̡=ӗ\xcfR\xf2gA\x96dWo\xc6[\x8e\x9c\x05Zt\x86\xc4t\x8c^\x93n\x8dS2L&w\xa64\xc1\v\x7f\x81\xb36\x01\x81\xb3\xcf\xdal&\x01\xbc\xe0Y\x1b\x8f\xe9p\xed\xfc\x05O\xda\x04Z,?\x84q\xe5ӘJ\xe0aK\xc8\xe5\xa0\xe4\xb1nc^l\x0f\x8f\xd1:\xbfq\xd6\xfdI\xb6\xe6\xf9\"\xf3\xffr~'\"6'\xa7S#P\x85A\xb9\xfacp\xe2,\xdaG\xa9\xed\xfe\x17\x1b]KX\xa7x\xdd5\x03\xddT\xcb~\xca\xeb\x1fG\xb0ϑ\xe4\xd4\xf371\xe7<\xae\xdb:\xc4Խ\xf3\x1e\xbfoZZ(\xbfTޒ\xa5\x9f{ߌ4{\xc6\xc9wn\x8e2\xdbk%Um\xfc\n\x0f\xf6\xf0!sW\x01\x84\x8e\xcc\x12e\xf0\x9e\xedU\x1d9\xe31Cׄ\xcc\xdbx\xbe\xad\xcf\xe0\x00\xcb\x0f\xef\xd7\xfd_\xac\xf2ٷ\x11\xac\x9f\x84ݻ\xfb\x18x\x9e\xa3\xa3\xde9\xe2\x13&\xaf\xbf\x93e(x\x11\x88J3)\n'\x95\x01B߀~\xa9ܒ\xdf\xd9~\xcb\xfc\xc2Sz\x8e\xee\xd2\xcc\xdc&\x97r\xdeK~F>\xee\xb2\xc3R\xb3\xb9\xb7)H\xb3\x94\x8c\xdb\xf1\\\xda\x19\xa8K\xf2lS\xd7\x14\x13rj\xd33i\xd3\xc8\xc3\xe8&\xa5\xd4\xfc\xd9\xe4(45W\xf6u2d\x13\xf3b;ٮ\xb3 \xcf̆M&XZ\xe6kr\xbek'\x8bu\x9eZ\x13Y\xae㹫\xb3 \xc7r[S2V\x93pM\xceSm\xb2O\xe7wF\x9e\x95\x9d\xfa\xf2\xe7`^r\xddb:\xd74)\xc34imc\x1e\xe7\xa4\x1cҥ\x99\xa3IT]\x9a%\xdad\x80Nt\x9c\x94\x1bz\x9a\xf795\x94ٌ\xd0x\xb6\xe7\x14ر<Є\x1c\xcf\t\x90\xdd\xec\xcf\xc5n\xc0\xac4\xcdVX\x9a\xbb9~?Z(\xf3ֹ\xf8-d\xf6\xb9dR\xba\xe74\xa7\x04w_\x06MPZ\x82\x9f8\xe6\x88\xc7Ce瞟\xe1\x88G@nv\xac\xac\v+\xaa\xa2sA\x99\xddñ\xb9\xf2\xe7WE\a\u05f7G\x82\xf6\xe5k#\xf21\x90\xfd\x90\x82\x1b\xf6\x04E\x81\xff\x9eP!s\xd7\x01fj\x05h\xa5\xe2\x1b\x81\xfe\xaa#\x7f\x97\xe0\x95[\x16\xa3S\xfdd\x01K\x844}\x01֤)\x99v\x8f\x9dWO\xdf\xfeR\x83>2\xbas+\xf8AQ1kO{\xfa\xc9l0&\f\xca\xc7k1w)e_\x19\xc5gC\xa3\x02\xd8\a\xe9\f\xf3\x10W\x82\x85Z\xa7\r\xa7\xa6\x94-FO1\x10R5\x10\"\xedS\xbc\xef%\xc7\x1f_#\xb8z\x89\xf0*\xc9\x11y\x8d\x10뵂\xac\xa5a֒䍤㋯\x11l-\t\xb7\x16\xf9\x8c\xe9\xc7\x13_\xebX\xe2+\x84]g\a^\x8bH\x97z\xecpq\xf8\x950\xbe\x99c\x86'>Z\x02\xc8\xe8\xf1\xc2\xf1\x10,\x01\xe2ɱ\xc2\xd9 ,e\x1e\fôg\x1f\x12LNdZ\xb4\x9b\x9e\x9a\x84\x94\xb6\xd1=\x7f\xf8/\xf1\xd0_\xe26x\n\xf6\x89\x87\xfb\x96\x1f\xeaK\xa4\xf3\x99\xe1\xd9d\u05c9\x87\xf7\x16\x05hg\x86h\x93\x10\xa7\x0e\xebM\ai\xd3\vp\xc3Czg\xb8\x13\t\x12\x96Pe\xf9A\xbbgo\xc6(\x9d\x83\x9e\xdd\xd7Z\"γ\x82<\x88\xa3\xfa\xfd\x0fvt\u008d\xa8X\xab\xbbg\x16\xe3\xa8j\xee\x1d\xc9؟\x84\xf4\xbb\xf5(\xb8\x1d\x9f\xa4\xb7\xf1\xd6:L\xf1}\x9d\xd6K\xf5\x17\xab\xbb\x1d;\x03\x15״\x8f\xbf=\xba\xa4 \xb3f\x9fx\xb6oz\x88\x80\xa4~\xf7ܰ\x9d\xd2%\xb7\xec\xb2\xd9\n}\xe7:\xc0\xbf/\u05cc\xfd\xa4\x9a\xf4\x91νb\x11\xa8F\x94UqĈ\x89]v\xc1ti^\xd6ɂ\x88\xc5(\xd8\\\xf8\v\v\a\x97\n\xefTQ\xa8\xa73\x97.x%\xfe\x8b\xde1I[\x1b\xfbp\xbb\xa1\xeaA\xaa\xe8\r\x94&{\xae\x91\xb1-L+\xf4v\xe0\xe4zt\xa1\x8ed\xaf6\x7fN@\xa4W\x04\x82\x9f\xe1\xd5x\xa6P\x8b\xddn\x1c\x96k\x12,.\x8fL\xf9{\xe2\x85\xceW\x15\xd7\xd1M=\xe6\xe5\xc1\\\xf50\fv|n\x05kҬ\x9d\xbe\x8a\xd0-=\x9a\x87\a\x12h\xb3\xf7X\xf5\xb7щ\xd2\x1dz>\a\xa7\xe9\x83˳G\x96_\x01\xa7i\x97iET\x8c\xfc\x14M\xc7{\xf1\xd5C\xe3/\x91\xffE\x1d\xe0ct\x15\xb1\xffd\xc0\xa0\xc9H\x02]\x80:umz\x9b5\x17\xbf\xce\xfa\x052\xe2\x02*\xfe\xe2\xeb\x05\xe3\xf3-\xc6^i\xf0\xf7\x7f\a\xd8\x13\xb6\r\xa7\xec\xed7\n7\x1bu\xe9g\xb8\x0f&\xc3R\xe1\xe0\xaa\xd6\b\xc8\xd83\v/E-\xab4\x7f\x80\x9f\x95{\t#\x85Z\xfd\x16\xbd\xc7P\xbc3\x17\xb2\x89\xfd\\\x8b)s?\xb6!\xc0\xf6\x90\xc1\xc9u\xf4\x88홷\xed[[$\f\xee\xfe\xfeg7 +JX\x7f\xac]\x86\t\xea]\x03H\xe90P\xd7h\x1b\xd7N{\xf5D\xf7\xb9w\x9f\xab\xe8<\b\x04t\xa8\x81\xd2F\xcf\x1a͡\xf7 D ]\x8a\xb0\x7f\x1bo\xd9\xf1\xa1:L\x9cJ!S\xbb(,n\x8c\xca\x04\xb9]\xb4fNg\t^\xef&\xe2)\xf7yB\x7f\xd6\x06\xbe{\xd6.\x87\xce>\xb26¿\xe6Y\x9d\xe8;3.\x90p\x8f\xa0\xad\x10\xfey\xec\x1c\x9d\at\x8d\xf5\xdc\x13DX\xa79\a\xe6\tM\r\xc3\xf5\xd7w1\xd4\xc7\x0f\xf6\xac\xd8g8u\xe4W\xec\x93\xc4A\x9c\xdawwz\arZD\x1d{\x90lr\x88\x87\xa6\x15\x1d\x9d\x1a\xd1\x16}57\xa8>H\xec\xa4\xe7L\x9a*\xee\x98\xd4\x18[\xffY\xec\xdc\nw\x86c\xfa\x97\x93\x1aQ\xc55\xa9\xb4b\nktJ\x9d|4\xa0\x0f\xf4~H\x10\x12oû_\xeam{\x1b9\xfb\xeb\xdf.\xfe/\x00\x00\xff\xff\x80\xea<õr\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Mw$)rw\xfd\n\x9e|\x18\xdbOU=m_\xfctk\xab{\xecz;ӭ\xd7\xd2\xf6\xc9\x17*3J\xc5(\x13r\x81,uy\xdf\xfew\xbf\b \xbf*\xc9$K\x92gfW\\\xba\x95\x05A\x10\x11\xc4\a\x04\xb0Z\xad.x%\xbe\x816B\xc9k\xc6+\x01\xdf-H\xfcˬ\x1f\xffì\x85zwx\x7f\xf1(d~\xcdnjcU\xf9\x15\x8c\xaau\x06\x1fa'\xa4\xb0Bɋ\x12,Ϲ\xe5\xd7\x17\x8cq)\x95\xe5\xf8\xd9\xe0\x9f\x8ceJZ\xad\x8a\x02\xf4\xea\x01\xe4\xfa\xb1\xde¶\x16E\x0e\x9a\x80\x87\xae\x0f?\xae\xdf\xff\xdb\xfa\xc7\v\xc6$/ᚙl\x0fy]\x80Y\x1f\xa0\x00\xad\xd6B]\x98\n2\x04\xfa\xa0U]]\xb3\xf6\a\xd7\xc8w落\xf3\xed\xe9S!\x8c\xfdS\xef\xf3\xcf\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5\b\xf9P\x17\\\xb7\xdf/\x183\x99\xaa\xe0\x9a}Ʈ*\x9eA~\xc1\x98ǟ\xba^1\x9e\xe7D\x11^\xdcj!-\xe8\x1bU\xd4e\xa0Ċ\xe5`2-*K#\xbe\xb3\xdcֆ\xa9\x1d\xb3{\xe8\xf6\x83\xe5W\xa3\xe4-\xb7\xfbk\xb66To]\xed\xb9\t\xbf:\x129\x00\xfe\x93=\"n\xc6j!\x1f\xc6z\xfb\xc0n\xb4\x92\f\xbeW\x1a\f\xa2\xccrb\xa0|`O{\x90\xcc*\xa6kI\xa8\xfc'\xcf\x1e\xebj\x04\x91\n\xb2\xf5\x00O\x8fI\xff\xe3\x1c.\xf7{`\x057\x96YQ\x02\xe3\xbeC\xf6\xc4\r\xe1\xb0S\x9aٽ0\xf34A =l\x1d:?\x0f?;\x84rn\xc1\xa3\xd3\x01\x15\x84w\x9di \xb9\xbd\x17%\x18\xcb\xcb>\xcc\x0f\x0f\x90\x00\x8cHT\xf1ڐp\xb4\xado\xbb\x9f\x1c\x80\xadR\x05py\xd1V:\xbcw\xb2\x97\xed\xa1\xe4\u05fe\xb2\xaa@~\xb8\xdd|\xfb\xf7\xbb\xdeg6\x90%O)&\f\xe3\xec\x1bM\f\xa6\xfdLev\xcf-Ӏ\x9c\ai\xb1F\xa5a\x15\xa8\x9b7 \x19S\x9aU\xa0\x85\xcaE\x16\xb8B\x8d\xcd^\xd5Eζ\x80\fZ7\r*\xad*\xd0V\x84\xa9\xe7JG\xa3t\xbe\x0e0\xfe\x01\a\xe5j9I\x04C\xc2\xe7'\x14\xe4\x9e\x0en~\b\xd3\xe2OL\xea\x01fX\x89K\xa6\xb6\xbfBf\xd7\xec\x0e4\x82\tXgJ\x1e@#\x052\xf5 \xc5\xff6\xb0\rJ\xbd%a\xb4\xe0\xf5A[h\x02K^\xb0\x03/j\xb8b\\\xe6\xac\xe4G\xa6\x01{a\xb5\xec\xc0\xa3*f\xcd~Q\x1a\x98\x90;u\xcd\xf6\xd6V\xe6\xfaݻ\aa\x83&\xcdTY\xd6R\xd8\xe3;R\x8ab[[\xa5ͻ\x1c\x0eP\xbc3\xe2a\xc5u\xb6\x17\x162[kx\xc7+\xb1\"\xd4%i\xd3u\x99\xffS\xe0\xa8\xf9\xa1\x87\xeb\xc9|s\x85\x14\xe1\x04\aP#:\x81qM\xdd(ZB\xe3'\xa4\xce\xd7Ow\xf7]a\x12fH}\xa2{G\xc2Z\x16 \xc1\x84܁\x9f\xd1;\xadJ\x82\t2\xaf\x94\x90\x96\xfe\xc8\n\x01rH~SoKa\x91\xef\x7f\xa9\xc1X\xe4՚ݐyA9\xac+\x9c\x81\xf9\x9am$\xbb\xe1%\x147\xdc\xc0\xab3\x00)mVH\xd84\x16t-㰲\xa3Z\xe7\x87`\xde\"\xfc\ns\xfc\xae\x82\xac7e\xb0\x9d؉\x8c&\x06i\xcfF\x05\f4\xa8+㳖~!55\xfc:\xc0\xc3\xe9\xb2\xd0+\x18\xb4\x1fvO\x1cn\xcd\x18ʕ\x83\x86:E\xaa!wǴ`\x87\x12\x1e\xca\f&}\xad\x97j\xdfN`2\xaf\xea\xd6\x11\x1cO\xb8J?AY\xa1ژA\xf1\xdeWC\x14\x91>y\xe35\x05\xc3\x1fԬ\xf2ڕ\x9d(7\xean\x0fȷ\x83Ƚ\xf6:\xe1*\x9b\xe4,\x96̈;\xc9+\xb3W\x16m\x9c\xaa\xedX\xad\xc1\x00n\xee6\x83F\x1d\xce#VdÉ\xd1V\xb1'.N9\xed\n\xca\xe5\xcd݆}C\x97\b\x02L\xe6,9\xb3\xb5\x96\xa4\x8e\xbf\x02Ϗ\xf7\xea\xcf\x06X^\x93V\nv\xf9*\x02x\v;\x9c\xf4\x1a\x10\x066\x00\xadq\x0e\x18BM\xd5vM\x0eG\x0e;^\x17\xd6+9a\xd8\xfb\x1fY)dm\xe1\x94\xefl\x9a\xf7D$ny\xa9\x0e\xa0\x13h\xf8\x91[\xfe\v\xd6\x1d\x90\x0ea0\x02\xe2\xd9Od\xdc\x1e#\x03\xc5&['\xa9l\xb3\xeb@\x15\x86]^\xe2<\xbbt.\xf1啫[\x8b®\x84\xa4~\"0]\xefO\xa2(B\xff\xe7Q\xc3\x11\xd7\xf1\xd6ܫ\x9f\x8c\x13\xeb\x14\xe2D\x9a\x8e(\x98J\xe5\xec@\xf5b2&\n`\xe6h,\x94\x9eR\x1dυ\x88Kڱ(<\x18öǀ\xfb\xf8\xb8e]\x14|[\xc05\xb3\xba\x1e\xefvJ\x91\x8d\xd1\xe6+\x18+\xb2\x04\xca\\\x0eI\xe3Z\x8e\x10F\xd3\x0f\x11\xa2\f(\x80.\x0f\x7fD\xb7\xdbS\b}\xa7\xa2\xe8\x10w\x9e*\x8c\xfd\x8fd\x1f\xd1\xdcgh\x84\xaf\xbdq\x17P\x90C!\x15+\x94|\x00\xedzD\xc7)H\x98\x06\x94\xb8<\x02\x15-\xad\x86\x02]\x06\xb6\xab\xd1\b\xaf\x19j\x82\xa8\x8c\bi,\xf0|}\xf9Z̃\xefYQ\xe7\x90\xdf\x14\xb5\xb1\xa0\xef0\x04\xccC\b<\xaae\aL\xfc4\t\xc0\xbb_\x85\xc8\x00\xf9\x90\xb9J+\x8a4cDj=\xb1c\x05.\xf0E\xa6zL[\x17\xab\xa3*\fX\xacr\xf9\xaf\x971%\x8a\x12\xd0\xef\xbdߏa\\CC\x8d\x9eF\x8d@l\xf4,\x94\x95=\x8eˑ\xb0PF\x888\xabr\x16\xb0\x97k\xcdǔj\x18N\x13џ\xcf\xde\x18\x88\x01\x83e\xa8\xf6\x1b\xb1x\xd8\xff?\"\x93\xcfb\xab\xa1u,.$\xb2\xb3\x10\xc6\xf6\xb89\f\x88\x1a\xcc0vF\x9ab\xd0\"\xa4\x83\x89ʭü\xdf3\xcdΙ\t1\xd1o$͋\xf3\x9eDŽ\xea\x0fH\xb0\xbdR\x8f)D\xfao\xac\xd7\x06\xca,\xa3%U\xb6\x85=?\b\xa5\xcdp\xb5\x05\xbeCVۨ\x9e\xe0\x96\xe5b\xb7\x03\x8d\xb0h\x81\xb0YO\x9c\"\xd6t\x98\xc0:\n(Za0\xae\x96\xe9\xc8<\xa2Fl(\x14\x8eE\xa12B\x1c\xbdx\xb2\xee\xb98\x88\xbc\xe6\x05\x19z.37>\xde\xe0\x17sOf\x04\xe2\x04\x7f\xe7N\x84Q \x97zQ\xb6\x92\x80\xeeu\xa9t\xcc\xf3t\xe5\x14L\x9c\f[N\xc1q,$m\x8b\xae\v0\x1e\x15\xe7\xc0\xb6z\xe7\xaa\xe5\x94[\xa0*\xf8\x16\nf\xa0\x80\xcc*\x1d'O\x8a\x10\xb8\x92\xaa?#\x94\x1dѤ\xfd hV\x89\xb6\x05\x03̽\xc8\xf6\xce\xddD)#X,W`Hc\xf0\xaa*\"V\xa8-\xb3\x92\xe1;\x9bS\x1amIP\x1fC\xb81EҖD\x1dܖ\x19mܧz#6oD\xef\xa1)\x9f%웓\xe6//\xecHn\x01\x86\x9c>\U000bab98\xb0\xe1k\nԞ\x1fh\xfe\xce\x18w\xdel\xd9\f[\xbf\xf8ly\x11\xae5h\xfc\x9d0\x8d\x8c՝\xb7U\x8b\x18\xf6s\xb7\xe5\x15\x13\xbb\x86a\xf9\x15ۉ\xc2\x02\xf9Rs\x88v\x1c\x9dYν$\x81Rm/\x96\x92\xdbl\xff\xa9Y\xd6Nh1\xa0\xd5\x10\x80\xf3\xcbC\fC\f<\x9d.\n4\xc0$\x90\x9dA\x91\x9b\xd6\xc4xn?\xef\x8aq\xf6\bG\xe7Y\x8d.\x0f\x8d\x15d-o@j\xa0\xcdER#\x8fp$P~\xb70\t\xde\x12Qq\xe5\x11\x8e\xa9U\aDE\xfc\xfc>\x85\xa3.~\xa0Q\xa4L\xa5\xb64D\xf5s\x87Y\x956X\xb6L)\x85\x12(~\xe6\xb0\x1b\x86\xf5\xb6\xc8\x1f\xe1\xf8\x83q\xec\xc3Y\xb3\x17\xd5\x02\n\xa0¦%\x19\xb5k\xf6\x86\xbf\xf1B\xe4Mg4O\x16@\xdc\xc8+\xf6YY\xfc\xe7\xd3wa\x10E\x99\xb3\x8f\n\xccge\xe9˫\x92\xd8\r\xe2L\x02\xbb\xc64-\xa53\vH\x97E\xfd\xb78\x90\tE\x11m\xd8&\f\xdbH\x8c\xcf\x1c}\x96\xb0i\x0f\x019\x87VY\x1b\xda]\x96J\xaeܒ\x96\xefm\x01\xd0.^\x9eUJ\xf78u\xb5\x10\xe2(\x8a\x1e\xbd{\xb4V\ue5d3}\xf9\xa9\xa2\xa1*x\x06y\xd8e\xa3$\x00n\xe1Ad\xac\x04\xfd\x00\xacB\xbb\x91.T\v4\xb9+gHa\xbak\x11\x8a7\v#{\xdace\x85\xb3>\xb1f`sR\xf5Ȏ\xfft\xf5\xb4Q\x92y'\x7f(\x89\xfa\xdd\x14\xb5e\x96e!\xbfN}\x10\x87\xa4s?JN\x1bO\x7fE\xf3J\xe2\xfd\xb74kȅ6k\xf6\x81\x12\xf4\n\xe8\xb6\x0f\xab\x84\x9d\xae\x92@\"&\xc20\x94\x93\x03/\xd0}@\xe5-\x19\x14ΙP\xbb\x13\x0f*M\xc5<\xed\x95q6\xbf\xd9\x18\xbb|\x84\xa3ߜ\xedj\x89ˍ\x8c\xae\xda\xf7\v\xea\xfc\x13\xa5\xd5x-J\x16GvI\xbf]\x92c\xb6d\x8a\x9c\xe1\xbc-\x90\xea\x05U\xbf\xaf\x1e\xeb-h\t\x16̪\xe4\xd5\xca\xcf\x06\xab\xca\xe8\x1e\xa7+\x94F\xb7$\x8c\xc08=x<ظI6C\xf7\x7f\x8e\x02\xc9\xf3\xa1R&\x92i\x11A\xebV\x19\xeb\x16\x0f{\xae\xfa\xc8\xeabJ\xe4\xe8W\x1c\x19\xdfY\xd0\xccX\xa5Cb\x17\xaa\xec\xc1\xe2:J\x8d\x99\x97\x1b\xb7O\xe4W2\x1d`\fP/[\xed\xe2\xec\xc1\xa5۫\xc2\xff\xcf\xc3\xcc\xc8\xd1\"ؕV\x19\x98h6B[\x12\xad\xce\xccbo\xb3\xd0\xcb]\xe0\xb7KR\xeb)\xcbС,s㑴g\x04E\x9f\xbew֬Q\x85\xe1\xdf)\xa2|\x0e\x8e\x8cr\xbb˒\x0f\x93\f\x93ѽq\xad\xc3\x04\xf4\xc0\\\xb0\xa5\x1fjRH\xcb|n/\x92\xbf7\xa7\xa5\x14rC\x1d\xb1\xf7\xaf\xe6\xe8\xb0`\x06b\x19Ice\xc0\x0e߾eH\xf3!5\xf6e!UM\xd1>\x8f\x86\x1egOwA\xd29\xc5\xd0\x11\x97\xcav\x17z|O?\x18\xb6\x13\xda\xd8\x16\xe1\x05P\x85\x99\xc8z\x1a\x1d\xde\x19\xf1\xa9\xfc\xa4\xf5\xd9\xe1\xe9\x17\u05fa\xb3$\xb9WO>\xc1sIP\x1e\x88\xbf\xe7\a`bDŽe 3UKZ,Cu\x81\xdd,\x80\xe8\x98\xe8\x8cI\xa2\xcd\xec4\x96u\x99N\x90\x15I\xa7\x90\xb3+k\xdd&?q\x91\xb6\xb2\xc5\xcec\xab\x9dJ\xa2\x1c+\xfd\xccP\x9fM\xd9\xcd\xe4-\xf9wQ\xd6%\xe3%\xb2eI̹sy\x98!\xed\xd7\xf1\xfa\x89\v\xebOS\xb8M\xd9e\xda4SeU\x80\x85\x90a\x99)iD\x0e\x8d\xfb\xe0\xf9?\x9a\xaf\x1a+\x9c\xed\xb8(j\xbd@G/\xe6\xccҘϫ\xa7\x97\x0f\xe4\xd2\x11Y\x111\x13\x17\xec\x178\xdc\xf3\xf6\xa3\xd2\xcb\\\xe6[\r/\xef\x9aVZ(ʁ\x9d\xf1Nga\x92\xf7\xda\xf7N\xbd\xf0ry\x8c\xb9\xa7\xb3P\t\x937\xf7\xb4)o\xee\xe9\x9b{\xfa\xe6\x9e\x0eʛ{\xfa枾\xb9\xa7\xe3\xe5\xcd=\xed\x947\xf74\xd9~\xa4`\xb8\xa2\x95ۉ\nIX%\xa6o̡=ӗ\xcfR\xf2gA\x96dWo\xc6[\x8e\x9c\x05Zt\x86\xc4t\x8c^\x93n\x8dS2L&w\xa64\xc1\v\x7f\x81\xb36\x01\x81\xb3\xcf\xdal&\x01\xbc\xe0Y\x1b\x8f\xe9p\xed\xfc\x05O\xda\x04Z,?\x84q\xe5ӘJ\xe0aK\xc8\xe5\xa0\xe4\xb1nc^l\x0f\x8f\xd1:\xbfq\xd6\xfdI\xb6\xe6\xf9\"\xf3\xffr~'\"6'\xa7S#P\x85A\xb9\xfacp\xe2,\xdaG\xa9\xed\xfe\x17\x1b]KX\xa7x\xdd5\x03\xddT\xcb~\xca\xeb\x1fG\xb0ϑ\xe4\xd4\xf371\xe7<\xae\xdb:\xc4Խ\xf3\x1e\xbfoZZ(\xbfTޒ\xa5\x9f{ߌ4{\xc6\xc9wn\x8e2\xdbk%Um\xfc\n\x0f\xf6\xf0!sW\x01\x84\x8e\xcc\x12e\xf0\x9e\xedU\x1d9\xe31Cׄ\xcc\xdbx\xbe\xad\xcf\xe0\x00\xcb\x0f\xef\xd7\xfd_\xac\xf2ٷ\x11\xac\x9f\x84ݻ\xfb\x18x\x9e\xa3\xa3\xde9\xe2\x13&\xaf\xbf\x93e(x\x11\x88J3)\n'\x95\x01B߀~\xa9ܒ\xdf\xd9~\xcb\xfc\xc2Sz\x8e\xee\xd2\xcc\xdc&\x97r\xdeK~F>\xee\xb2\xc3R\xb3\xb9\xb7)H\xb3\x94\x8c\xdb\xf1\\\xda\x19\xa8K\xf2lS\xd7\x14\x13rj\xd33i\xd3\xc8\xc3\xe8&\xa5\xd4\xfc\xd9\xe4(45W\xf6u2d\x13\xf3b;ٮ\xb3 \xcf̆M&XZ\xe6kr\xbek'\x8bu\x9eZ\x13Y\xae㹫\xb3 \xc7r[S2V\x93pM\xceSm\xb2O\xe7wF\x9e\x95\x9d\xfa\xf2\xe7`^r\xddb:\xd74)\xc34imc\x1e\xe7\xa4\x1cҥ\x99\xa3IT]\x9a%\xdad\x80Nt\x9c\x94\x1bz\x9a\xf795\x94ٌ\xd0x\xb6\xe7\x14ر<Є\x1c\xcf\t\x90\xdd\xec\xcf\xc5n\xc0\xac4\xcdVX\x9a\xbb9~?Z(\xf3ֹ\xf8-d\xf6\xb9dR\xba\xe74\xa7\x04w_\x06MPZ\x82\x9f8\xe6\x88\xc7Ce瞟\xe1\x88G@nv\xac\xac\v+\xaa\xa2sA\x99\xddñ\xb9\xf2\xe7WE\a\u05f7G\x82\xf6\xe5k#\xf21\x90\xfd\x90\x82\x1b\xf6\x04E\x81\xff\x9eP!s\xd7\x01fj\x05h\xa5\xe2\x1b\x81\xfe\xaa#\x7f\x97\xe0\x95[\x16\xa3S\xfdd\x01K\x844}\x01֤)\x99v\x8f\x9dWO\xdf\xfeR\x83>2\xbas+\xf8AQ1kO{\xfa\xc9l0&\f\xca\xc7k1w)e_\x19\xc5gC\xa3\x02\xd8\a\xe9\f\xf3\x10W\x82\x85Z\xa7\r\xa7\xa6\x94-FO1\x10R5\x10\"\xedS\xbc\xef%\xc7\x1f_#\xb8z\x89\xf0*\xc9\x11y\x8d\x10뵂\xac\xa5a֒䍤㋯\x11l-\t\xb7\x16\xf9\x8c\xe9\xc7\x13_\xebX\xe2+\x84]g\a^\x8bH\x97z\xecpq\xf8\x950\xbe\x99c\x86'>Z\x02\xc8\xe8\xf1\xc2\xf1\x10,\x01\xe2ɱ\xc2\xd9 ,e\x1e\fôg\x1f\x12LNdZ\xb4\x9b\x9e\x9a\x84\x94\xb6\xd1=\x7f\xf8/\xf1\xd0_\xe26x\n\xf6\x89\x87\xfb\x96\x1f\xeaK\xa4\xf3\x99\xe1\xd9d\u05c9\x87\xf7\x16\x05hg\x86h\x93\x10\xa7\x0e\xebM\ai\xd3\vp\xc3Czg\xb8\x13\t\x12\x96Pe\xf9A\xbbgo\xc6(\x9d\x83\x9e\xdd\xd7Z\"γ\x82<\x88\xa3\xfa\xfd\x0fvt\u008d\xa8X\xab\xbbg\x16\xe3\xa8j\xee\x1d\xc9؟\x84\xf4\xbb\xf5(\xb8\x1d\x9f\xa4\xb7\xf1\xd6:L\xf1}\x9d\xd6K\xf5\x17\xab\xbb\x1d;\x03\x15״\x8f\xbf=\xba\xa4 \xb3f\x9fx\xb6oz\x88\x80\xa4~\xf7ܰ\x9d\xd2%\xb7\xec\xb2\xd9\n}\xe7:\xc0\xbf/\u05cc\xfd\xa4\x9a\xf4\x91νb\x11\xa8F\x94UqĈ\x89]v\xc1ti^\xd6ɂ\x88\xc5(\xd8\\\xf8\v\v\a\x97\n\xefTQ\xa8\xa73\x97.x%\xfe\x8b\xde1I[\x1b\xfbp\xbb\xa1\xeaA\xaa\xe8\r\x94&{\xae\x91\xb1-L+\xf4v\xe0\xe4zt\xa1\x8ed\xaf6\x7fN@\xa4W\x04\x82\x9f\xe1\xd5x\xa6P\x8b\xddn\x1c\x96k\x12,.\x8fL\xf9{\xe2\x85\xceW\x15\xd7\xd1M=\xe6\xe5\xc1\\\xf50\fv|n\x05kҬ\x9d\xbe\x8a\xd0-=\x9a\x87\a\x12h\xb3\xf7X\xf5\xb7щ\xd2\x1dz>\a\xa7\xe9\x83˳G\x96_\x01\xa7i\x97iET\x8c\xfc\x14M\xc7{\xf1\xd5C\xe3/\x91\xffE\x1d\xe0ct\x15\xb1\xffd\xc0\xa0\xc9H\x02]\x80:umz\x9b5\x17\xbf\xce\xfa\x052\xe2\x02*\xfe\xe2\xeb\x05\xe3\xf3-\xc6^i\xf0\xf7\x7f\a\xd8\x13\xb6\r\xa7\xec\xed7\n7\x1bu\xe9g\xb8\x0f&\xc3R\xe1\xe0\xaa\xd6\b\xc8\xd83\v/E-\xab4\x7f\x80\x9f\x95{\t#\x85Z\xfd\x16\xbd\xc7P\xbc3\x17\xb2\x89\xfd\\\x8b)s?\xb6!\xc0\xf6\x90\xc1\xc9u\xf4\x88홷\xed[[$\f\xee\xfe\xfeg7 +JX\x7f\xac]\x86\t\xea]\x03H\xe90P\xd7h\x1b\xd7N{\xf5D\xf7\xb9w\x9f\xab\xe8<\b\x04t\xa8\x81\xd2F\xcf\x1aM]\x15\x8a\xe7\xa0o\x94܉\x87\x84\x81\xfd\xb9\xd7``\xd93\xfa\xe8\a\x1b\xeccd`\xa1\xe7W\xcc\fA\x97\xad(\xa0\xf8I\x14`\x1c≊\xfe\xf6\xb4e\xa3\xf7\xebr\xeb\x1c\xd4\x1d\xfe\xd8t2aM\xddPiM\xbf\x02\x8d\x8e\xa0[\xfd\xafM\x10\xf0ib\xb0\x86\x8fBZx\x88$\xf8\xcch\xf8C\xef\xe9\x8f0IR\xd4ڷ\xf1\x96\x1do\xb93]\xa7\x92\x05\xd5.\n\x8b\x1b\xa32A\x0e6\xed\x8eЩ\x91\u05fbsz*P\x9a\xa0cm\xe0˓\x04\xfd5\xa8d\xb3\x91\xb1\xb76\xfa\xb3\xe6\xa4a\xf4\x9d\r\xabȭ\x1fT\x1f\v\xe7\xa4'\x90q\xaf\xb4\x84m\x1ea\x9a\xb7yNI73\xaf\xe2Z~\xdc'Y\x8d?\x87\xb3j^\xe8\xb9H\xa0\xac{\x85\xa6\x0fx\xfc\xf1%\xf7\\M\xc6+[\xeb\xa0rjM\x17p#\x10p\xf7S\x9f\xf7\xfcR\xfb\x9c\xdd\f/\xdb\a\xeeڅ\xef\xd9\xe7\xf4F\xf8\xd7<\xa0\x14}Qȅ\x8c\uee7b\x15\xc2?\x8f\x9d\xa3\xf3\x80.,\x9f\x19\xe9-\xd6iN\xfcyBS\xc3p\xd1\xf9]\f\xf5\xf1#\\+\xf6\x19NC\xb6\x15\xfb$q\x10\xa7\x9e\x9c;\xa7\x059-\x97\x8f==79\xc4Cӊ\x0eɍh\x8b\xbe\x9a\x1bT\x1f\xa4\xf0\xd2\xc35M\x15w n\x8c\xad\xff,vn/#\xc31\xfd\xcbI\x8d\xa8\xe2\x9aTZ1\x855:\xa5N>\x1a\xd0\az)&\b\x89\xf7ֺ_\xeam{\xef<\xfb\xeb\xdf.\xfe/\x00\x00\xff\xff\x89~\x02\xaf\x9ft\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4V\xcfo\xeb6\f\xbe\xe7\xaf \xb0û\xcc\xce\xebv\x19r\x1b\xba\x1d\x8am\x0fE\xf3л\"\xd3\tWY\xf2H*]\xf6\xd7\x0f\x92\xec&\xb1\x9d\xb5\x1b0\xdd\"\xf1\xc7Ǐ\xe4\xe7TU\xb52==#\v\x05\xbf\x01\xd3\x13\xfe\xa9\xe8\xd3/\xa9_~\x90\x9a\xc2\xfax\xb7z!\xdfl\xe0>\x8a\x86\xee\t%D\xb6\xf8\x13\xb6\xe4I)\xf8U\x87j\x1a\xa3f\xb3\x020\xde\a5\xe9Z\xd2O\x00\x1b\xbcrp\x0e\xb9ڣ\xaf_\xe2\x0ew\x91\\\x83\x9c\x83\x8f\xa9\x8f\x9f\xeb\xbb\xef\xea\xcf+\x00o:܀ \xa775\x1a\x85\U0004f222R\x1f\xd1!\x87\x9a\xc2Jz\xb4)\xfe\x9eC\xec7p~(\xfeC\xee\x82{\x9bCms\xa8\xa7\x12*\xbf:\x12\xfd\xe5\x96ů4X\xf5.\xb2qˀ\xb2\x81\x1c\x02\xeb\x97s\xd2\nD\xb8\xbc\x90\xdfGgx\xd1y\x05 6\xf4\xb8\x81\xec\xdb\x1b\x8b\xcd\n` $Ǫ\x06.\x8ew%\x9c=`gJ\x12\x80У\xff\xf1\xf1\xe1\xf9\xfb\xed\xd55@\x83b\x99zʹ.T\x06$``@\x01\x1a\xc0X\x8b\"`#3z\x85\x82\x12ȷ\x81\xbb\xdcɷ\xd0\x00f\x17\xa2\x82\x1e\x10\x9e3\xe5Ce\xf5\x9bIϡGV\x1a\xd9\x18\xdc\xceCvq;\xc1\xfa)\x95S\xac\xa0IӅ\x923\r\x94`30\x00\xa1\x05=\x90\x00c\xcf(\xe8u\x8a2\xf3ӂ\xf1\x10v\xbf\xa3\xd5z\xe0AR\xb3\xa2k\xd2P\x1e\x91\x15\x18m\xd8{\xfa\xeb-\xb6$BRRgt\x9c\x93\xf3!\xaf\xc8\xde88\x1a\x17\xf1[0\xbe\x81Μ\x801e\x81\xe8/\xe2e\x13\xa9\xe1\xb7\xc0\x98\xc9\xdc\xc0A\xb5\x97\xcdz\xbd'\x1d\x97ˆ\xae\x8b\x9e\xf4\xb4\xce{B\xbb\xa8\x81e\xdd\xe0\x11\xddZh_\x19\xb6\aR\xb4\x1a\x19צ\xa7*C\xf7y\xc1\xea\xae\xf9\x86\x87u\x94OWX\xf5\x94&K\x94\xc9\xef/\x1e\xf2B\xfcC\a\xd2:\x94\xf9(\xae\xa5\x8a3\xd1\xe9*\xb1\xf3\xf4\xf3\xf6+\x8c\xa9s3\xa6\xecg\xdeώrnA\"\x8c|\x8b\\\x9a\xd8r\xe8rL\xf4M\x1fȗ鲎\xd0O闸\xebHe\x9c\xddԫ\x1a\xee\xb3\xe2\xc0\x0e!\xf6\x8dQljx\xf0po:t\xf7F\xf0\x7fo@bZ\xaaD\xec\xc7Zp)\x96S\xe3\xc2\xda\xc5\xc3(s7\xfa\xb5\xb0\xdd\xdb\x1em\xea`\"1ySK6\xaf\a\xb4\x81\xc1,\xb9\xd4\x1fB\x92=\xfe%\x96AI\n\x9a\x89\xbe\xa4\xfd|\x1fͲ\x9c䗃\x11\x9c^N0=&\x9bi~G-ړuXB\x145\xc1\xf7\xa1\xa4\x83>v\xf3\x9c\x15|\xc1ׅ\xdbG\x0eIY\xb3\xae_\x9f\x1b\xb3\x01\xe5{\xb3'?+wZY\xb1\xca߰K\xa9\xbe\x10\xe8!\x10p\xf4>\xed\xedL!3\x90\xa9\x92\xcflH\xb1[@\xb3\x88\xe7\xc1\xb7!\x7f\xf0MJl\xb4\xec\x13\x0e\xcd\x1e\xf2\x14\\\v\x01o\xf7\xba\x9c\xb9x}\x88\xd0r\xf2\x97\xf4\xbf9'\xb9!\xc6\xc5\xdcUF\xb5\xf8\x902.1\xbe\xbc_\x03\xca\xe8\x9c\xd99܀r\x9c{\x17_\xc3lNө\x19G\xed+u(j\xba\xfe\xbd\x01\x9a9\xa4=y=\xa0\xbf\xb5\r\xf0j\xa6*\x7f\x95\x19v\xa7[\xae\xf7o\xff\x01\xe7+UFw\x03I\xbb+\xa5\x05\xce>D\xcab\xf7\xcaH/\xfe\xf3\x98\x11\xb2\xbd\xb4\x1d5\xe3j5\xc6?\"\xf3\x1anBXl\xf6\xec2\x87o.\xca\x13\rl\xf6c\xc1\x7f\a\x00\x00\xff\xff\xb1J-\xe7\xa6\v\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4VA\x93\xdb6\x0f\xbd\xfbW`&\x87\xbdDr\xf2}\x97\x8e/\x9d̦\x87L\x93f'N\xf7N\x8b\x90\x8d\x9a\"U\x10\xd4\xc6\xfd\xf5\x1d\x90\xd2\xdak\xcb\xc9n\xa7\xd5\xc5c\n\x04\x1f\xde\xc3\x03UU\xd5\xc2\xf4t\x8f\x1c)\xf8\x15\x98\x9e\xf0\x9b\xa0\xd7\x7f\xb1\xde\xff\x14k\n\xcb\xe1\xedbOޮ\xe06E\t\xdd\x17\x8c!q\x83\xef\xb1%OB\xc1/:\x14c\x8d\x98\xd5\x02\xc0x\x1f\xc4\xe8rԿ\x00M\xf0\xc2\xc19\xe4j\x8b\xbeާ\rn\x129\x8b\x9c\x93OG\x0fo\xea\xb7\xff\xab\xdf,\x00\xbc\xe9p\x05Cp\xa9\xc3\xe8M\x1fwA\\hJ\xcez@\x87\x1cj\n\x8b\xd8c\xa3Gl9\xa4~\x05\xc7\x17%\xc5x|\x81~\x9f\xb3\xad\xc7l\x1f\xc7l9\xc0Q\x94_\xbf\x13\xf4\x91\xa2\xe4\xc0\xde%6\xee*\xb2\x1c\x13w\x81\xe5\xb7\xe3\xe9\x15\fѕ7\xe4\xb7\xc9\x19\xbe\xb6\x7f\x01\x10\x9b\xd0\xe3\n\xf2\xf6\xde4h\x17\x00#?9]5Q\xf3\xb6dlvؙr\x0e@\xe8ѿ\xbb\xfbp\xff\xff\xf5\x93e\x00\x8b\xb1a\xea%\xb3<_\"P\x04\x03\x13\x12x\xd8!#\xdcg>!J`\x8c#\xe8Ǥ\x00\x13\xfeX?.\xf6\x1czd\xa1\xa9\xf8\xf2\x9c\xf4\xd7\xc9\xea\x19\xae\x1b\x85^\xa2\xc0jca\x04\xd9\xe1T>ڱZ\b-Ȏ\"0\xf6\x8c\x11\xbd\x1c\x85<>\xa1\x05\xe3!l\xfe\xc0FjX#k\x1a\xd5&9\xab\xfd8 \v06a\xeb\xe9\xaf\xc7\xdc\x11$\xe4C\x9d\x11\x1c5?>\xe4\x05\xd9\x1b\a\x83q\t_\x83\xf1\x16:s\x00F=\x05\x92?ɗCb\r\x9f\x02#\x90o\xc3\nv\"}\\-\x97[\x92\xc9WM\xe8\xba\xe4I\x0e\xcbl\x11\xda$\t\x1c\x97\x16\at\xcbH\xdb\xcap\xb3#\xc1F\x12\xe3\xd2\xf4Te\xe8\xbe\xf8\xa0\xb3\xafxtb\xbcy\x82U\x0e\xdaEQ\x98\xfc\xf6\xe4E6\xc2w\x14P\x0f\x94F([K\x15G\xa2uI\xd9\xf9\xf2\xcb\xfa+LGg1\xce\xd9ϼ\x1f7ƣ\x04J\x18\xf9\x16\xb9\x88\xd8r\xe8rN\xf4\xb6\x0f\xe4%\xffi\x1c\xa1?\xa7?\xa6MG\xa2\xba\xff\x990\x8ajU\xc3m\x1e6\xb0AH\xbd5\x82\xb6\x86\x0f\x1enM\x87\xee\xd6D\xfc\xcf\x05P\xa6c\xa5\xc4>O\x82\xd39y\x1e\\X;5\xd88ޮ\xe85\xef\xe4u\x8f\xcd\x13\x03i\x16jitv\x1b\xf8\x8cW3\xf9|>_\xfd$|\xde\xe0P\x86|K\xdb\xf3U\x00cm\xbe\"\x8c\xbb\xbb\xba\xf7;\x84\xcd\xd4}\x9bO\xd2Fm\x03+\xa2\x81,r5\xd59\"I<\x16L\xe8l\xac/R^\xe1<\x97\xc2hUc\xe3.\x81>E\xf2\x18\x98\xef8C\xbeP~L\x90[\x8f\xbbq\xc6zAo\xf3P\xbf@\x13r\x0fG\xb4\xf0@\xb2+\xe6p\xa7\x97\xd4\xf3T\xd0g\x8f\x87\xb9\xe53\xec_w\xa8\x91e\x9c\"Dl\x18EqDtj^uf\r\xf0)\xc5l/3\x9b\x11tD\x90\x9dv\xef\xf1pI4\xfcH\xdc\xf1\xbe\xff1\xe4\x1b\xbd\x17'\xc0\x8c-2z\x99\xb5\xb8~b\xb0G\xc1\xecr\x1b\x9a\xa8\x06o\xb0\x97\xb8\f\x03\xf2@\xf8\xb0|\b\xbc'\xbf\xad\x94\xf0\xaa4B\\\xe6\xef\x86\xe5\xab\xfcs\xa5䯟\xdf\x7f^\xc1;k!\xc8\x0eYUk\x93\x9b\x1a\xed\xe4\xb6{\x9d'\xeekHd\x7f\xbe\xf9'\xbc\x84\xbe8\xe7\x19ܬs\xf7\x1f\xf4\xe6Π\x94\xa2uQ%0\xe8\xdcT\xb1\xbbQ\xcd2\x1f\xe6\x1aq´\t\xc1\xa1\xb9l=\x9d\xbe\xc4h/!Uz\xc2Kl\x06\xf0\xad:\nUu\xa6\xafJ\xb4\x91\xd0Qs\x16=\xf9\xfc\a\x96\xbc\x1b\xc3t<(\aӶ\xa9m\xcaWL\xfe\xa61[\xbc6\x16f\x14\x99/\xbcz<\xe0Y\x03]\x8c\xa4\xf8\U000917b7\x8d\x91\x9bq\xac7\x89\xb5\xfdǜ3\x9f?\xff\xceX\xefw&\xcex\xf3\x19\xa8\xeft\xe7$\x83\xa3\x16\x9bC\xe3\xb0$\x84\xd0\xce\xf4ދ \xeb\x83>us\x8d\xf8n0\xe4\xcc\xc6\xe1̻߽\xb9\xfa\xf6\xaa\xf8\xb3z^,F\xfdƱ+\x10N%\xf7\xd8e\xe3\xca\xdf\x01\x00\x00\xff\xff\xec\xa0\xe0\xa1k\r\x00\x00"), } diff --git a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml index 9f873afad..324a98181 100644 --- a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml @@ -125,6 +125,14 @@ spec: description: SourcePVC is the name of the PVC which the snapshot is taken for. type: string + uploaderConfig: + description: UploaderConfig specifies the configuration for the uploader. + properties: + parallelFilesUpload: + description: ParallelFilesUpload is the number of files parallel + uploads to perform when using the uploader. + type: integer + type: object required: - backupStorageLocation - operationTimeout diff --git a/config/crd/v2alpha1/crds/crds.go b/config/crd/v2alpha1/crds/crds.go index acbdbed1f..f68d2ce64 100644 --- a/config/crd/v2alpha1/crds/crds.go +++ b/config/crd/v2alpha1/crds/crds.go @@ -30,7 +30,7 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcY_s\xe3\xb8\r\x7fϧ\xc0l\x1f\xf2\xb2Rn\xaf\x9dN\xc7o\xbbN;\x93\xe9m\xea\xb9\xec\xe4\x9d\x12a\x99\x17\x8adI\xc8i\xda\xe9w\uf014d\xfd\xa1\xe3d\xafwz3\t\x82?\xfc\x00\x02 ]\x14ŕp\xea\x11}P\xd6l@8\x85\xff\"4\xfc+\x94O\x7f\t\xa5\xb27\xc7OWO\xca\xc8\rl\xbb@\xb6\xfd\x19\x83\xed|\x8d\xb7\xb8WF\x91\xb2\xe6\xaaE\x12R\x90\xd8\\\x01\bc,\t\x1e\x0e\xfc\x13\xa0\xb6\x86\xbc\xd5\x1a}Ѡ)\x9f\xba\n\xabNi\x89>*\x1f\xb6>\xfeP~\xfa\xb1\xfc\xe1\n\xc0\x88\x167\xc0\xfa\xa4}6\xda\n\x19\xca#j\xf4\xb6T\xf6*8\xacYq\xe3m\xe76p\x9aH\v\xfbM\x13\xe0[A\xe2\xb6\xd7\x11\x87\xb5\n\xf4\xf7\xd5\xd4O*P\x9cv\xba\xf3B/\xf6\x8e3A\x99\xa6\xd3\xc2\xcf\xe7\xae\x00Bm\x1dn\xe0\x9e\xb7v\xa2F\x1e\xebm\x8aP\n\x10RF\x96\x84\xdeye\b\xfd\xd6\xea\xae\x1d\xd8)@b\xa8\xbdr\x14Y\x98\u0082@\x82\xba\x00\xa1\xab\x0f \x02\xdc\xe3\xf3͝\xd9y\xdbx\f\t\x16\xc0/\xc1\x9a\x9d\xa0\xc3\x06\xca$^\xba\x83\b\xd8\xcf&*\x1f\xe2D?D/\x8c7\x90W\xa6\xc9!\xf8\xa6Z\x04\xd9\xf9\xe8B\xb6\xbbF\xa0\x83\nsh\xcf\"0\xa0\xc7(Q%\t\x8e^P\xec;볮sX\x97I\xb6W6\xe8Z\xf8o\xbe\xd1\xff=\xb6j\x8f\"\x1b[C\xaa)\xa3\x84\xb2&\x1f`\x9f\x1b|SpMI4Vℱ\x19&\x15\xc0y[c\b\xaf\x04<+\x98\xa1\xb8?\r\xac\xa8I\x12\xc7\x1f\x85v\a\xf1)%\x99\xfa\x80\xad\xd8\xf4+\xacC\xf3yw\xf7\xf8LJ\xd90\xbc\x920DM\x813\x05\xc3wޒ\xad\xad\x86\n\xe9\x19\xd1$\u05f7\xf6\x88\x9e\xf3\\\xa3L\x185r֖S\x81S\xce\xe6\xf8\x8e\xfax6Mz\x8c\xd1\xc3\x00\xfd\xd4\xfb\xc0{:\xf4\xa4\x86,\xdc\xeb>\x15\x98\xc9\xe8\u008ek65I\x81\xe4ʂɌ>\x97\xa2\xec\xd9I\xceR\x01<:\x8f\x01\r\xcd!\xf4\xdc\xedA\x18\xb0\xd5/XS\t\x0f\xe8Y\r\x84\x83\xed\xb4d\xe3\x8e\xe8\t<ֶ1\xeaߣ\xee\x00d\xe3\xa6Z\x10\xf6%\xe1\xf4\xc5\xdcm\x84\x86\xa3\xd0\x1d~\x8c\x94\xb5\xe2\x05<\xf2.Й\x89\xbe(\x12J\xf8\xca<)\xb3\xb7\x1b8\x10\xb9\xb0\xb9\xb9i\x14\r\x85\xb5\xb6m\xdb\x19E/7\x91oUud}\xb8\x91xD}\x13TS\b_\x1f\x14aM\x9d\xc7\x1b\xe1T\x11\xa1\x9bX\\\xcbV\xfe\xc1\xf7\xa58\\ϰ\xaeb-}\xb1&\xbe\xe2\x01.\x8c\x1c\xe8\xa2_\x9a\xac8\x11\xcdC\xcc\xce\xcf\x7f}\xf8\x06\xc3\xd6\xd1\x19K\xf6#暈\xe1\xe4\x02&L\x99=\xfa\xe4Ľ\xb7mԉF:\xab\f\xc5\x1f\xb5Vh\x96\xf4\x87\xaej\x15\xb1\xdf\xff\xd9a \xf6U\t\xdb\xd8m@\x85\xd09>Ⲅ;\x03[Ѣފ\x80\xbf\xb9\x03\x98\xe9P0\xb1os\xc1\xb4QZ\n'\xd6&\x13C\xa7s\xc6_ӓ\xff\xe0\xb0f\xd71{\xbcL\xedU_\x01\xf8\xf8\x8a\x99l9S\x99?\xb2\xfce\xab\xc0Rh\x81\xe9Kn\xcd\x00\xccLrm_\x8eB\x92\\)\x05\xd0gK\x98Gg\x83\"\xeb_N\x85\xac\\i8\xe3\x00\xfejaj\xd4\x17,\xd9F!PF2\x938\xc6\x1d\xa7\x88\xa4 b\xb2\xa6\xb1|.\xce\x13\x9c\xbe;\xe2U\x1c\xa8\x01\x89m2\xd9\x1a\xa3\f\x9c:<\x98vrK\xcb*k5\x8ae\xde\xe3\xd8\xfa\xcaIzk\xcd^5k\x1b\xa7\xcd\xe89\xc7_\xa0/\x13\x86\x93-\xd9\n\x8e9FR\xc4zQ\f\x01ɉw\xaf\x9a\xce\xe726\x7f{\x85Z\x86s\xbe\\\x9d\x8f\xc1\xe0\xb8\xcb\x05w\x8e(\x87\xe3ї\x97I\xcd#\x1b\xf3H\x88\x8d&Of\x10\xa6\x10,\xe1n?Ѩ\x02|\xf8\x00\xd6Çt\x19\xf9\xf01\x85k\xa74\x15jZx3\x1a\x9f\x95\xd6þ\xef\x8a\xe2\xb1\xfar\x03d;\xba@\xc0?\x16\xe2\v\x1e\x88;\xb3h;Yx\x16\x8a\xc6r\x97\xc1B\x85{N\xb1\x1e\xa9\xf3\x86O\x02z\xcf)'D\x95\xb6\xa3w\x19\x15\x8cp\xe1`\xe9\xee\xf6\x829\x0f\xa3\xe0\x90]\xeen\x87\xdc\xf2\x18\xbd0\xa6\x98^\x12\xc8\xe6\x1c\x8aC;#c1z\x1f\xdaX\x01ǫ\xdf%\xc8s\xe9\x01\xb7\xf5\xaaQ\xdcV\x98q\xe6\x94\xf2\x8e|U\xcc\x05\xa2\n\xd1>\x94й\x04\x9cS\fW\xd7\nA\xaa\xfd\x1e=\x1aJ\xf55m\xbc{\xdc^\x87\xd3&9\x9d\xfb\t\x86\xd8a\xb5\xc29\x94ܱ\xb3g{\xa2\xdeE\x11\t\xdf =F3.\xf0\xf3m\":\x90Õ\x9b\xafW\\\bz\xef&\x8d\xb0{\xdcr\a\x961c\xf7\xb8Fx\xbe\xcaAߊ\x9f\xf1\xe0\n\xe5\xca\x7f=\x9e\u05c8\xbd\x90N\x01\xdc\xf1\r;\xef\x1es\x85t\xa4\x03\xe8 \x88%\xfa\xab\x13T/Y\x9d0\x9c\x8fޝ߇\xb7~\x13\xe0\xed\xab\x88\xb7K\xc8g\xf0V/\xbf\x1a2\x17o\xe5Q\xaeQ\x17\xafx\xae\x00w\xcc\x0e\xd6o/Q\xf9\x9d\x8b|w\xb5\x90Y\xa6\xf8\xc5\xf4)Y.'\xe6\x99f1;=\x92ojC\xe3\xe5\xf6\xad\x8dhz\xb2\xea\xdd^w>\xa6\xa1\xfe!\x8boe\xdfՊ\xd6\xe9\thzۿԾ\xadW\xc4\xfb\x9e\x97\x93z'ƫl|r\x18ޙr\xfd\xdbI_Z\x1a\xd3#\xabC\txD\x03\xdcj\v\xa5Q\x0e:C\t߸\x1b\x8f\x17\x9f\xeb\xe5\x15)\xf2\xdd+\x8ae\x97{\xa6\f\xe8\xf5\xbaὉ\xaf;\x05\xabXI\x98NkQi\xdc\x00\xf9\xee\\\xff\x98=(-\x86 \x9aK\x89\xfak\x92JW\xc5~\t\x88\x8a\x9b\x8aeO{\x1dz߿\xabh\x18+/a\xb8\xb72\x020\xdf\xf1~\xf3.,\xb1\a\xbf\x00f\xc72\xb9\x98\x1f\xa1\xbd~=@ӵ\xb9\xcct\x8fϙ\xd1\xcfu\x8d.\x97-\v\xd8yt\xc2g\xa7V\x0f\xc7\xd3\xc9t\xd9\xc9%\xcea.\xabs|\x99\xcd\xcc\xfd-\x1e\x86w1\xdd\xe3\xbbD\xf6p;:X=\x1c\xe6\xf8xj\xba\xb6Bό\xc7\xe7ف\xfa!Kf\x0e\xa00r沓\x86\xb1'\x8c\xaa\xf8$s\x95J\x17\xb8\xa1K\x96*8-rEv\xb0d־\x9c\x0e\xc8\xea\xfd\xec\xbd\xfd\xca\xf8\x98\x9d/¹\x17\xe9\x9c\x17\xa6oˋ\xf9\xf1\x91\xfa\xb7\xd9\xe1\x95\v\xdd\xfcO\x83K-\xf5L\xf8R\x82\xef\xff\xafȥ\xf7i\xa6^\xe7\xe5\xf96\xbfgJ\xce\x12\xb5\x1a\x8c\xc8\xe5Dw\xff\xac2\x1d\xe9\xaa\xf1\xb1p\x03\xff\xf9\xef\xd5\xff\x02\x00\x00\xff\xffG\x0e\xcf\xec\xfa\x1b\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYOsۺ\x11\xbf\xfbS줇\\\"\xfa\xe5\xb5\xd3\xe9\xe8\x96\xc8팦/\xae'J}\x87\xc8\x15\x89g\x10`\xf1G\xae\xdb\xe9w\xef,@P \t\x89R\xfaR\x1e<\x16\xfe,v\x17\xbb\xbf\xfd\x01X\xadVw\xac\xe3Ϩ\rWr\r\xac\xe3\xf8O\x8b\x92~\x99\xe2\xe5O\xa6\xe0\xea\xfe\xf8\xf1\xee\x85\xcbj\r\x1bg\xacj\xbf\xa2QN\x97\xf8\x80\a.\xb9\xe5J\u07b5hY\xc5,[\xdf\x010)\x95e\xd4l\xe8'@\xa9\xa4\xd5J\bԫ\x1ae\xf1\xe2\xf6\xb8w\\T\xa8\xbd\xf0\xb8\xf4\xf1\xa7\xe2\xe3\xcf\xc5Ow\x00\x92\xb5\xb8\x06\x92\xe7:\xa1Xe\x8a#\nԪ\xe0\xea\xcetX\x92\xd8Z+\u05ed\xe1\xd4\x11\xa6\xf5K\x06u\x1f\x98e\x7f\xf7\x12|\xa3\xe0\xc6\xfeu\xd2\xf1\v7\xd6wv\xc2i&F\xab\xfav\xc3e\xed\x04\xd3i\xcf\x1d\x80)U\x87kx\xa4%;V\"\xb5\xf5\x96x\x15V\xc0\xaa\xca\xfb\x86\x89'ͥE\xbdQµ\xd1'+\xa8Д\x9aw\xd6\xdb~R\b\x8ce\xd6\x190\xael\x80\x19x\xc4\xd7\xfb\xad|Ҫ\xd6h\x82J\x00\xbf\x1a%\x9f\x98m\xd6P\x84\xe1E\xd70\x83}op\xdf\xcew\xf4M\xf6\x8d\xb45VsY\xe7\xd6\xff\xc6[\x84\xcai\xbfmds\x89`\x1bnR\xc5^\x99!\xe5\xb4\xc5\xea\xac\x1a\xbe\x9f\x84\x19\xcb\xdan\xaaO25(T1\x8b9u6\xaa\xed\x04Z\xac`\xfff1\x1aqP\xbaev\r\\\xda?\xfe\xe1\xbc'zW\x15~ꃒc\xb7|\xa6VH\x9a\x83&\xb4C5\xea\xaco\x94e\xe2\x7fQĒ\x80\xcf\xc9\xfc\xa0I\x90\x9b\xb6/\xaaB\xe1\x06\xea\x00\xb6A\xf8\xcc\xca\x17\xd7\xc1\xce*\xcdj\x84_T\x196\xef\xb5A\xddo\xde>\f1\x8dr\xa2\x82}\xb4\x18\xc0X\xa5\xb3\xbb\xd8aY\x84Y\xbd\xdc(v\xb2\x95\xe35\x7f\xe3 +5\xb2l\x90E\x94)\xfc\b\xaed>\xd2>\xd5xU\x94\xa5ޔ\xaa\xc2\xc1u\x98j\xc4\rtZ\x95h̅\xb8\xa7\xe9#\x1d\x1eO\r3\xb7\x84\x11ǟ\x99\xe8\x1a\xf61\xa0L\xd9`\xcb\xd6\xfd\fա\xfc\xf4\xb4}\xfe\xfdn\xd4\fg1\x83\x95\xd6\x10X\x90\xea\x9dVV\x95J\xc0\x1e\xed+\xa2\xf4\xb8\x05\xad:\xa2&\x90\xab\xb94\xc0d5Ȅt\xc0\t\xaa)Ƚ<\xea\r\x9d}8\xa9\x0eu\xba\xed@Kv\xa8-\x8f\xe8\x1b\xbe\xa4\xac$\xad\x13#ޓ\x9da\x14TTO0X\xd1c)V\xbdk\xc2>q\x03\x1a;\x8d\x06\xa5\x1d\xab\xd0;\xee\x00L\x82\xda\xff\x8a\xa5-`\x87\x9a\xc4\xc4\xf8/\x95<\xa2\xb6\xa0\xb1T\xb5\xe4\xff\x1ad\x1b\xb0\xca/*\x98ž\x1c\x9c>\x8fݒ\t82\xe1\xf0\x03\xf9\x0eZ\xf6\x06\x1ai\x15p2\x91燘\x02\xbe(\x8d\xc0\xe5A\xad\xa1\xb1\xb63\xeb\xfb\xfb\x9a\xdbXNKնNr\xfbv\xef\xdd\xcd\xf7\xce*m\xee+<\xa2\xb87\xbc^1]6\xdcbi\x9d\xc6{\xd6\xf1\x95W]\xfa\x92Z\xb4\xd5\xeft_\x80\xcd\xfb\x91\xae\xb3@\v\x9f\xaf\x85\x17v\x80J\"E9\xeb\xa7\x06+N\x8e\xa6&\xf2\xce\xd7?\xef\xbeA\\\xdao\xc6\xd4\xfb\xde柳\xe6\xb4\x05\xe40.\x0f\xa8\xc3&\x1e\xb4j\xbdL\x94U\xa7\xb8\xb4\xfeG)8ʩ\xfb\x8d۷\xdcҾ\xffá\xb1\xb4W\x05l<ǀ=\x82\xeb(\xbb\xab\x02\xb6\x126\xacE\xb1a\x06\x7f\xf8\x06\x90\xa7͊\x1c{\xdd\x16\xa4\xf4h:8x-\xe9\x88\f\xe7\xcc~\x9d\xd2~\xd7aI\x1bG\xbe\xa3I\xfc\xc0\xfb\x1a@\xb9˒\x91\xc5H\\>]\xe9\xcbB\xfft\xd0D\x9fϹ9Q-\x99@l\xacFa\xe4L(\x80\x98\x96\xb0a\x8e\xc6N\x19n\x95~#\xc1\xa1z\x153\tg\x9cO_\xc9d\x89b\xc1\x92\x8d\x1f\x04\\V\xe4G\x1cb\x8e\xe0!\b\xf0:)Y+ʉs\xee\r\xdf\xd6\xd2\x1c\nQ\x83\x96,\x92\x99\xc2\xc2%\x9c\xb8\x1d\xa4\x1cnj\xd5^)\x81l\x8aw\xa5\xe1;\xc9:\xd3(\xbb`\xdb\xf6\x00q䷷\x0ei\xf1\xcdn\xfb\x81\xfe\xc4v\x8a\x8b#\xafz\x00\xa6\xe4!\x963\aY\b@K\x836\xbb-\x98~\xfa\xdc\t\xd2\t\xc1\xf6\x02\xd7`\xb5\x9b\x1bv>\f\xe9\x8bb7\x82\x99쀉\x81\xbbt|.\xfc\xa2@(\xfd\b۰)\xd4\f\x1e\xa7\xfaCd=\x99\xc4\aZ\x02\xaf\xdc6ٙ\x17\xe2\x0fz\xd2\xc5j\xbcڠdx֞\x9e\x84\x05s\xd4\xe1\x821O\xcf\x1bo\xef\x92e\x04\xcb\xdfcY\x10y>\x12g\xb6=\x8f&䬛hy\xce8E\tF \x81\x15\xb8\xeev\xdd)ù\xc6j\xae\xf3j\xb4_\x99\xee\xb1\xd1g\xd2v\x06\xee\xd0\xf3\xad/Ĩ6J\x1ex=_;=:^ʑ\x8b\xa6͊F\xb2$y\x9cj\x04i\xb2\xf2\xe4n\x15\v\bѤ\x03\xaf\x9d>\x97\xfa\a\x8e\xa227g\xfb\x82?\xbc\x12\v\x186\x18\x11\xab]\x0fU\t\x7f\r\x01\xe1\x8c?9RgƀPS\n\x82ēDn\xe0\xdd;P\x1aޅ\x1b\x85w\x1fB\xfdq\\\xd8\x15OItF\xe2+\x17\"\xae{SY\x1a\xa84\x1dd\x94[\x02\xf1\xbfM\x86O\xfc`\xe9|\xe5m\xb7\n^\x19\xb7\x03w\xcd!x\x94e>\xc0\x1e\x0fė4Z\xa7%\x956Ԛ\x18\x84\xf1\"\x95\xcb`\xfb\x05\xa3LRg\x16\f\x9a\x96$o\x05\xfd?\xc5\xec4\xd13Ƹ\xee6\r=\x85\x1d\xeen\x96\x94\x1c\x8f\x8ez*\xcdkN\xe7\x029\xf4\x9cxK\x00\x87\x8c\xa6\xfd\xa9\xdcÕ\xc7ۂ\x98B$q\x04\x80'q\x94\xa1aq\x02p:wlvی\xccaF\xd5\xe7W&;\x17\xbd\xf1\xf4\xbc\xb9\xca\x0f\xa4J\x06\xaf\xa9\xf9\xb5\xe1e3\u07b7\xd9\x19\xc1\xeb\xc2^\xd0s\xd4\x1b\xd4\xcc\x03\xf5*\xcfX'c\xa6Y6\xe9N\xe3u\xda5\xde\xfal\xef\xd3\xf3\xe6*V\xef/\n\xae\xe3\xf5\xe1\x06\xb0\xf7r\xe9\xb4Fi\xe3\xbd \x1dq\xbf\x83ٗ\xe1F-\xbd3Yb\xc3\xf3\x19\xfe謫\x04mXd\xe8\xfe\xde&\xde\xda\xe5\xf8\xf0I\\\x98\xe9\x8f\xf2$\r+\xc0#J\xa0c\vゐۋ4\xc5tN>\x9d\x06)=\x8a\x85+\xdaxh\x8dw\x16\xfd\x95\xc07\nN\x7f&}o.\xc8\xf4 J\xe9\x97q\xc2<\xa2\xe3u \x9dDWY\xa1W\xd5\xc6lr\x0e\\\xe1+\x1a'2\x05\xe2\ar\x85\xb0d8n\x99,W\xb8|H`\x06\x18\xe8 \xa4\x87\x89Kg\xa6\xef'\x10-\x1a\xc3\xea%\x1c\xff\x12F\x85\xab\x8e~\n\xb0=\xd5ѱj\xefM\x9fl7\xc1\xa8TՒ\x06\x8f\xaa\xf2\xcb˛/\x1eoҤc\xb6Y\xd0\xe4\x89\xd9&\x02\xcc\xc1\t\xe1\xe7̪nO\xc2\xf7H\xd9\xf4[\x15_\x7f\xca]R\x8f\xc6\xe4\x00\x10\xaf\t$\x94\xae\xcd\x11\xfaG|ʹ~*K\xeclƲ\x154JDd\xf7\x0f\x13ҵ{\xd4\xe4o\xff\xf41>\x98\xe5\xb2]V\xa3\xedJ\xe6\x0f\x84\xc7K\"\x14\xa6Cg\xb8\x1d\x89|\xb5\xe2\xa6\x13\xec-#8\x1a\x92\xa2M\x92\xb7\xd3[\xe9y<\\\xbeh\x18\x9e\x89\xf2\xe7\xd6\xdc[On\x0f\xd2W\x9bI\xff\xf0\xfc\xf3cV\xb8\x00\x8c1\x93\xb7\x0fW\x12\xf1\xedC\xcc:^\xa1\xb4t\xb68\xbd\x04\x9cX\x9d\xbcx\xb4J\xae\xebn#\xa2\xa3\xc7\xc3%\x8dG\x83\x17\x98I\xffl\x99\xe3%;Jq\x02\x16\x7f'\xbd\x99>,}\x18ީ\x98\xed/\xc6ˆɚ\x12BRq\xf3\xc51'xF5F\xc4b\xac\xfe\xff\x93Sd\xc3e\xd6\xe85\xaf\x12\xd9\xfdmH\xda\xe2\xf6\xc3C\xc4\x1a\xfe\xfd\x9f\xbb\xff\x06\x00\x00\xff\xff?\xb7\xcf\xe0L \x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93۸\x11\xbeϯ\xe8r\x0e\xbe\x8c8\xebM*\x95\xd2\xcd\xd6d\xabTYOT\x96w\xee\x10\xd9\"\xb1\x03\x02\f\x1e\x9aLR\xf9\xef\xa9\x06\b\n$\xa1\x97c\x87\x87\xa9\x11\x80nt7\xd0_?\xb0X,\xeeXǟQ\x1b\xae\xe4\x12X\xc7\xf1\x9f\x16%\xfd2\xc5\xcb_L\xc1\xd5\xc3\xe1\xc3\xdd\v\x97\xd5\x12V\xceX\xd5~A\xa3\x9c.\xf1\x11\xf7\\r˕\xbckѲ\x8aY\xb6\xbc\x03`R*\xcbh\xd8\xd0O\x80RI\xab\x95\x10\xa8\x175\xca\xe2\xc5\xedp縨P{\xe6q\xeb\xc3OŇ\x9f\x8b\x9f\xee\x00$kq\t\xc4\xcfuB\xb1\xca\x14\a\x14\xa8U\xc1՝\xe9\xb0$\xb6\xb5V\xae[\xc2q\"\x90\xf5[\x06q\x1f\x99e\xbfy\x0e~Ppc\xff6\x99\xf8\x95\x1b\xeb';\xe14\x13\xa3]\xfd\xb8\xe1\xb2v\x82\xe9t\xe6\x0e\xc0\x94\xaa\xc3%<і\x1d+\x91\xc6zM\xbc\b\v`U\xe5m\xc3\xc4FsiQ\xaf\x94pm\xb4\xc9\x02*4\xa5\xe6\x9d\xf5\xba\x1f\x05\x02c\x99u\x06\x8c+\x1b`\x06\x9e\xf0\xf5a-7Z\xd5\x1aM\x10\t\xe0w\xa3\xe4\x86\xd9f\tEX^t\r3\xd8\xcf\x06\xf3m\xfdD?d\xdfHZc5\x97un\xff\xaf\xbcE\xa8\x9c\xf6\xc7F:\x97\b\xb6\xe1&\x15\xec\x95\x19\x12N[\xacN\x8a\xe1牙\xb1\xac\xed\xa6\xf2$\xa4A\xa0\x8aỶ\xb3Rm'\xd0b\x05\xbb7\x8bQ\x89\xbd\xd2-\xb3K\xe0\xd2\xfe\xf9O\xa7-ћ\xaa\xf0\xa4\x8fJ\x8e\xcd\xf2\x89F!\x19\x0e\x92\xd0\tը\xb3\xb6Q\x96\x89\xffE\x10K\f>%\xf4A\x92\xc07\x1d\xbf(\n]7P{\xb0\r\xc2'V\xbe\xb8\x0e\xb6ViV#\xfc\xaa\xcapx\xaf\r\xea\xfe\xf0va\x89i\x94\x13\x15\xec\xa2\xc6\x00\xc6*\x9d=\xc5\x0e\xcb\"P\xf5|#\xdb\xc9Q\x8e\xf7\xfcΗ\xac\xd4Ȳ\x97,\xa2L\xe1Wp%\xf37\xedc\x8dWݲԚRU8\x98\x0eS\x89\xb8\x81N\xab\x12\x8d9s\xef\x89|$\xc3\xd3q`f\x96\xb0\xe2\xf03\x13]\xc3>\x04\x94)\x1blٲ\xa7P\x1dʏ\x9b\xf5\xf3\x1f\xb7\xa3a8\x89\x19\xac\xb4\x86\xc0\x82Dﴲ\xaaT\x02vh_\x11\xa5\xc7-h\xd5\x015\x81\\ͥ\x01&\xab\x81'\xa4\v\x8ePM\x97\xdc\xf3\xa3\xd90\xd9_'աN\x8f\x1dh\xcb\x0e\xb5\xe5\x11}×\x84\x95dt\xa2\xc4{\xd23\xac\x82\x8a\xe2\t\x06-z,Ū7M8'n@c\xa7Ѡ\xb4c\x11z\xc3\xed\x81IP\xbb߱\xb4\x05lQ\x13\x9bx\xffK%\x0f\xa8-h,U-\xf9\xbf\x06\xde\x06\xac\xf2\x9b\nf\xb1\x0f\a\xc7\xcfc\xb7d\x02\x0eL8\xbc'\xdbA\xcb\xde@#\xed\x02N&\xfc\xfc\x12S\xc0g\xa5\x11\xb8ܫ%4\xd6vf\xf9\xf0Ps\x1b\xc3i\xa9\xda\xd6In\xdf\x1e\xbc\xb9\xf9\xceY\xa5\xcdC\x85\a\x14\x0f\x86\xd7\v\xa6ˆ[,\xad\xd3\xf8\xc0:\xbe\xf0\xa2K\x1fR\x8b\xb6\xfa\x83\xee\x03\xb0y?\x92uv\xd1\xc2\xe7c\xe1\x99\x13\xa0\x90H\xb7\x9c\xf5\xa4A\x8b\xa3\xa1i\x88\xac\xf3\xe5\xafۯ\x10\xb7\xf6\x871\xb5\xbe\xb7\xfb\x91\xd0\x1c\x8f\x80\f\xc6\xe5\x1eu8ĽV\xad牲\xea\x14\x97\xd6\xff(\x05G95\xbfq\xbb\x96[:\xf7\x7f84\x96Ϊ\x80\x95\xcf1`\x87\xe0:\xf2\uea80\xb5\x84\x15kQ\xac\x98\xc1\x1f~\x00di\xb3 \xc3^w\x04iz4]\x1c\xac\x96L\xc4\f\xe7\xc4y\x1d\xdd~\xdbaI\aG\xb6#\"\xbe\xe7}\f \xdfe\xc9\xcab\xc4.\xef\xae\xf4e\xa1\x7f\xbah\"ϧ\x1cM\x14K&\x10\x1b\xa3QX9c\n \xa6!l\xa0\xd1\xd8)í\xd2o\xc48D\xafb\xc6\xe1\x84\xf1\xe9+\x99,Q\\\xd0d\xe5\x17\x01\x97\x15\xd9\x11\x87;G\xf0\x10\x18x\x99\x94\xac\x15\xf9\xc4)\xf3\x86om\x89\x86\xae\xa8AK\x1a\xc9L`\xe1\x12\x8e\xb9\x1d\xa49\xdcT\xab\x9dR\x02\xd9\x14\xefJ÷\x92u\xa6Q\xf6\x82n\xeb=ĕ_\xdf:\xa4\xcdW\xdb\xf5=\xfd\x89\xe3t/\x0e\xbc\xea\x01\x98\x9c\x87\xb2\x9c9\xc8B\x00ZZ\xb4ڮ\xc1\xf4\xe4s#H'\x04\xdb\t\\\x82\xd5n\xae\xd8\xe9kH_d\xbb\x12\xccd\x17L\x14ܦ\xebs\xd7/2\x84ү\xb0\r\x9bB\xcd`q\x8a?\x94\xac'D|HK\xe0\x95\xdb&Ky\xe6\xfeA\x9ft\xb1\x1a\xafV(Y\x9eէO\u0082:j\x7fF\x99\xcd\xf3\xca\xeb{I3\x82\xe5o\xd1,\xb0<}\x13g\xba=\x8f\br\xdaM\xa4<\xa5\x9c\"\a#\x90\xc0\n\\w\xbb\xec\xe4\xe1\\c5\x97y1:\xaf\xcc\xf4X\xe9\x13n;\x03w\xe8\xf3\xadϔQ\xad\x94\xdc\xf3z\xbewZ:\x9e\U000d1cea͂F\xb2%Y\x9cb\x04I\xb2\xf0\xc9\xdd\"\x06\x10J\x93\xf6\xbcv\xfa\x94\xeb\xef9\x8a\xca\xdc\xec\xed\x17\xecᅸ\x80a\x83\x121\xda\xf5P\x95\xe4\xaf\xe1B8\xe3+G\x9a\xcc(\x10bJA\x90x\xe4\xc8\r\xbc{\aJû\xd0Qxw\x1f\xe2\x8f\xe3\xc2.x\x9aDg8\xber!\xe2\xbe7\x85\xa5!\x95\xa6BF\xb9K \xfe\xf7\xc9\xf2\x89\x1d,\xd5W^w\xab\xe0\x95q;\xe4\xae9\x04\x8f\xbc\xcc=\xecpO\xf9\x92F봤ІZS\x06a\x85\x1dz7\x97\x84\x1c\xaf\x8er*\xcdkNu\x81\x1cf\x8eyK\x00\x87\x8c\xa4}U\xee\xe1\xca\xe3mA\x99BL\xe2\b\x00\x8f\xec\xc8C\xc3\xe6\x04\xe0Tw\xac\xb6\xeb\fρ\xa2\xea\xfd+\xe3\x9d\x17\xad\xb1y^]e\a\x12%\x83\xd74\xfc\xda\xf0\xb2\x19\x9f۬F\xf0\xb2\xb0\x17\xf49\xeaMb\x86\xd6\xdbi\xd0\x1c\xc9\xfa\xdbh\xf1\xc4OF\b7Թ\x91\xff\\\xa8\xf3\x89J\xc74\x13\x02\xc5/\\\xa0\t\xfb^\x11\x017s\xaa\xc1\xac\xaeݡ&\xc3\xeeir\xd8\xe0D\f웕\xe4\"\x1djJ\xdaB\xb6\xe9L,\xdcNk\x06٦\xd3|6\x03\xdb\xf9ȹȗ\x10\x935S؛L\xa7\x002\x9d\x1a\xfbbvv\U000fceaa\xcc\xf2\x9d\x9b\xeb\n\xadВ\xedϧtZ\xa3\xb4\xb1Q\xab\xf6\xdfTj\x95\xa1ř6\xb1.\x95's\n\xdf\xcb\xd0U\x02\xff,\x96L\xbe\x91\x16ۨ\xb9\x93?\xb2\v\x94\xbe\xb7Bܰ\x02<\xa0\x04\xf2\r\xc6\x05\x85R\xcf\xd2\x14S\x9a<\xbe\r\\\xfa\xb0\x12\xae_\xbc\x8c\xb1\x89\xd4\xf7h\xbe\x12Z\xf8&\xc1{s\x86\xa7\x8fj\xe4\xa9\x19#\xcc!&\xf6g+fq\x91ezU\xb2\x92\x85\xa1!y\xfb\x82ƉL\xc4\xfe\x81\xc9[\xd82Կ&\x9b\xbc\x9d\xafژ\x01\x06:0\xe9q\xfb\\\x11\xfb\xed\x19]\x8bư\xfaR`\xfd\x1cV\x85\xdeSO\x02lG\x89\xcdX\xb4\xf7\xa6w\xb6\x9b\x02\x86T\xd5%\t\x9eT巗7w\x82o\x92\xa4c\xb6\xb9 Ɇ\xd9&\x02\xcc\xde\t\xe1ifiP_\x15퐼\xe9{eC\xbe\xedpI\xcd\x1c\xd3ly\xb6\xd6M\xfa\xa7\xb7U\x06\xa3\xd7\xdcK\x12\x8f\x16_\xc8L\xfaw\xe4\\^\xb2%\x17'`\xf1\x8f\x04\xab\xe9K\xdf\xfd\xf0p\xc8l\xffRQ6L\xd6\xe4\x10\x92\x82\x9b\x0f\x8e9ƳTc\x94X\x8c\xc5\xff\x7f\xe6\x14\xd9\xeb2\x1b\xf4\x92W\t\xef\xbe=\x95\x8e\xb8\xdd\xf02\xb4\x84\x7f\xff\xe7\xee\xbf\x01\x00\x00\xff\xff\xf8I\x957\xdd!\x00\x00"), } var CRDs = crds() diff --git a/design/Implemented/restore-with-EnableAPIGroupVersions-feature.md b/design/Implemented/restore-with-EnableAPIGroupVersions-feature.md index c5f9e1c15..e5580f948 100644 --- a/design/Implemented/restore-with-EnableAPIGroupVersions-feature.md +++ b/design/Implemented/restore-with-EnableAPIGroupVersions-feature.md @@ -29,7 +29,7 @@ During restore, the proposal is that Velero will determine if the `APIGroupVersi The proposed code starts with creating three lists for each backed up resource. The three lists will be created by (1) reading the directory names in the backup tarball file and seeing which API group versions were backed up from the source cluster, (2) looking at the target cluster and determining which API group versions are supported, and - (3) getting config maps from the target cluster in order to get user-defined prioritization of versions. + (3) getting ConfigMaps from the target cluster in order to get user-defined prioritization of versions. The three lists will be used to create a map of chosen versions for each resource to restore. If there is a user-defined list of priority versions, the versions will be checked against the supported versions lists. The highest user-defined priority version that is/was supported by both target and source clusters will be the chosen version for that resource. If no user specified versions are supported by neither target nor source, the versions will be logged and the restore will continue with other prioritizations. diff --git a/design/pv_backup_info.md b/design/pv_backup_info.md index 771f8255c..107305fe5 100644 --- a/design/pv_backup_info.md +++ b/design/pv_backup_info.md @@ -61,7 +61,7 @@ type VolumeInfo struct { // CSISnapshotInfo is used for displaying the CSI snapshot status type CSISnapshotInfo struct { SnapshotHandle string // It's the storage provider's snapshot ID for CSI. - Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. + Size int64 // The snapshot corresponding volume size. Driver string // The name of the CSI driver. VSCName string // The name of the VolumeSnapshotContent. @@ -70,7 +70,7 @@ type CSISnapshotInfo struct { // SnapshotDataMovementInfo is used for displaying the snapshot data mover status. type SnapshotDataMovementInfo struct { DataMover string // The data mover used by the backup. The valid values are `velero` and ``(equals to `velero`). - UploaderType string // The type of the uploader that uploads the snapshot data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover. + UploaderType string // The type of the uploader that uploads the snapshot data. The valid values are `kopia` and `restic`. RetainedSnapshot string // The name or ID of the snapshot associated object(SAO). SAO is used to support local snapshots for the snapshot data mover, e.g. it could be a VolumeSnapshot for CSI snapshot data moign/pv_backup_info. SnapshotHandle string // It's the filesystem repository's snapshot ID. @@ -79,7 +79,6 @@ type SnapshotDataMovementInfo struct { // VeleroNativeSnapshotInfo is used for displaying the Velero native snapshot status. type VeleroNativeSnapshotInfo struct { SnapshotHandle string // It's the storage provider's snapshot ID for the Velero-native snapshot. - Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. VolumeType string // The cloud provider snapshot volume type. VolumeAZ string // The cloud provider snapshot volume's availability zones. @@ -89,11 +88,12 @@ type VeleroNativeSnapshotInfo struct { // PodVolumeBackupInfo is used for displaying the PodVolumeBackup snapshot status. type PodVolumeBackupInfo struct { SnapshotHandle string // It's the file-system uploader's snapshot ID for PodVolumeBackup. - Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. + Size int64 // The snapshot corresponding volume size. - UploaderType string // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover. + UploaderType string // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. VolumeName string // The PVC's corresponding volume name used by Pod: https://github.com/kubernetes/kubernetes/blob/e4b74dd12fa8cb63c174091d5536a10b8ec19d34/pkg/apis/core/types.go#L48 - PodName string // The Pod name mounting this PVC. The format should be /. + PodName string // The Pod name mounting this PVC. + PodNamespace string // The Pod namespace. NodeName string // The PVB-taken k8s node's name. } diff --git a/design/volume-snapshot-data-movement/volume-snapshot-data-movement.md b/design/volume-snapshot-data-movement/volume-snapshot-data-movement.md index 105f7d096..d04a6d5b3 100644 --- a/design/volume-snapshot-data-movement/volume-snapshot-data-movement.md +++ b/design/volume-snapshot-data-movement/volume-snapshot-data-movement.md @@ -691,7 +691,8 @@ type Provider interface { tags map[string]string, forceFull bool, parentSnapshot string, - volMode uploader.PersistentVolumeMode, + volMode uploader.PersistentVolumeMode, + uploaderCfg shared.UploaderConfig, updater uploader.ProgressUpdater) (string, bool, error) RunRestore( diff --git a/internal/resourcemodifiers/json_patch.go b/internal/resourcemodifiers/json_patch.go index b5af7c362..d137d40be 100644 --- a/internal/resourcemodifiers/json_patch.go +++ b/internal/resourcemodifiers/json_patch.go @@ -19,30 +19,37 @@ type JSONPatch struct { } func (p *JSONPatch) ToString() string { - if addQuotes(p.Value) { + if addQuotes(&p.Value) { return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": "%s"}`, p.Operation, p.From, p.Path, p.Value) } return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": %s}`, p.Operation, p.From, p.Path, p.Value) } -func addQuotes(value string) bool { - if value == "" { +func addQuotes(value *string) bool { + if *value == "" { + return true + } + // if value is escaped, remove escape and add quotes + // this is useful for scenarios where boolean, null and numbers are required to be set as string. + if strings.HasPrefix(*value, "\"") && strings.HasSuffix(*value, "\"") { + *value = strings.TrimPrefix(*value, "\"") + *value = strings.TrimSuffix(*value, "\"") return true } // if value is null, then don't add quotes - if value == "null" { + if *value == "null" { return false } // if value is a boolean, then don't add quotes - if _, err := strconv.ParseBool(value); err == nil { + if strings.ToLower(*value) == "true" || strings.ToLower(*value) == "false" { return false } // if value is a json object or array, then don't add quotes. - if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") { + if strings.HasPrefix(*value, "{") || strings.HasPrefix(*value, "[") { return false } // if value is a number, then don't add quotes - if _, err := strconv.ParseFloat(value, 64); err == nil { + if _, err := strconv.ParseFloat(*value, 64); err == nil { return false } return true diff --git a/internal/resourcemodifiers/resource_modifiers_test.go b/internal/resourcemodifiers/resource_modifiers_test.go index b09b473d9..648d827be 100644 --- a/internal/resourcemodifiers/resource_modifiers_test.go +++ b/internal/resourcemodifiers/resource_modifiers_test.go @@ -256,6 +256,64 @@ func TestGetResourceModifiersFromConfig(t *testing.T) { }, }, } + cm9 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/value/bool\"\n value: \"\\\"true\\\"\"\n\n\n", + }, + } + + rules9 := &ResourceModifiers{ + Version: "v1", + ResourceModifierRules: []ResourceModifierRule{ + { + Conditions: Conditions{ + GroupResource: "deployments.apps", + ResourceNameRegex: "^test-.*$", + Namespaces: []string{"bar", "foo"}, + }, + Patches: []JSONPatch{ + { + Operation: "replace", + Path: "/value/bool", + Value: `"true"`, + }, + }, + }, + }, + } + cm10 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: deployments.apps\n resourceNameRegex: \"^test-.*$\"\n namespaces:\n - bar\n - foo\n patches:\n - operation: replace\n path: \"/value/bool\"\n value: \"true\"\n\n\n", + }, + } + + rules10 := &ResourceModifiers{ + Version: "v1", + ResourceModifierRules: []ResourceModifierRule{ + { + Conditions: Conditions{ + GroupResource: "deployments.apps", + ResourceNameRegex: "^test-.*$", + Namespaces: []string{"bar", "foo"}, + }, + Patches: []JSONPatch{ + { + Operation: "replace", + Path: "/value/bool", + Value: "true", + }, + }, + }, + }, + } type args struct { cm *v1.ConfigMap @@ -338,6 +396,22 @@ func TestGetResourceModifiersFromConfig(t *testing.T) { want: rules8, wantErr: false, }, + { + name: "bool value as string", + args: args{ + cm: cm9, + }, + want: rules9, + wantErr: false, + }, + { + name: "bool value as bool", + args: args{ + cm: cm10, + }, + want: rules10, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -480,7 +554,24 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) { }, }, } - + cmTrue := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": map[string]interface{}{ + "test": "true", + }, + }, + } + cmFalse := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": map[string]interface{}{ + "test": "false", + }, + }, + } type fields struct { Version string ResourceModifierRules []ResourceModifierRule @@ -496,6 +587,33 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) { wantErr bool wantObj *unstructured.Unstructured }{ + { + name: "configmap true false string", + fields: fields{ + Version: "v1", + ResourceModifierRules: []ResourceModifierRule{ + { + Conditions: Conditions{ + GroupResource: "configmaps", + ResourceNameRegex: ".*", + }, + Patches: []JSONPatch{ + { + Operation: "replace", + Path: "/data/test", + Value: `"false"`, + }, + }, + }, + }, + }, + args: args{ + obj: cmTrue.DeepCopy(), + groupResource: "configmaps", + }, + wantErr: false, + wantObj: cmFalse.DeepCopy(), + }, { name: "Invalid Regex throws error", fields: fields{ diff --git a/pkg/apis/velero/shared/uploader_config.go b/pkg/apis/velero/shared/uploader_config.go new file mode 100644 index 000000000..347b0cc1e --- /dev/null +++ b/pkg/apis/velero/shared/uploader_config.go @@ -0,0 +1,23 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package shared + +// UploaderConfig defines the configuration for the uploader. +type UploaderConfig struct { + // ParallelFilesUpload is the number of files parallel uploads to perform when using the uploader. + ParallelFilesUpload int `json:"parallelFilesUpload,omitempty"` +} diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index ed1518010..628eda393 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -19,6 +19,8 @@ package v1 import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" ) type Metadata struct { @@ -175,6 +177,11 @@ type BackupSpec struct { // If DataMover is "" or "velero", the built-in data mover will be used. // +optional DataMover string `json:"datamover,omitempty"` + + // UploaderConfig specifies the configuration for the uploader. + // +optional + // +nullable + UploaderConfig shared.UploaderConfig `json:"uploaderConfig,omitempty"` } // BackupHooks contains custom behaviors that should be executed at different phases of the backup. diff --git a/pkg/apis/velero/v1/pod_volume_backup_types.go b/pkg/apis/velero/v1/pod_volume_backup_types.go index 859617807..8b0d478ce 100644 --- a/pkg/apis/velero/v1/pod_volume_backup_types.go +++ b/pkg/apis/velero/v1/pod_volume_backup_types.go @@ -51,6 +51,9 @@ type PodVolumeBackupSpec struct { // volume backup as tags. // +optional Tags map[string]string `json:"tags,omitempty"` + + // UploaderConfig specifies the configuration for the uploader. + UploaderConfig shared.UploaderConfig `json:"uploaderConfig,omitempty"` } // PodVolumeBackupPhase represents the lifecycle phase of a PodVolumeBackup. diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index 5aa1b18ab..69d727806 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -381,6 +381,7 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { *out = new(bool) **out = **in } + out.UploaderConfig = in.UploaderConfig } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec. @@ -951,6 +952,7 @@ func (in *PodVolumeBackupSpec) DeepCopyInto(out *PodVolumeBackupSpec) { (*out)[key] = val } } + out.UploaderConfig = in.UploaderConfig } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodVolumeBackupSpec. diff --git a/pkg/apis/velero/v2alpha1/data_upload_types.go b/pkg/apis/velero/v2alpha1/data_upload_types.go index 98f441064..4fb822ff4 100644 --- a/pkg/apis/velero/v2alpha1/data_upload_types.go +++ b/pkg/apis/velero/v2alpha1/data_upload_types.go @@ -60,6 +60,9 @@ type DataUploadSpec struct { // OperationTimeout specifies the time used to wait internal operations, // before returning error as timeout. OperationTimeout metav1.Duration `json:"operationTimeout"` + + // UploaderConfig specifies the configuration for the uploader. + UploaderConfig shared.UploaderConfig `json:"uploaderConfig,omitempty"` } type SnapshotType string diff --git a/pkg/apis/velero/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/velero/v2alpha1/zz_generated.deepcopy.go index 9a9afaa6d..7b85568a6 100644 --- a/pkg/apis/velero/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v2alpha1/zz_generated.deepcopy.go @@ -236,6 +236,7 @@ func (in *DataUploadSpec) DeepCopyInto(out *DataUploadSpec) { } } out.OperationTimeout = in.OperationTimeout + out.UploaderConfig = in.UploaderConfig } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUploadSpec. diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index e9c15dbc4..6760d627b 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -71,6 +71,7 @@ func TestBackedUpItemsMatchesTarballContents(t *testing.T) { req := &Request{ Backup: defaultBackup().Result(), SkippedPVTracker: NewSkipPVTracker(), + PVMap: map[string]PvcPvInfo{}, } backupFile := bytes.NewBuffer([]byte{}) @@ -84,8 +85,8 @@ func TestBackedUpItemsMatchesTarballContents(t *testing.T) { builder.ForDeployment("zoo", "raz").Result(), ), test.PVs( - builder.ForPersistentVolume("bar").Result(), - builder.ForPersistentVolume("baz").Result(), + builder.ForPersistentVolume("bar").ClaimRef("foo", "pvc1").Result(), + builder.ForPersistentVolume("baz").ClaimRef("bar", "pvc2").Result(), ), } for _, resource := range apiResources { diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 61f6834d6..ae8074521 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -250,6 +250,10 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti namespace = metadata.GetNamespace() if groupResource == kuberesource.PersistentVolumes { + if err := ib.addVolumeInfo(obj, log); err != nil { + backupErrs = append(backupErrs, err) + } + if err := ib.takePVSnapshot(obj, log); err != nil { backupErrs = append(backupErrs, err) } @@ -685,6 +689,39 @@ func (ib *itemBackupper) unTrackSkippedPV(obj runtime.Unstructured, groupResourc } } +func (ib *itemBackupper) addVolumeInfo(obj runtime.Unstructured, log logrus.FieldLogger) error { + pv := new(corev1api.PersistentVolume) + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pv) + if err != nil { + log.WithError(err).Warnf("Fail to convert PV") + return err + } + + if ib.backupRequest.PVMap == nil { + ib.backupRequest.PVMap = make(map[string]PvcPvInfo) + } + + pvcName := "" + pvcNamespace := "" + if pv.Spec.ClaimRef != nil { + pvcName = pv.Spec.ClaimRef.Name + pvcNamespace = pv.Spec.ClaimRef.Namespace + + ib.backupRequest.PVMap[pvcNamespace+"/"+pvcName] = PvcPvInfo{ + PVCName: pvcName, + PVCNamespace: pvcNamespace, + PV: *pv, + } + } + + ib.backupRequest.PVMap[pv.Name] = PvcPvInfo{ + PVCName: pvcName, + PVCNamespace: pvcNamespace, + PV: *pv, + } + return nil +} + // convert the input object to PV/PVC and get the PV name func getPVName(obj runtime.Unstructured, groupResource schema.GroupResource) (string, error) { if groupResource == kuberesource.PersistentVolumes { diff --git a/pkg/backup/item_backupper_test.go b/pkg/backup/item_backupper_test.go index 7bd7548bc..481092a0c 100644 --- a/pkg/backup/item_backupper_test.go +++ b/pkg/backup/item_backupper_test.go @@ -19,10 +19,12 @@ package backup import ( "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/volume" "github.com/stretchr/testify/assert" corev1api "k8s.io/api/core/v1" @@ -237,3 +239,55 @@ func TestRandom(t *testing.T) { err2 := runtime.DefaultUnstructuredConverter.FromUnstructured(o, pvc) t.Logf("err1: %v, err2: %v", err1, err2) } + +func TestAddVolumeInfo(t *testing.T) { + tests := []struct { + name string + pv *corev1api.PersistentVolume + expectedVolumeInfo map[string]PvcPvInfo + }{ + { + name: "PV has ClaimRef", + pv: builder.ForPersistentVolume("testPV").ClaimRef("testNS", "testPVC").Result(), + expectedVolumeInfo: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV").ClaimRef("testNS", "testPVC").Result(), + }, + "testNS/testPVC": { + PVCName: "testPVC", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV").ClaimRef("testNS", "testPVC").Result(), + }, + }, + }, + { + name: "PV has no ClaimRef", + pv: builder.ForPersistentVolume("testPV").Result(), + expectedVolumeInfo: map[string]PvcPvInfo{ + "testPV": { + PVCName: "", + PVCNamespace: "", + PV: *builder.ForPersistentVolume("testPV").Result(), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ib := itemBackupper{} + ib.backupRequest = new(Request) + ib.backupRequest.VolumeInfos.VolumeInfos = make([]volume.VolumeInfo, 0) + + pvObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pv) + require.NoError(t, err) + logger := logrus.StandardLogger() + + err = ib.addVolumeInfo(&unstructured.Unstructured{Object: pvObj}, logger) + require.NoError(t, err) + require.Equal(t, tc.expectedVolumeInfo, ib.backupRequest.PVMap) + }) + } +} diff --git a/pkg/backup/pv_skip_tracker.go b/pkg/backup/pv_skip_tracker.go index 859456a37..64241a240 100644 --- a/pkg/backup/pv_skip_tracker.go +++ b/pkg/backup/pv_skip_tracker.go @@ -10,6 +10,14 @@ type SkippedPV struct { Reasons []PVSkipReason `json:"reasons"` } +func (s *SkippedPV) SerializeSkipReasons() string { + ret := "" + for _, reason := range s.Reasons { + ret = ret + reason.Approach + ": " + reason.Reason + ";" + } + return ret +} + type PVSkipReason struct { Approach string `json:"approach"` Reason string `json:"reason"` diff --git a/pkg/backup/pv_skip_tracker_test.go b/pkg/backup/pv_skip_tracker_test.go index 9fdcb034f..16de8f555 100644 --- a/pkg/backup/pv_skip_tracker_test.go +++ b/pkg/backup/pv_skip_tracker_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSummary(t *testing.T) { @@ -41,3 +42,14 @@ func TestSummary(t *testing.T) { } assert.Equal(t, expected, tracker.Summary()) } + +func TestSerializeSkipReasons(t *testing.T) { + tracker := NewSkipPVTracker() + //tracker.Track("pv5", "", "skipped due to policy") + tracker.Track("pv3", podVolumeApproach, "it's set to opt-out") + tracker.Track("pv3", csiSnapshotApproach, "not applicable for CSI ") + + for _, skippedPV := range tracker.Summary() { + require.Equal(t, "csiSnapshot: not applicable for CSI ;podvolume: it's set to opt-out;", skippedPV.SerializeSkipReasons()) + } +} diff --git a/pkg/backup/request.go b/pkg/backup/request.go index 44bc5578f..6735c23a2 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -20,6 +20,8 @@ import ( "fmt" "sort" + corev1api "k8s.io/api/core/v1" + "github.com/vmware-tanzu/velero/internal/hook" "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -52,6 +54,16 @@ type Request struct { itemOperationsList *[]*itemoperation.BackupOperation ResPolicies *resourcepolicies.Policies SkippedPVTracker *skipPVTracker + // A map contains the backup-included PV detail content. + // The key is PV name or PVC name(The format is PVC-namespace/PVC-name) + PVMap map[string]PvcPvInfo + VolumeInfos volume.VolumeInfos +} + +type PvcPvInfo struct { + PVCName string + PVCNamespace string + PV corev1api.PersistentVolume } // GetItemOperationsList returns ItemOperationsList, initializing it if necessary diff --git a/pkg/backup/snapshots.go b/pkg/backup/snapshots.go index a5c659705..e9724b9e3 100644 --- a/pkg/backup/snapshots.go +++ b/pkg/backup/snapshots.go @@ -4,7 +4,6 @@ import ( "context" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/sets" kbclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -17,27 +16,25 @@ import ( // Common function to update the status of CSI snapshots // returns VolumeSnapshot, VolumeSnapshotContent, VolumeSnapshotClasses referenced -func UpdateBackupCSISnapshotsStatus(client kbclient.Client, volumeSnapshotLister snapshotv1listers.VolumeSnapshotLister, backup *velerov1api.Backup, backupLog logrus.FieldLogger) (volumeSnapshots []snapshotv1api.VolumeSnapshot, volumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, volumeSnapshotClasses []snapshotv1api.VolumeSnapshotClass) { +func UpdateBackupCSISnapshotsStatus(client kbclient.Client, globalCRClient kbclient.Client, backup *velerov1api.Backup, backupLog logrus.FieldLogger) (volumeSnapshots []snapshotv1api.VolumeSnapshot, volumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, volumeSnapshotClasses []snapshotv1api.VolumeSnapshotClass) { if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) { backupLog.Info("backup SnapshotMoveData is set to true, skip VolumeSnapshot resource persistence.") } else if features.IsEnabled(velerov1api.CSIFeatureFlag) { selector := label.NewSelectorForBackup(backup.Name) vscList := &snapshotv1api.VolumeSnapshotContentList{} - if volumeSnapshotLister != nil { - tmpVSs, err := volumeSnapshotLister.List(label.NewSelectorForBackup(backup.Name)) - if err != nil { - backupLog.Error(err) - } - for _, vs := range tmpVSs { - volumeSnapshots = append(volumeSnapshots, *vs) - } - } - - err := client.List(context.Background(), vscList, &kbclient.ListOptions{LabelSelector: selector}) + vsList := new(snapshotv1api.VolumeSnapshotList) + err := globalCRClient.List(context.TODO(), vsList, &kbclient.ListOptions{ + LabelSelector: label.NewSelectorForBackup(backup.Name), + }) if err != nil { backupLog.Error(err) } + volumeSnapshots = append(volumeSnapshots, vsList.Items...) + + if err := client.List(context.Background(), vscList, &kbclient.ListOptions{LabelSelector: selector}); err != nil { + backupLog.Error(err) + } if len(vscList.Items) >= 0 { volumeSnapshotContents = vscList.Items } diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index b689bbcae..038e75350 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -300,6 +300,12 @@ func (b *BackupBuilder) DataMover(name string) *BackupBuilder { return b } +// ParallelFilesUpload sets the Backup's uploader parallel uploads +func (b *BackupBuilder) ParallelFilesUpload(parallel int) *BackupBuilder { + b.object.Spec.UploaderConfig.ParallelFilesUpload = parallel + return b +} + // WithStatus sets the Backup's status. func (b *BackupBuilder) WithStatus(status velerov1api.BackupStatus) *BackupBuilder { b.object.Status = status diff --git a/pkg/builder/volume_snapshot_builder.go b/pkg/builder/volume_snapshot_builder.go index bbaedd16e..0abc48d2a 100644 --- a/pkg/builder/volume_snapshot_builder.go +++ b/pkg/builder/volume_snapshot_builder.go @@ -18,6 +18,7 @@ package builder import ( snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -68,7 +69,21 @@ func (v *VolumeSnapshotBuilder) BoundVolumeSnapshotContentName(vscName string) * return v } +// SourcePVC set the built VolumeSnapshot's spec.Source.PersistentVolumeClaimName. func (v *VolumeSnapshotBuilder) SourcePVC(name string) *VolumeSnapshotBuilder { v.object.Spec.Source.PersistentVolumeClaimName = &name return v } + +// RestoreSize set the built VolumeSnapshot's status.RestoreSize. +func (v *VolumeSnapshotBuilder) RestoreSize(size string) *VolumeSnapshotBuilder { + resourceSize := resource.MustParse(size) + v.object.Status.RestoreSize = &resourceSize + return v +} + +// VolumeSnapshotClass set the built VolumeSnapshot's spec.VolumeSnapshotClassName value. +func (v *VolumeSnapshotBuilder) VolumeSnapshotClass(name string) *VolumeSnapshotBuilder { + v.object.Spec.VolumeSnapshotClassName = &name + return v +} diff --git a/pkg/builder/volume_snapshot_content_builder.go b/pkg/builder/volume_snapshot_content_builder.go index 734eeedf3..bbfbe5477 100644 --- a/pkg/builder/volume_snapshot_content_builder.go +++ b/pkg/builder/volume_snapshot_content_builder.go @@ -59,6 +59,7 @@ func (v *VolumeSnapshotContentBuilder) DeletionPolicy(policy snapshotv1api.Delet return v } +// VolumeSnapshotRef sets the built VolumeSnapshotContent's spec.VolumeSnapshotRef value. func (v *VolumeSnapshotContentBuilder) VolumeSnapshotRef(namespace, name string) *VolumeSnapshotContentBuilder { v.object.Spec.VolumeSnapshotRef = v1.ObjectReference{ APIVersion: "snapshot.storage.k8s.io/v1", @@ -68,3 +69,18 @@ func (v *VolumeSnapshotContentBuilder) VolumeSnapshotRef(namespace, name string) } return v } + +// VolumeSnapshotClassName sets the built VolumeSnapshotContent's spec.VolumeSnapshotClassName value. +func (v *VolumeSnapshotContentBuilder) VolumeSnapshotClassName(name string) *VolumeSnapshotContentBuilder { + v.object.Spec.VolumeSnapshotClassName = &name + return v +} + +// ObjectMeta applies functional options to the VolumeSnapshotContent's ObjectMeta. +func (v *VolumeSnapshotContentBuilder) ObjectMeta(opts ...ObjectMetaOpt) *VolumeSnapshotContentBuilder { + for _, opt := range opts { + opt(v.object) + } + + return v +} diff --git a/pkg/client/factory.go b/pkg/client/factory.go index 9ff2040c6..9fcb097fb 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -24,6 +24,7 @@ import ( k8scheme "k8s.io/client-go/kubernetes/scheme" kbclient "sigs.k8s.io/controller-runtime/pkg/client" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" @@ -158,6 +159,9 @@ func (f *factory) KubebuilderClient() (kbclient.Client, error) { if err := apiextv1.AddToScheme(scheme); err != nil { return nil, err } + if err := snapshotv1api.AddToScheme(scheme); err != nil { + return nil, err + } kubebuilderClient, err := kbclient.New(clientConfig, kbclient.Options{ Scheme: scheme, }) diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index 4925179dc..896f060c5 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -106,6 +106,7 @@ type CreateOptions struct { ItemOperationTimeout time.Duration ResPoliciesConfigmap string client kbclient.WithWatch + ParallelFilesUpload int } func NewCreateOptions() *CreateOptions { @@ -151,6 +152,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { flags.StringVar(&o.ResPoliciesConfigmap, "resource-policies-configmap", "", "Reference to the resource policies configmap that backup using") flags.StringVar(&o.DataMover, "data-mover", "", "Specify the data mover to be used by the backup. If the parameter is not set or set as 'velero', the built-in data mover will be used") + flags.IntVar(&o.ParallelFilesUpload, "parallel-files-upload", 0, "Number of files uploads simultaneously when running a backup. This is only applicable for the kopia uploader") } // BindWait binds the wait flag separately so it is not called by other create @@ -396,6 +398,9 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro if o.ResPoliciesConfigmap != "" { backupBuilder.ResourcePolicies(o.ResPoliciesConfigmap) } + if o.ParallelFilesUpload > 0 { + backupBuilder.ParallelFilesUpload(o.ParallelFilesUpload) + } } backup := backupBuilder.ObjectMeta(builder.WithLabelsMap(o.Labels.Data())).Result() diff --git a/pkg/cmd/cli/backup/create_test.go b/pkg/cmd/cli/backup/create_test.go index 4b88998d7..e115bbd19 100644 --- a/pkg/cmd/cli/backup/create_test.go +++ b/pkg/cmd/cli/backup/create_test.go @@ -185,7 +185,7 @@ func TestCreateCommand(t *testing.T) { defaultVolumesToFsBackup := "true" resPoliciesConfigmap := "cm-name-2" dataMover := "velero" - + parallelFilesUpload := 10 flags := new(flag.FlagSet) o := NewCreateOptions() o.BindFlags(flags) @@ -213,6 +213,7 @@ func TestCreateCommand(t *testing.T) { flags.Parse([]string{"--default-volumes-to-fs-backup", defaultVolumesToFsBackup}) flags.Parse([]string{"--resource-policies-configmap", resPoliciesConfigmap}) flags.Parse([]string{"--data-mover", dataMover}) + flags.Parse([]string{"--parallel-files-upload", fmt.Sprintf("%d", parallelFilesUpload)}) //flags.Parse([]string{"--wait"}) client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch) @@ -261,6 +262,7 @@ func TestCreateCommand(t *testing.T) { require.Equal(t, defaultVolumesToFsBackup, o.DefaultVolumesToFsBackup.String()) require.Equal(t, resPoliciesConfigmap, o.ResPoliciesConfigmap) require.Equal(t, dataMover, o.DataMover) + require.Equal(t, parallelFilesUpload, o.ParallelFilesUpload) //assert.Equal(t, true, o.Wait) // verify oldAndNewFilterParametersUsedTogether diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index fb9d96cb3..884246762 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -23,21 +23,16 @@ import ( "net/http" "net/http/pprof" "os" - "reflect" "strings" "time" logrusr "github.com/bombsimon/logrusr/v3" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" - snapshotv1client "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" - snapshotv1informers "github.com/kubernetes-csi/external-snapshotter/client/v4/informers/externalversions" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" "github.com/spf13/cobra" corev1api "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -244,15 +239,17 @@ func NewCommand(f client.Factory) *cobra.Command { } type server struct { - namespace string - metricsAddress string - kubeClientConfig *rest.Config - kubeClient kubernetes.Interface - discoveryClient discovery.DiscoveryInterface - discoveryHelper velerodiscovery.Helper - dynamicClient dynamic.Interface - csiSnapshotClient *snapshotv1client.Clientset - csiSnapshotLister snapshotv1listers.VolumeSnapshotLister + namespace string + metricsAddress string + kubeClientConfig *rest.Config + kubeClient kubernetes.Interface + discoveryClient discovery.DiscoveryInterface + discoveryHelper velerodiscovery.Helper + dynamicClient dynamic.Interface + // controller-runtime client. the difference from the controller-manager's client + // is that the the controller-manager's client is limited to list namespaced-scoped + // resources in the namespace where Velero is installed, or the cluster-scoped + // resources. The crClient doesn't have the limitation. crClient ctrlclient.Client ctx context.Context cancelFunc context.CancelFunc @@ -399,23 +396,6 @@ func newServer(f client.Factory, config serverConfig, logger *logrus.Logger) (*s featureVerifier: featureVerifier, } - // Setup CSI snapshot client and lister - var csiSnapClient *snapshotv1client.Clientset - if features.IsEnabled(velerov1api.CSIFeatureFlag) { - csiSnapClient, err = snapshotv1client.NewForConfig(clientConfig) - if err != nil { - cancelFunc() - return nil, err - } - s.csiSnapshotClient = csiSnapClient - - s.csiSnapshotLister, err = s.getCSIVolumeSnapshotListers() - if err != nil { - cancelFunc() - return nil, err - } - } - return s, nil } @@ -541,7 +521,7 @@ High priorities: - PVs go before PVCs because PVCs depend on them. - PVCs go before pods or controllers so they can be mounted as volumes. - Service accounts go before secrets so service account token secrets can be filled automatically. - - Secrets and config maps go before pods or controllers so they can be mounted + - Secrets and ConfigMaps go before pods or controllers so they can be mounted as volumes. - Limit ranges go before pods or controllers so pods can use them. - Pods go before controllers so they can be explicitly restored and potentially @@ -615,40 +595,6 @@ func (s *server) initRepoManager() error { return nil } -func (s *server) getCSIVolumeSnapshotListers() (vsLister snapshotv1listers.VolumeSnapshotLister, err error) { - _, err = s.discoveryClient.ServerResourcesForGroupVersion(snapshotv1api.SchemeGroupVersion.String()) - switch { - case apierrors.IsNotFound(err): - // CSI is enabled, but the required CRDs aren't installed, so halt. - s.logger.Warnf("The '%s' feature flag was specified, but CSI API group [%s] was not found.", velerov1api.CSIFeatureFlag, snapshotv1api.SchemeGroupVersion.String()) - err = nil - case err == nil: - wrapper := NewCSIInformerFactoryWrapper(s.csiSnapshotClient) - - s.logger.Debug("Creating CSI listers") - // Access the wrapped factory directly here since we've already done the feature flag check above to know it's safe. - vsLister = wrapper.factory.Snapshot().V1().VolumeSnapshots().Lister() - - // start the informers & and wait for the caches to sync - wrapper.Start(s.ctx.Done()) - s.logger.Info("Waiting for informer caches to sync") - csiCacheSyncResults := wrapper.WaitForCacheSync(s.ctx.Done()) - s.logger.Info("Done waiting for informer caches to sync") - - for informer, synced := range csiCacheSyncResults { - if !synced { - err = errors.Errorf("cache was not synced for informer %v", informer) - return - } - s.logger.WithField("informer", informer).Info("Informer cache synced") - } - case err != nil: - s.logger.Errorf("fail to find snapshot v1 schema: %s", err) - } - - return vsLister, err -} - func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string) error { s.logger.Info("Starting controllers") @@ -775,10 +721,10 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.metrics, backupStoreGetter, s.config.formatFlag.Parse(), - s.csiSnapshotLister, s.credentialFileStore, s.config.maxConcurrentK8SConnections, s.config.defaultSnapshotMoveData, + s.crClient, ).SetupWithManager(s.mgr); err != nil { s.logger.Fatal(err, "unable to create controller", "controller", controller.Backup) } @@ -837,7 +783,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string cmd.CheckError(err) r := controller.NewBackupFinalizerReconciler( s.mgr.GetClient(), - s.csiSnapshotLister, + s.crClient, clock.RealClock{}, backupper, newPluginManager, @@ -1027,37 +973,6 @@ func (s *server) runProfiler() { } } -// CSIInformerFactoryWrapper is a proxy around the CSI SharedInformerFactory that checks the CSI feature flag before performing operations. -type CSIInformerFactoryWrapper struct { - factory snapshotv1informers.SharedInformerFactory -} - -func NewCSIInformerFactoryWrapper(c snapshotv1client.Interface) *CSIInformerFactoryWrapper { - // If no namespace is specified, all namespaces are watched. - // This is desirable for VolumeSnapshots, as we want to query for all VolumeSnapshots across all namespaces using this informer - w := &CSIInformerFactoryWrapper{} - - if features.IsEnabled(velerov1api.CSIFeatureFlag) { - w.factory = snapshotv1informers.NewSharedInformerFactoryWithOptions(c, 0) - } - return w -} - -// Start proxies the Start call to the CSI SharedInformerFactory. -func (w *CSIInformerFactoryWrapper) Start(stopCh <-chan struct{}) { - if features.IsEnabled(velerov1api.CSIFeatureFlag) { - w.factory.Start(stopCh) - } -} - -// WaitForCacheSync proxies the WaitForCacheSync call to the CSI SharedInformerFactory. -func (w *CSIInformerFactoryWrapper) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { - if features.IsEnabled(velerov1api.CSIFeatureFlag) { - return w.factory.WaitForCacheSync(stopCh) - } - return nil -} - // if there is a restarting during the reconciling of backups/restores/etc, these CRs may be stuck in progress status // markInProgressCRsFailed tries to mark the in progress CRs as failed when starting the server to avoid the issue func markInProgressCRsFailed(ctx context.Context, cfg *rest.Config, scheme *runtime.Scheme, namespace string, log logrus.FieldLogger) { @@ -1081,13 +996,13 @@ func markInProgressBackupsFailed(ctx context.Context, client ctrlclient.Client, } for i, backup := range backups.Items { - if backup.Status.Phase != velerov1api.BackupPhaseInProgress && backup.Status.Phase != velerov1api.BackupPhaseWaitingForPluginOperations { + if backup.Status.Phase != velerov1api.BackupPhaseInProgress { log.Debugf("the status of backup %q is %q, skip", backup.GetName(), backup.Status.Phase) continue } updated := backup.DeepCopy() updated.Status.Phase = velerov1api.BackupPhaseFailed - updated.Status.FailureReason = fmt.Sprintf("found a backup with status %q during the server starting, mark it as %q", velerov1api.BackupPhaseInProgress, updated.Status.Phase) + updated.Status.FailureReason = fmt.Sprintf("found a backup with status %q during the server starting, mark it as %q", backup.Status.Phase, updated.Status.Phase) updated.Status.CompletionTimestamp = &metav1.Time{Time: time.Now()} if err := client.Patch(ctx, updated, ctrlclient.MergeFrom(&backups.Items[i])); err != nil { log.WithError(errors.WithStack(err)).Errorf("failed to patch backup %q", backup.GetName()) @@ -1105,13 +1020,13 @@ func markInProgressRestoresFailed(ctx context.Context, client ctrlclient.Client, return } for i, restore := range restores.Items { - if restore.Status.Phase != velerov1api.RestorePhaseInProgress && restore.Status.Phase != velerov1api.RestorePhaseWaitingForPluginOperations { + if restore.Status.Phase != velerov1api.RestorePhaseInProgress { log.Debugf("the status of restore %q is %q, skip", restore.GetName(), restore.Status.Phase) continue } updated := restore.DeepCopy() updated.Status.Phase = velerov1api.RestorePhaseFailed - updated.Status.FailureReason = fmt.Sprintf("found a restore with status %q during the server starting, mark it as %q", velerov1api.RestorePhaseInProgress, updated.Status.Phase) + updated.Status.FailureReason = fmt.Sprintf("found a restore with status %q during the server starting, mark it as %q", restore.Status.Phase, updated.Status.Phase) updated.Status.CompletionTimestamp = &metav1.Time{Time: time.Now()} if err := client.Patch(ctx, updated, ctrlclient.MergeFrom(&restores.Items[i])); err != nil { log.WithError(errors.WithStack(err)).Errorf("failed to patch restore %q", restore.GetName()) @@ -1134,7 +1049,9 @@ func markDataUploadsCancel(ctx context.Context, client ctrlclient.Client, backup du := dataUploads.Items[i] if du.Status.Phase == velerov2alpha1api.DataUploadPhaseAccepted || du.Status.Phase == velerov2alpha1api.DataUploadPhasePrepared || - du.Status.Phase == velerov2alpha1api.DataUploadPhaseInProgress { + du.Status.Phase == velerov2alpha1api.DataUploadPhaseInProgress || + du.Status.Phase == velerov2alpha1api.DataUploadPhaseNew || + du.Status.Phase == "" { err := controller.UpdateDataUploadWithRetry(ctx, client, types.NamespacedName{Namespace: du.Namespace, Name: du.Name}, log.WithField("dataupload", du.Name), func(dataUpload *velerov2alpha1api.DataUpload) { dataUpload.Spec.Cancel = true @@ -1162,7 +1079,9 @@ func markDataDownloadsCancel(ctx context.Context, client ctrlclient.Client, rest dd := dataDownloads.Items[i] if dd.Status.Phase == velerov2alpha1api.DataDownloadPhaseAccepted || dd.Status.Phase == velerov2alpha1api.DataDownloadPhasePrepared || - dd.Status.Phase == velerov2alpha1api.DataDownloadPhaseInProgress { + dd.Status.Phase == velerov2alpha1api.DataDownloadPhaseInProgress || + dd.Status.Phase == velerov2alpha1api.DataDownloadPhaseNew || + dd.Status.Phase == "" { err := controller.UpdateDataDownloadWithRetry(ctx, client, types.NamespacedName{Namespace: dd.Namespace, Name: dd.Name}, log.WithField("datadownload", dd.Name), func(dataDownload *velerov2alpha1api.DataDownload) { dataDownload.Spec.Cancel = true diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index e63b79929..77a8305fc 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -86,6 +86,11 @@ func DescribeBackup( DescribeResourcePolicies(d, backup.Spec.ResourcePolicy) } + if backup.Spec.UploaderConfig.ParallelFilesUpload > 0 { + d.Println() + DescribeUploaderConfig(d, backup.Spec) + } + status := backup.Status if len(status.ValidationErrors) > 0 { d.Println() @@ -111,13 +116,19 @@ func DescribeBackup( }) } -// DescribeResourcePolicies describes resource policiesin human-readable format +// DescribeResourcePolicies describes resource policies in human-readable format func DescribeResourcePolicies(d *Describer, resPolicies *v1.TypedLocalObjectReference) { d.Printf("Resource policies:\n") d.Printf("\tType:\t%s\n", resPolicies.Kind) d.Printf("\tName:\t%s\n", resPolicies.Name) } +// DescribeUploaderConfig describes uploader config in human-readable format +func DescribeUploaderConfig(d *Describer, spec velerov1api.BackupSpec) { + d.Printf("Uploader config:\n") + d.Printf("\tParallel files upload:\t%d\n", spec.UploaderConfig.ParallelFilesUpload) +} + // DescribeBackupSpec describes a backup spec in human-readable format. func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) { // TODO make a helper for this and use it in all the describers. diff --git a/pkg/cmd/util/output/backup_describer_test.go b/pkg/cmd/util/output/backup_describer_test.go index 533ceb655..7f547d4f5 100644 --- a/pkg/cmd/util/output/backup_describer_test.go +++ b/pkg/cmd/util/output/backup_describer_test.go @@ -20,6 +20,22 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" ) +func TestDescribeUploaderConfig(t *testing.T) { + input := builder.ForBackup("test-ns", "test-backup-1").ParallelFilesUpload(10).Result().Spec + d := &Describer{ + Prefix: "", + out: &tabwriter.Writer{}, + buf: &bytes.Buffer{}, + } + d.out.Init(d.buf, 0, 8, 2, ' ', 0) + DescribeUploaderConfig(d, input) + d.out.Flush() + expect := `Uploader config: + Parallel files upload: 10 +` + assert.Equal(t, expect, d.buf.String()) +} + func TestDescribeResourcePolicies(t *testing.T) { input := &v1.TypedLocalObjectReference{ Kind: "configmap", diff --git a/pkg/cmd/util/output/schedule_describe_test.go b/pkg/cmd/util/output/schedule_describe_test.go index 7123199ec..ad4cde9ed 100644 --- a/pkg/cmd/util/output/schedule_describe_test.go +++ b/pkg/cmd/util/output/schedule_describe_test.go @@ -59,7 +59,7 @@ Last Backup: input2 := builder.ForSchedule("velero", "schedule-2"). Phase(velerov1api.SchedulePhaseEnabled). CronSchedule("0 0 * * *"). - Template(builder.ForBackup("velero", "backup-1").Result().Spec). + Template(builder.ForBackup("velero", "backup-1").ParallelFilesUpload(10).Result().Spec). LastBackupTime("2023-06-25 15:04:05").Result() expect2 := `Name: schedule-2 Namespace: velero @@ -68,6 +68,9 @@ Annotations: Phase: Enabled +Uploader config: + Parallel files upload: 10 + Paused: false Schedule: 0 0 * * * diff --git a/pkg/cmd/util/output/schedule_describer.go b/pkg/cmd/util/output/schedule_describer.go index 57bd1726d..d02680206 100644 --- a/pkg/cmd/util/output/schedule_describer.go +++ b/pkg/cmd/util/output/schedule_describer.go @@ -48,6 +48,11 @@ func DescribeSchedule(schedule *v1.Schedule) string { DescribeResourcePolicies(d, schedule.Spec.Template.ResourcePolicy) } + if schedule.Spec.Template.UploaderConfig.ParallelFilesUpload > 0 { + d.Println() + DescribeUploaderConfig(d, schedule.Spec.Template) + } + status := schedule.Status if len(status.ValidationErrors) > 0 { d.Println() diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 746c9d789..e8a8c2eec 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -21,12 +21,11 @@ import ( "context" "fmt" "os" + "strconv" "strings" "time" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" - snapshotterClientSet "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" @@ -43,14 +42,18 @@ import ( "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/storage" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/features" + "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/metrics" "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/util/encode" @@ -84,11 +87,10 @@ type backupReconciler struct { metrics *metrics.ServerMetrics backupStoreGetter persistence.ObjectBackupStoreGetter formatFlag logging.Format - volumeSnapshotLister snapshotv1listers.VolumeSnapshotLister - volumeSnapshotClient snapshotterClientSet.Interface credentialFileStore credentials.FileStore maxConcurrentK8SConnections int defaultSnapshotMoveData bool + globalCRClient kbclient.Client } func NewBackupReconciler( @@ -110,10 +112,10 @@ func NewBackupReconciler( metrics *metrics.ServerMetrics, backupStoreGetter persistence.ObjectBackupStoreGetter, formatFlag logging.Format, - volumeSnapshotLister snapshotv1listers.VolumeSnapshotLister, credentialStore credentials.FileStore, maxConcurrentK8SConnections int, defaultSnapshotMoveData bool, + globalCRClient kbclient.Client, ) *backupReconciler { b := &backupReconciler{ ctx: ctx, @@ -135,10 +137,10 @@ func NewBackupReconciler( metrics: metrics, backupStoreGetter: backupStoreGetter, formatFlag: formatFlag, - volumeSnapshotLister: volumeSnapshotLister, credentialFileStore: credentialStore, maxConcurrentK8SConnections: maxConcurrentK8SConnections, defaultSnapshotMoveData: defaultSnapshotMoveData, + globalCRClient: globalCRClient, } b.updateTotalBackupMetric() return b @@ -317,6 +319,7 @@ func (b *backupReconciler) prepareBackupRequest(backup *velerov1api.Backup, logg request := &pkgbackup.Request{ Backup: backup.DeepCopy(), // don't modify items in the cache SkippedPVTracker: pkgbackup.NewSkipPVTracker(), + PVMap: map[string]pkgbackup.PvcPvInfo{}, } // set backup major version - deprecated, use Status.FormatVersion @@ -665,7 +668,7 @@ func (b *backupReconciler) runBackup(backup *pkgbackup.Request) error { backup.Status.VolumeSnapshotsCompleted++ } } - volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses := pkgbackup.UpdateBackupCSISnapshotsStatus(b.kbClient, b.volumeSnapshotLister, backup.Backup, backupLog) + volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses := pkgbackup.UpdateBackupCSISnapshotsStatus(b.kbClient, b.globalCRClient, backup.Backup, backupLog) // Iterate over backup item operations and update progress. // Any errors on operations at this point should be added to backup errors. @@ -734,6 +737,8 @@ func (b *backupReconciler) runBackup(backup *pkgbackup.Request) error { if logFile, err := backupLog.GetPersistFile(); err != nil { fatalErrs = append(fatalErrs, errors.Wrap(err, "error getting backup log file")) } else { + backup.VolumeInfos.VolumeInfos = generateVolumeInfo(backup, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses, b.globalCRClient, backupLog) + if errs := persistBackup(backup, backupFile, logFile, backupStore, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses, results); len(errs) > 0 { fatalErrs = append(fatalErrs, errs...) } @@ -796,7 +801,6 @@ func persistBackup(backup *pkgbackup.Request, ) []error { persistErrs := []error{} backupJSON := new(bytes.Buffer) - volumeInfos := make([]volume.VolumeInfo, 0) if err := encode.To(backup.Backup, "json", backupJSON); err != nil { persistErrs = append(persistErrs, errors.Wrap(err, "error encoding backup")) @@ -843,7 +847,7 @@ func persistBackup(backup *pkgbackup.Request, persistErrs = append(persistErrs, errs...) } - volumeInfoJSON, errs := encode.ToJSONGzip(volumeInfos, "backup volumes information") + volumeInfoJSON, errs := encode.ToJSONGzip(backup.VolumeInfos, "backup volumes information") if errs != nil { persistErrs = append(persistErrs, errs...) } @@ -908,3 +912,328 @@ func oldAndNewFilterParametersUsedTogether(backupSpec velerov1api.BackupSpec) bo return haveOldResourceFilterParameters && haveNewResourceFilterParameters } + +func generateVolumeInfo(backup *pkgbackup.Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, + csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, + crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + volumeInfos := make([]volume.VolumeInfo, 0) + + skippedVolumeInfos := generateVolumeInfoForSkippedPV(backup, logger) + volumeInfos = append(volumeInfos, skippedVolumeInfos...) + + nativeSnapshotVolumeInfos := generateVolumeInfoForVeleroNativeSnapshot(backup, logger) + volumeInfos = append(volumeInfos, nativeSnapshotVolumeInfos...) + + csiVolumeInfos := generateVolumeInfoForCSIVolumeSnapshot(backup, csiVolumeSnapshots, csiVolumeSnapshotContents, csiVolumesnapshotClasses, logger) + volumeInfos = append(volumeInfos, csiVolumeInfos...) + + pvbVolumeInfos := generateVolumeInfoFromPVB(backup, crClient, logger) + volumeInfos = append(volumeInfos, pvbVolumeInfos...) + + dataUploadVolumeInfos := generateVolumeInfoFromDataUpload(backup, crClient, logger) + volumeInfos = append(volumeInfos, dataUploadVolumeInfos...) + + return volumeInfos +} + +// generateVolumeInfoForSkippedPV generate VolumeInfos for SkippedPV. +func generateVolumeInfoForSkippedPV(backup *pkgbackup.Request, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, skippedPV := range backup.SkippedPVTracker.Summary() { + if pvcPVInfo, ok := backup.PVMap[skippedPV.Name]; ok { + volumeInfo := volume.VolumeInfo{ + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: skippedPV.Name, + SnapshotDataMoved: false, + Skipped: true, + SkippedReason: skippedPV.SerializeSkipReasons(), + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("Cannot find info for PV %s", skippedPV.Name) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoForVeleroNativeSnapshot generate VolumeInfos for Velero native snapshot +func generateVolumeInfoForVeleroNativeSnapshot(backup *pkgbackup.Request, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, nativeSnapshot := range backup.VolumeSnapshots { + var iops int64 + if nativeSnapshot.Spec.VolumeIOPS != nil { + iops = *nativeSnapshot.Spec.VolumeIOPS + } + + if pvcPVInfo, ok := backup.PVMap[nativeSnapshot.Spec.PersistentVolumeName]; ok { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.NativeSnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + SnapshotDataMoved: false, + Skipped: false, + NativeSnapshotInfo: volume.NativeSnapshotInfo{ + SnapshotHandle: nativeSnapshot.Status.ProviderSnapshotID, + VolumeType: nativeSnapshot.Spec.VolumeType, + VolumeAZ: nativeSnapshot.Spec.VolumeAZ, + IOPS: strconv.FormatInt(iops, 10), + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("cannot find info for PV %s", nativeSnapshot.Spec.PersistentVolumeName) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoForCSIVolumeSnapshot generate VolumeInfos for CSI VolumeSnapshot +func generateVolumeInfoForCSIVolumeSnapshot(backup *pkgbackup.Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, + csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, + logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, volumeSnapshot := range csiVolumeSnapshots { + var volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass + var volumeSnapshotContent *snapshotv1api.VolumeSnapshotContent + + // This is protective logic. The passed-in VS should be all related + // to this backup. + if volumeSnapshot.Labels[velerov1api.BackupNameLabel] != backup.Name { + continue + } + + if volumeSnapshot.Spec.VolumeSnapshotClassName == nil { + logger.Warnf("Cannot find VolumeSnapshotClass for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + if volumeSnapshot.Status == nil || volumeSnapshot.Status.BoundVolumeSnapshotContentName == nil { + logger.Warnf("Cannot fine VolumeSnapshotContent for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + if volumeSnapshot.Spec.Source.PersistentVolumeClaimName == nil { + logger.Warnf("VolumeSnapshot %s/%s doesn't have a source PVC", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + for index := range csiVolumesnapshotClasses { + if *volumeSnapshot.Spec.VolumeSnapshotClassName == csiVolumesnapshotClasses[index].Name { + volumeSnapshotClass = &csiVolumesnapshotClasses[index] + } + } + + for index := range csiVolumeSnapshotContents { + if *volumeSnapshot.Status.BoundVolumeSnapshotContentName == csiVolumeSnapshotContents[index].Name { + volumeSnapshotContent = &csiVolumeSnapshotContents[index] + } + } + + if volumeSnapshotClass == nil || volumeSnapshotContent == nil { + logger.Warnf("fail to get VolumeSnapshotContent or VolumeSnapshotClass for VolumeSnapshot: %s/%s", + volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + var operation itemoperation.BackupOperation + for _, op := range *backup.GetItemOperationsList() { + if op.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.VolumeSnapshots.String() && + op.Spec.ResourceIdentifier.Name == volumeSnapshot.Name && + op.Spec.ResourceIdentifier.Namespace == volumeSnapshot.Namespace { + operation = *op + } + } + + var size int64 + if volumeSnapshot.Status.RestoreSize != nil { + size = volumeSnapshot.Status.RestoreSize.Value() + } + snapshotHandle := "" + if volumeSnapshotContent.Status.SnapshotHandle != nil { + snapshotHandle = *volumeSnapshotContent.Status.SnapshotHandle + } + if pvcPVInfo, ok := backup.PVMap[volumeSnapshot.Namespace+"/"+*volumeSnapshot.Spec.Source.PersistentVolumeClaimName]; ok { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.CSISnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + Skipped: false, + SnapshotDataMoved: false, + PreserveLocalSnapshot: true, + OperationID: operation.Spec.OperationID, + StartTimestamp: &volumeSnapshot.CreationTimestamp, + CSISnapshotInfo: volume.CSISnapshotInfo{ + VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName, + Size: size, + Driver: volumeSnapshotClass.Driver, + SnapshotHandle: snapshotHandle, + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("cannot find info for PVC %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Spec.Source.PersistentVolumeClaimName) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoFromPVB generate VolumeInfo for PVB. +func generateVolumeInfoFromPVB(backup *pkgbackup.Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, pvb := range backup.PodVolumeBackups { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.PodVolumeBackup, + SnapshotDataMoved: false, + Skipped: false, + StartTimestamp: pvb.Status.StartTimestamp, + PVBInfo: volume.PodVolumeBackupInfo{ + SnapshotHandle: pvb.Status.SnapshotID, + Size: pvb.Status.Progress.TotalBytes, + UploaderType: pvb.Spec.UploaderType, + VolumeName: pvb.Spec.Volume, + PodName: pvb.Spec.Pod.Name, + PodNamespace: pvb.Spec.Pod.Namespace, + NodeName: pvb.Spec.Node, + }, + } + + pod := new(corev1api.Pod) + pvcName := "" + err := crClient.Get(context.TODO(), kbclient.ObjectKey{Namespace: pvb.Spec.Pod.Namespace, Name: pvb.Spec.Pod.Name}, pod) + if err != nil { + logger.WithError(err).Warn("Fail to get pod for PodVolumeBackup: ", pvb.Name) + continue + } + for _, volume := range pod.Spec.Volumes { + if volume.Name == pvb.Spec.Volume && volume.PersistentVolumeClaim != nil { + pvcName = volume.PersistentVolumeClaim.ClaimName + } + } + + if pvcName != "" { + if pvcPVInfo, ok := backup.PVMap[pod.Namespace+"/"+pvcName]; ok { + volumeInfo.PVCName = pvcPVInfo.PVCName + volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace + volumeInfo.PVName = pvcPVInfo.PV.Name + volumeInfo.PVInfo = volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + } + } else { + logger.Warnf("Cannot find info for PVC %s/%s", pod.Namespace, pvcName) + continue + } + } else { + logger.Debug("The PVB %s doesn't have a corresponding PVC", pvb.Name) + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } + + return tmpVolumeInfos +} + +// generateVolumeInfoFromDataUpload generate VolumeInfo for DataUpload. +func generateVolumeInfoFromDataUpload(backup *pkgbackup.Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + vsClassList := new(snapshotv1api.VolumeSnapshotClassList) + if err := crClient.List(context.TODO(), vsClassList); err != nil { + logger.WithError(err).Errorf("cannot list VolumeSnapshotClass %s", err.Error()) + return tmpVolumeInfos + } + + for _, operation := range *backup.GetItemOperationsList() { + if operation.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.PersistentVolumeClaims.String() { + var duIdentifier velero.ResourceIdentifier + + for _, identifier := range operation.Spec.PostOperationItems { + if identifier.GroupResource.String() == "datauploads.velero.io" { + duIdentifier = identifier + } + } + if duIdentifier.Empty() { + logger.Warnf("cannot find DataUpload for PVC %s/%s backup async operation", + operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) + continue + } + + dataUpload := new(velerov2alpha1.DataUpload) + err := crClient.Get( + context.TODO(), + kbclient.ObjectKey{ + Namespace: duIdentifier.Namespace, + Name: duIdentifier.Name}, + dataUpload, + ) + if err != nil { + logger.Warnf("fail to get DataUpload for operation %s: %s", operation.Spec.OperationID, err.Error()) + continue + } + + driverUsedByVSClass := "" + for index := range vsClassList.Items { + if vsClassList.Items[index].Name == dataUpload.Spec.CSISnapshot.SnapshotClass { + driverUsedByVSClass = vsClassList.Items[index].Driver + } + } + + if pvcPVInfo, ok := backup.PVMap[operation.Spec.ResourceIdentifier.Namespace+"/"+operation.Spec.ResourceIdentifier.Name]; ok { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.CSISnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + SnapshotDataMoved: true, + Skipped: false, + OperationID: operation.Spec.OperationID, + StartTimestamp: operation.Status.Created, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: driverUsedByVSClass, + }, + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: dataUpload.Spec.DataMover, + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("Cannot find info for PVC %s/%s", operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) + continue + } + } + } + + return tmpVolumeInfos +} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index df2e22a22..736209e4a 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -29,15 +29,16 @@ import ( "github.com/google/go-cmp/cmp" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" - snapshotfake "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned/fake" - snapshotinformers "github.com/kubernetes-csi/external-snapshotter/client/v4/informers/externalversions" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + corev1api "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/version" "k8s.io/utils/clock" @@ -45,11 +46,14 @@ import ( ctrl "sigs.k8s.io/controller-runtime" kbclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/vmware-tanzu/velero/pkg/backup" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" + "github.com/vmware-tanzu/velero/pkg/volume" fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/discovery" @@ -61,6 +65,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/boolptr" @@ -1062,12 +1067,11 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, { - name: "backup with snapshot data movement set to false when CSI feature is enabled", - backup: defaultBackup().SnapshotMoveData(false).Result(), - //backup: defaultBackup().Result(), + name: "backup with snapshot data movement set to false when CSI feature is enabled", + backup: defaultBackup().SnapshotMoveData(false).Result(), backupLocation: defaultBackupLocation, defaultVolumesToFsBackup: false, expectedResult: &velerov1api.Backup{ @@ -1103,7 +1107,7 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, { name: "backup with snapshot data movement not set when CSI feature is enabled", @@ -1143,7 +1147,7 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, { name: "backup with snapshot data movement set to true and defaultSnapshotMoveData set to false", @@ -1184,7 +1188,7 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, { name: "backup with snapshot data movement set to false and defaultSnapshotMoveData set to true", @@ -1225,7 +1229,7 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, { name: "backup with snapshot data movement not set and defaultSnapshotMoveData set to true", @@ -1266,35 +1270,43 @@ func TestProcessBackupCompletions(t *testing.T) { CSIVolumeSnapshotsCompleted: 0, }, }, - volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + volumeSnapshot: builder.ForVolumeSnapshot("velero", "testVS").VolumeSnapshotClass("testClass").Status().BoundVolumeSnapshotContentName("testVSC").RestoreSize("10G").SourcePVC("testPVC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), }, } + snapshotHandle := "testSnapshotID" + for _, test := range tests { t.Run(test.name, func(t *testing.T) { formatFlag := logging.FormatText var ( - logger = logging.DefaultLogger(logrus.DebugLevel, formatFlag) - pluginManager = new(pluginmocks.Manager) - backupStore = new(persistencemocks.BackupStore) - backupper = new(fakeBackupper) - snapshotClient = snapshotfake.NewSimpleClientset() - sharedInformer = snapshotinformers.NewSharedInformerFactory(snapshotClient, 0) - snapshotLister = sharedInformer.Snapshot().V1().VolumeSnapshots().Lister() + logger = logging.DefaultLogger(logrus.DebugLevel, formatFlag) + pluginManager = new(pluginmocks.Manager) + backupStore = new(persistencemocks.BackupStore) + backupper = new(fakeBackupper) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) ) var fakeClient kbclient.Client // add the test's backup storage location if it's different than the default if test.backupLocation != nil && test.backupLocation != defaultBackupLocation { - fakeClient = velerotest.NewFakeControllerRuntimeClient(t, test.backupLocation) + fakeClient = velerotest.NewFakeControllerRuntimeClient(t, test.backupLocation, + builder.ForVolumeSnapshotClass("testClass").Driver("testDriver").Result(), + builder.ForVolumeSnapshotContent("testVSC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).VolumeSnapshotClassName("testClass").Status(&snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &snapshotHandle, + }).Result(), + ) } else { - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeClient = velerotest.NewFakeControllerRuntimeClient(t, + builder.ForVolumeSnapshotClass("testClass").Driver("testDriver").Result(), + builder.ForVolumeSnapshotContent("testVSC").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).VolumeSnapshotClassName("testClass").Status(&snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &snapshotHandle, + }).Result(), + ) } if test.volumeSnapshot != nil { - snapshotClient.SnapshotV1().VolumeSnapshots(test.volumeSnapshot.Namespace).Create(context.Background(), test.volumeSnapshot, metav1.CreateOptions{}) - sharedInformer.Snapshot().V1().VolumeSnapshots().Informer().GetStore().Add(test.volumeSnapshot) - sharedInformer.WaitForCacheSync(make(chan struct{})) + require.NoError(t, fakeGlobalClient.Create(context.TODO(), test.volumeSnapshot)) } apiServer := velerotest.NewAPIServer(t) @@ -1328,8 +1340,7 @@ func TestProcessBackupCompletions(t *testing.T) { backupStoreGetter: NewFakeSingleObjectBackupStoreGetter(backupStore), backupper: backupper, formatFlag: formatFlag, - volumeSnapshotClient: snapshotClient, - volumeSnapshotLister: snapshotLister, + globalCRClient: fakeGlobalClient, } pluginManager.On("GetBackupItemActionsV2").Return(nil, nil) @@ -1731,3 +1742,749 @@ func TestPatchResourceWorksWithStatus(t *testing.T) { } } +func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { + tests := []struct { + name string + skippedPVName string + pvMap map[string]backup.PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Cannot find info for PV", + skippedPVName: "testPV", + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal Skipped PV info", + skippedPVName: "testPV", + pvMap: map[string]backup.PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + Skipped: true, + SkippedReason: "CSI: skipped for PodVolumeBackup;", + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(backup.Request) + request.SkippedPVTracker = backup.NewSkipPVTracker() + if tc.skippedPVName != "" { + request.SkippedPVTracker.Track(tc.skippedPVName, "CSI", "skipped for PodVolumeBackup") + } + if tc.pvMap != nil { + request.PVMap = tc.pvMap + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForSkippedPV(request, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { + resourceQuantity := resource.MustParse("100Gi") + now := metav1.Now() + tests := []struct { + name string + volumeSnapshot snapshotv1api.VolumeSnapshot + volumeSnapshotContent snapshotv1api.VolumeSnapshotContent + volumeSnapshotClass snapshotv1api.VolumeSnapshotClass + pvMap map[string]backup.PvcPvInfo + operation *itemoperation.BackupOperation + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "VS doesn't have VolumeSnapshotClass name", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{}, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VS doesn't have status", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VS doesn't have PVC", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find VSC for VS", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find VolumeInfo for PVC", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal VolumeSnapshot case", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + CreationTimestamp: now, + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + RestoreSize: &resourceQuantity, + }, + }, + volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), + pvMap: map[string]backup.PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testID", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "snapshot.storage.k8s.io", + Resource: "volumesnapshots", + }, + Namespace: "velero", + Name: "testVS", + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + OperationID: "testID", + StartTimestamp: &now, + PreserveLocalSnapshot: true, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + SnapshotHandle: "testSnapshotHandle", + Size: 107374182400, + VSCName: "testContent", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(backup.Request) + request.Backup = new(velerov1api.Backup) + if tc.pvMap != nil { + request.PVMap = tc.pvMap + } + operationList := request.GetItemOperationsList() + if tc.operation != nil { + *operationList = append(*operationList, tc.operation) + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForCSIVolumeSnapshot(request, []snapshotv1api.VolumeSnapshot{tc.volumeSnapshot}, []snapshotv1api.VolumeSnapshotContent{tc.volumeSnapshotContent}, []snapshotv1api.VolumeSnapshotClass{tc.volumeSnapshotClass}, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } + +} + +func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { + tests := []struct { + name string + nativeSnapshot volume.Snapshot + pvMap map[string]backup.PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Native snapshot's IPOS pointer is nil", + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: nil, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find info for the PV", + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: int64Ptr(100), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal native snapshot", + pvMap: map[string]backup.PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: int64Ptr(100), + VolumeType: "ssd", + VolumeAZ: "us-central1-a", + }, + Status: volume.SnapshotStatus{ + ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.NativeSnapshot, + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + NativeSnapshotInfo: volume.NativeSnapshotInfo{ + SnapshotHandle: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", + VolumeType: "ssd", + VolumeAZ: "us-central1-a", + IOPS: "100", + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(backup.Request) + request.VolumeSnapshots = append(request.VolumeSnapshots, &tc.nativeSnapshot) + if tc.pvMap != nil { + request.PVMap = tc.pvMap + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForVeleroNativeSnapshot(request, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoFromPVB(t *testing.T) { + tests := []struct { + name string + pvb *velerov1api.PodVolumeBackup + pod *corev1api.Pod + pvMap map[string]backup.PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "cannot find PVB's pod, should fail", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "PVB doesn't have a related PVC", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + HostPath: &corev1api.HostPathVolumeSource{}, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "", + PVCNamespace: "", + PVName: "", + BackupMethod: volume.PodVolumeBackup, + PVBInfo: volume.PodVolumeBackupInfo{ + PodName: "testPod", + PodNamespace: "velero", + }, + }, + }, + }, + { + name: "Backup doesn't have information for PVC", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ + ClaimName: "testPVC", + }, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "PVB's volume has a PVC", + pvMap: map[string]backup.PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ + ClaimName: "testPVC", + }, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.PodVolumeBackup, + PVBInfo: volume.PodVolumeBackupInfo{ + PodName: "testPod", + PodNamespace: "velero", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + crClient := velerotest.NewFakeControllerRuntimeClient(t) + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + request := new(pkgbackup.Request) + request.PodVolumeBackups = append(request.PodVolumeBackups, tc.pvb) + if tc.pvMap != nil { + request.PVMap = tc.pvMap + } + if tc.pod != nil { + require.NoError(t, crClient.Create(context.TODO(), tc.pod)) + } + + volumeInfos := generateVolumeInfoFromPVB(request, crClient, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { + now := metav1.Now() + tests := []struct { + name string + volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass + dataUpload *velerov2alpha1.DataUpload + operation *itemoperation.BackupOperation + pvMap map[string]backup.PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Operation is not for PVC", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Operation doesn't have DataUpload PostItemOperation", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "DataUpload cannot be found for operation", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VolumeSnapshotClass cannot be found for operation", + dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ + VolumeSnapshot: "testVS", + }).SnapshotID("testSnapshotHandle").Result(), + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + }, + pvMap: map[string]backup.PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: true, + OperationID: "testOperation", + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + { + name: "Normal DataUpload case", + dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ + VolumeSnapshot: "testVS", + SnapshotClass: "testClass", + }).SnapshotID("testSnapshotHandle").Result(), + volumeSnapshotClass: builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + Status: itemoperation.OperationStatus{ + Created: &now, + }, + }, + pvMap: map[string]backup.PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: true, + OperationID: "testOperation", + StartTimestamp: &now, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + }, + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(backup.Request) + operationList := request.GetItemOperationsList() + if tc.operation != nil { + *operationList = append(*operationList, tc.operation) + } + if tc.pvMap != nil { + request.PVMap = tc.pvMap + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + crClient := velerotest.NewFakeControllerRuntimeClient(t) + if tc.dataUpload != nil { + crClient.Create(context.TODO(), tc.dataUpload) + } + + if tc.volumeSnapshotClass != nil { + crClient.Create(context.TODO(), tc.volumeSnapshotClass) + } + + volumeInfos := generateVolumeInfoFromDataUpload(request, crClient, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func int64Ptr(val int) *int64 { + i := int64(val) + return &i +} + +func stringPtr(str string) *string { + return &str +} diff --git a/pkg/controller/backup_finalizer_controller.go b/pkg/controller/backup_finalizer_controller.go index eb99f6ee5..ea9c0364b 100644 --- a/pkg/controller/backup_finalizer_controller.go +++ b/pkg/controller/backup_finalizer_controller.go @@ -29,8 +29,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" kbclient "sigs.k8s.io/controller-runtime/pkg/client" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/metrics" @@ -42,21 +40,21 @@ import ( // backupFinalizerReconciler reconciles a Backup object type backupFinalizerReconciler struct { - client kbclient.Client - volumeSnapshotLister snapshotv1listers.VolumeSnapshotLister - clock clocks.WithTickerAndDelayedExecution - backupper pkgbackup.Backupper - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager - backupTracker BackupTracker - metrics *metrics.ServerMetrics - backupStoreGetter persistence.ObjectBackupStoreGetter - log logrus.FieldLogger + client kbclient.Client + globalCRClient kbclient.Client + clock clocks.WithTickerAndDelayedExecution + backupper pkgbackup.Backupper + newPluginManager func(logrus.FieldLogger) clientmgmt.Manager + backupTracker BackupTracker + metrics *metrics.ServerMetrics + backupStoreGetter persistence.ObjectBackupStoreGetter + log logrus.FieldLogger } // NewBackupFinalizerReconciler initializes and returns backupFinalizerReconciler struct. func NewBackupFinalizerReconciler( client kbclient.Client, - volumeSnapshotLister snapshotv1listers.VolumeSnapshotLister, + globalCRClient kbclient.Client, clock clocks.WithTickerAndDelayedExecution, backupper pkgbackup.Backupper, newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, @@ -67,6 +65,7 @@ func NewBackupFinalizerReconciler( ) *backupFinalizerReconciler { return &backupFinalizerReconciler{ client: client, + globalCRClient: globalCRClient, clock: clock, backupper: backupper, newPluginManager: newPluginManager, @@ -191,7 +190,7 @@ func (r *backupFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Requ backup.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()} recordBackupMetrics(log, backup, outBackupFile, r.metrics, true) - pkgbackup.UpdateBackupCSISnapshotsStatus(r.client, r.volumeSnapshotLister, backup, log) + pkgbackup.UpdateBackupCSISnapshotsStatus(r.client, r.globalCRClient, backup, log) // update backup metadata in object store backupJSON := new(bytes.Buffer) if err := encode.To(backup, "json", backupJSON); err != nil { diff --git a/pkg/controller/backup_finalizer_controller_test.go b/pkg/controller/backup_finalizer_controller_test.go index f759d0318..74f6da57c 100644 --- a/pkg/controller/backup_finalizer_controller_test.go +++ b/pkg/controller/backup_finalizer_controller_test.go @@ -23,7 +23,6 @@ import ( "testing" "time" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -44,14 +43,13 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" velerotest "github.com/vmware-tanzu/velero/pkg/test" - velerotestmocks "github.com/vmware-tanzu/velero/pkg/test/mocks" ) -func mockBackupFinalizerReconciler(fakeClient kbclient.Client, fakeVolumeSnapshotLister snapshotv1listers.VolumeSnapshotLister, fakeClock *testclocks.FakeClock) (*backupFinalizerReconciler, *fakeBackupper) { +func mockBackupFinalizerReconciler(fakeClient kbclient.Client, fakeGlobalClient kbclient.Client, fakeClock *testclocks.FakeClock) (*backupFinalizerReconciler, *fakeBackupper) { backupper := new(fakeBackupper) return NewBackupFinalizerReconciler( fakeClient, - fakeVolumeSnapshotLister, + fakeGlobalClient, fakeClock, backupper, func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, @@ -164,9 +162,9 @@ func TestBackupFinalizerReconcile(t *testing.T) { fakeClient := velerotest.NewFakeControllerRuntimeClient(t, initObjs...) - fakeVolumeSnapshotLister := velerotestmocks.NewVolumeSnapshotLister(t) + fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t, initObjs...) - reconciler, backupper := mockBackupFinalizerReconciler(fakeClient, fakeVolumeSnapshotLister, fakeClock) + reconciler, backupper := mockBackupFinalizerReconciler(fakeClient, fakeGlobalClient, fakeClock) pluginManager.On("CleanupClients").Return(nil) backupStore.On("GetBackupItemOperations", test.backup.Name).Return(test.backupOperations, nil) backupStore.On("GetBackupContents", mock.Anything).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil) diff --git a/pkg/controller/backup_operations_controller.go b/pkg/controller/backup_operations_controller.go index 5e9a5cfd3..e36691e9c 100644 --- a/pkg/controller/backup_operations_controller.go +++ b/pkg/controller/backup_operations_controller.go @@ -19,8 +19,11 @@ package controller import ( "bytes" "context" + "fmt" "time" + v2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2" + "github.com/pkg/errors" "github.com/sirupsen/logrus" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -293,7 +296,7 @@ func getBackupItemOperationProgress( if err != nil { operation.Status.Phase = itemoperation.OperationPhaseFailed operation.Status.Error = err.Error() - errs = append(errs, err.Error()) + errs = append(errs, wrapErrMsg(err.Error(), bia)) changes = true failedCount++ continue @@ -302,7 +305,7 @@ func getBackupItemOperationProgress( if err != nil { operation.Status.Phase = itemoperation.OperationPhaseFailed operation.Status.Error = err.Error() - errs = append(errs, err.Error()) + errs = append(errs, wrapErrMsg(err.Error(), bia)) changes = true failedCount++ continue @@ -340,7 +343,7 @@ func getBackupItemOperationProgress( if operationProgress.Err != "" { operation.Status.Phase = itemoperation.OperationPhaseFailed operation.Status.Error = operationProgress.Err - errs = append(errs, operationProgress.Err) + errs = append(errs, wrapErrMsg(operationProgress.Err, bia)) changes = true failedCount++ continue @@ -355,7 +358,7 @@ func getBackupItemOperationProgress( _ = bia.Cancel(operation.Spec.OperationID, backup) operation.Status.Phase = itemoperation.OperationPhaseFailed operation.Status.Error = "Asynchronous action timed out" - errs = append(errs, operation.Status.Error) + errs = append(errs, wrapErrMsg(operation.Status.Error, bia)) changes = true failedCount++ continue @@ -375,3 +378,15 @@ func getBackupItemOperationProgress( } return inProgressOperations, changes, completedCount, failedCount, errs } + +// wrap the error message to include the BIA name +func wrapErrMsg(errMsg string, bia v2.BackupItemAction) string { + plugin := "unknown" + if bia != nil { + plugin = bia.Name() + } + if len(errMsg) > 0 { + errMsg += ", " + } + return fmt.Sprintf("%splugin: %s", errMsg, plugin) +} diff --git a/pkg/controller/backup_operations_controller_test.go b/pkg/controller/backup_operations_controller_test.go index 417294a7c..00ffd7f7a 100644 --- a/pkg/controller/backup_operations_controller_test.go +++ b/pkg/controller/backup_operations_controller_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + v2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -286,6 +288,7 @@ func TestBackupOperationsReconcile(t *testing.T) { backupStore.On("PutBackupItemOperations", mock.Anything, mock.Anything).Return(nil) backupStore.On("PutBackupMetadata", mock.Anything, mock.Anything).Return(nil) for _, operation := range test.backupOperations { + bia.On("Name").Return("test") bia.On("Progress", operation.Spec.OperationID, mock.Anything). Return(velero.OperationProgress{ Completed: test.operationComplete, @@ -308,3 +311,40 @@ func TestBackupOperationsReconcile(t *testing.T) { }) } } + +func TestWrapErrMsg(t *testing.T) { + bia2 := &biav2mocks.BackupItemAction{} + bia2.On("Name").Return("test-bia") + cases := []struct { + name string + inputErr string + plugin v2.BackupItemAction + expect string + }{ + { + name: "empty error message", + inputErr: "", + plugin: bia2, + expect: "plugin: test-bia", + }, + { + name: "nil bia", + inputErr: "some error happened", + plugin: nil, + expect: "some error happened, plugin: unknown", + }, + { + name: "regular error and bia", + inputErr: "some error happened", + plugin: bia2, + expect: "some error happened, plugin: test-bia", + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + got := wrapErrMsg(test.inputErr, test.plugin) + assert.Equal(t, test.expect, got) + }) + } +} diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 9465528e3..feb1e9866 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -344,7 +344,7 @@ func (r *DataUploadReconciler) runCancelableDataUpload(ctx context.Context, fsBa tags := map[string]string{ velerov1api.AsyncOperationIDLabel: du.Labels[velerov1api.AsyncOperationIDLabel], } - if err := fsBackup.StartBackup(path, fmt.Sprintf("%s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC), "", false, tags); err != nil { + if err := fsBackup.StartBackup(path, fmt.Sprintf("%s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC), "", false, tags, du.Spec.UploaderConfig); err != nil { return r.errorOut(ctx, du, err, "error starting data path backup", log) } diff --git a/pkg/controller/data_upload_controller_test.go b/pkg/controller/data_upload_controller_test.go index b61cd07b3..fd35cfe7d 100644 --- a/pkg/controller/data_upload_controller_test.go +++ b/pkg/controller/data_upload_controller_test.go @@ -45,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" @@ -283,7 +284,7 @@ func (f *fakeDataUploadFSBR) Init(ctx context.Context, bslName string, sourceNam return nil } -func (f *fakeDataUploadFSBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string) error { +func (f *fakeDataUploadFSBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string, uploaderConfigs shared.UploaderConfig) error { du := f.du original := f.du.DeepCopy() du.Status.Phase = velerov2alpha1api.DataUploadPhaseCompleted diff --git a/pkg/controller/pod_volume_backup_controller.go b/pkg/controller/pod_volume_backup_controller.go index 6a66f98e1..cebe777c1 100644 --- a/pkg/controller/pod_volume_backup_controller.go +++ b/pkg/controller/pod_volume_backup_controller.go @@ -178,7 +178,7 @@ func (r *PodVolumeBackupReconciler) Reconcile(ctx context.Context, req ctrl.Requ } } - if err := fsBackup.StartBackup(path, "", parentSnapshotID, false, pvb.Spec.Tags); err != nil { + if err := fsBackup.StartBackup(path, "", parentSnapshotID, false, pvb.Spec.Tags, pvb.Spec.UploaderConfig); err != nil { return r.errorOut(ctx, &pvb, err, "error starting data path backup", log) } diff --git a/pkg/controller/pod_volume_backup_controller_test.go b/pkg/controller/pod_volume_backup_controller_test.go index 25fcae80a..8e0a5dcf9 100644 --- a/pkg/controller/pod_volume_backup_controller_test.go +++ b/pkg/controller/pod_volume_backup_controller_test.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/datapath" @@ -103,7 +104,7 @@ func (b *fakeFSBR) Init(ctx context.Context, bslName string, sourceNamespace str return nil } -func (b *fakeFSBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string) error { +func (b *fakeFSBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string, uploaderConfigs shared.UploaderConfig) error { pvb := b.pvb original := b.pvb.DeepCopy() diff --git a/pkg/datapath/file_system.go b/pkg/datapath/file_system.go index fba9eac7b..fcbc220c4 100644 --- a/pkg/datapath/file_system.go +++ b/pkg/datapath/file_system.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/repository" repokey "github.com/vmware-tanzu/velero/pkg/repository/keys" @@ -129,14 +130,14 @@ func (fs *fileSystemBR) Close(ctx context.Context) { fs.log.WithField("user", fs.jobName).Info("FileSystemBR is closed") } -func (fs *fileSystemBR) StartBackup(source AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string) error { +func (fs *fileSystemBR) StartBackup(source AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string, uploaderConfigs shared.UploaderConfig) error { if !fs.initialized { return errors.New("file system data path is not initialized") } go func() { snapshotID, emptySnapshot, err := fs.uploaderProv.RunBackup(fs.ctx, source.ByPath, realSource, tags, forceFull, - parentSnapshot, source.VolMode, fs) + parentSnapshot, source.VolMode, uploaderConfigs, fs) if err == provider.ErrorCanceled { fs.callbacks.OnCancelled(context.Background(), fs.namespace, fs.jobName) diff --git a/pkg/datapath/file_system_test.go b/pkg/datapath/file_system_test.go index c8e4b881c..eb3ad7a23 100644 --- a/pkg/datapath/file_system_test.go +++ b/pkg/datapath/file_system_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/uploader/provider" providerMock "github.com/vmware-tanzu/velero/pkg/uploader/provider/mocks" @@ -95,12 +96,12 @@ func TestAsyncBackup(t *testing.T) { t.Run(test.name, func(t *testing.T) { fs := newFileSystemBR("job-1", "test", nil, "velero", Callbacks{}, velerotest.NewLogger()).(*fileSystemBR) mockProvider := providerMock.NewProvider(t) - mockProvider.On("RunBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.result.Backup.SnapshotID, test.result.Backup.EmptySnapshot, test.err) + mockProvider.On("RunBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.result.Backup.SnapshotID, test.result.Backup.EmptySnapshot, test.err) fs.uploaderProv = mockProvider fs.initialized = true fs.callbacks = test.callbacks - err := fs.StartBackup(AccessPoint{ByPath: test.path}, "", "", false, nil) + err := fs.StartBackup(AccessPoint{ByPath: test.path}, "", "", false, nil, shared.UploaderConfig{}) require.Equal(t, nil, err) <-finish diff --git a/pkg/datapath/mocks/types.go b/pkg/datapath/mocks/types.go index ecf655df0..6096523ce 100644 --- a/pkg/datapath/mocks/types.go +++ b/pkg/datapath/mocks/types.go @@ -11,6 +11,8 @@ import ( mock "github.com/stretchr/testify/mock" repository "github.com/vmware-tanzu/velero/pkg/repository" + + shared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" ) // AsyncBR is an autogenerated mock type for the AsyncBR type @@ -42,13 +44,13 @@ func (_m *AsyncBR) Init(ctx context.Context, bslName string, sourceNamespace str return r0 } -// StartBackup provides a mock function with given fields: source, realSource, parentSnapshot, forceFull, tags -func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string) error { - ret := _m.Called(source, realSource, parentSnapshot, forceFull, tags) +// StartBackup provides a mock function with given fields: source, realSource, parentSnapshot, forceFull, tags, uploaderConfig +func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string, uploaderConfig shared.UploaderConfig) error { + ret := _m.Called(source, realSource, parentSnapshot, forceFull, tags, uploaderConfig) var r0 error - if rf, ok := ret.Get(0).(func(datapath.AccessPoint, string, string, bool, map[string]string) error); ok { - r0 = rf(source, realSource, parentSnapshot, forceFull, tags) + if rf, ok := ret.Get(0).(func(datapath.AccessPoint, string, string, bool, map[string]string, shared.UploaderConfig) error); ok { + r0 = rf(source, realSource, parentSnapshot, forceFull, tags, uploaderConfig) } else { r0 = ret.Error(0) } diff --git a/pkg/datapath/types.go b/pkg/datapath/types.go index e26cf9482..9530538d9 100644 --- a/pkg/datapath/types.go +++ b/pkg/datapath/types.go @@ -20,6 +20,7 @@ import ( "context" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/uploader" ) @@ -62,7 +63,7 @@ type AsyncBR interface { Init(ctx context.Context, bslName string, sourceNamespace string, uploaderType string, repositoryType string, repoIdentifier string, repositoryEnsurer *repository.Ensurer, credentialGetter *credentials.CredentialGetter) error // StartBackup starts an asynchronous data path instance for backup - StartBackup(source AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string) error + StartBackup(source AccessPoint, realSource string, parentSnapshot string, forceFull bool, tags map[string]string, uploaderConfig shared.UploaderConfig) error // StartRestore starts an asynchronous data path instance for restore StartRestore(snapshotID string, target AccessPoint) error diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index 6c149f188..ba6a7bcb8 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -768,6 +768,13 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindRestoreResourceList: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-resource-list.json.gz", }, }, + { + name: "", + targetName: "my-backup", + expectedKeyByKind: map[velerov1api.DownloadTargetKind]string{ + velerov1api.DownloadTargetKindBackupVolumeInfos: "backups/my-backup/my-backup-volumeinfos.json.gz", + }, + }, } for _, test := range tests { diff --git a/pkg/podvolume/backupper.go b/pkg/podvolume/backupper.go index 84be78a0f..3239f10f2 100644 --- a/pkg/podvolume/backupper.go +++ b/pkg/podvolume/backupper.go @@ -392,6 +392,7 @@ func newPodVolumeBackup(backup *velerov1api.Backup, pod *corev1api.Pod, volume c BackupStorageLocation: backup.Spec.StorageLocation, RepoIdentifier: repoIdentifier, UploaderType: uploaderType, + UploaderConfig: backup.Spec.UploaderConfig, }, } diff --git a/pkg/uploader/kopia/snapshot.go b/pkg/uploader/kopia/snapshot.go index d87404a36..a34a1e553 100644 --- a/pkg/uploader/kopia/snapshot.go +++ b/pkg/uploader/kopia/snapshot.go @@ -28,10 +28,6 @@ import ( "github.com/sirupsen/logrus" - "github.com/vmware-tanzu/velero/pkg/kopia" - "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" - "github.com/vmware-tanzu/velero/pkg/uploader" - "github.com/kopia/kopia/fs" "github.com/kopia/kopia/fs/localfs" "github.com/kopia/kopia/repo" @@ -41,6 +37,11 @@ import ( "github.com/kopia/kopia/snapshot/restore" "github.com/kopia/kopia/snapshot/snapshotfs" "github.com/pkg/errors" + + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" + "github.com/vmware-tanzu/velero/pkg/kopia" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/uploader" ) // All function mainly used to make testing more convenient @@ -104,9 +105,14 @@ func getDefaultPolicy() *policy.Policy { } } -func setupDefaultPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo) (*policy.Tree, error) { +func setupPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo, uploaderCfg shared.UploaderConfig) (*policy.Tree, error) { // some internal operations from Kopia code retrieves policies from repo directly, so we need to persist the policy to repo - err := setPolicyFunc(ctx, rep, sourceInfo, getDefaultPolicy()) + curPolicy := getDefaultPolicy() + if uploaderCfg.ParallelFilesUpload > 0 { + curPolicy.UploadPolicy.MaxParallelFileReads = newOptionalInt(uploaderCfg.ParallelFilesUpload) + } + + err := setPolicyFunc(ctx, rep, sourceInfo, curPolicy) if err != nil { return nil, errors.Wrap(err, "error to set policy") } @@ -127,7 +133,7 @@ func setupDefaultPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceIn // Backup backup specific sourcePath and update progress func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, - forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { if fsUploader == nil { return nil, false, errors.New("get empty kopia uploader") } @@ -172,7 +178,7 @@ func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.Re } kopiaCtx := kopia.SetupKopiaLog(ctx, log) - snapID, snapshotSize, err := SnapshotSource(kopiaCtx, repoWriter, fsUploader, sourceInfo, sourceEntry, forceFull, parentSnapshot, tags, log, "Kopia Uploader") + snapID, snapshotSize, err := SnapshotSource(kopiaCtx, repoWriter, fsUploader, sourceInfo, sourceEntry, forceFull, parentSnapshot, tags, uploaderCfg, log, "Kopia Uploader") if err != nil { return nil, false, err } @@ -223,6 +229,7 @@ func SnapshotSource( forceFull bool, parentSnapshot string, snapshotTags map[string]string, + uploaderCfg shared.UploaderConfig, log logrus.FieldLogger, description string, ) (string, int64, error) { @@ -258,7 +265,7 @@ func SnapshotSource( log.Infof("Using parent snapshot %s, start time %v, end time %v, description %s", previous[i].ID, previous[i].StartTime.ToTime(), previous[i].EndTime.ToTime(), previous[i].Description) } - policyTree, err := setupDefaultPolicy(ctx, rep, sourceInfo) + policyTree, err := setupPolicy(ctx, rep, sourceInfo, uploaderCfg) if err != nil { return "", 0, errors.Wrapf(err, "unable to set policy for si %v", sourceInfo) } diff --git a/pkg/uploader/kopia/snapshot_test.go b/pkg/uploader/kopia/snapshot_test.go index 34bc530b2..645434942 100644 --- a/pkg/uploader/kopia/snapshot_test.go +++ b/pkg/uploader/kopia/snapshot_test.go @@ -35,6 +35,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" repomocks "github.com/vmware-tanzu/velero/pkg/repository/mocks" "github.com/vmware-tanzu/velero/pkg/uploader" uploadermocks "github.com/vmware-tanzu/velero/pkg/uploader/mocks" @@ -94,9 +95,10 @@ func TestSnapshotSource(t *testing.T) { } testCases := []struct { - name string - args []mockArgs - notError bool + name string + args []mockArgs + uploaderCfg shared.UploaderConfig + notError bool }{ { name: "regular test", @@ -150,6 +152,20 @@ func TestSnapshotSource(t *testing.T) { }, notError: false, }, + { + name: "set policy with ParallelFilesUpload", + args: []mockArgs{ + {methodName: "LoadSnapshot", returns: []interface{}{manifest, nil}}, + {methodName: "SaveSnapshot", returns: []interface{}{manifest.ID, nil}}, + {methodName: "TreeForSource", returns: []interface{}{nil, nil}}, + {methodName: "ApplyRetentionPolicy", returns: []interface{}{nil, nil}}, + {methodName: "SetPolicy", returns: []interface{}{nil}}, + {methodName: "Upload", returns: []interface{}{manifest, nil}}, + {methodName: "Flush", returns: []interface{}{nil}}, + }, + uploaderCfg: shared.UploaderConfig{ParallelFilesUpload: 10}, + notError: true, + }, { name: "failed to upload snapshot", args: []mockArgs{ @@ -182,7 +198,7 @@ func TestSnapshotSource(t *testing.T) { t.Run(tc.name, func(t *testing.T) { s := injectSnapshotFuncs() MockFuncs(s, tc.args) - _, _, err = SnapshotSource(ctx, s.repoWriterMock, s.uploderMock, sourceInfo, rootDir, false, "/", nil, log, "TestSnapshotSource") + _, _, err = SnapshotSource(ctx, s.repoWriterMock, s.uploderMock, sourceInfo, rootDir, false, "/", nil, tc.uploaderCfg, log, "TestSnapshotSource") if tc.notError { assert.NoError(t, err) } else { @@ -630,9 +646,9 @@ func TestBackup(t *testing.T) { var snapshotInfo *uploader.SnapshotInfo var err error if tc.isEmptyUploader { - snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), nil, s.repoWriterMock, tc.sourcePath, "", tc.forceFull, tc.parentSnapshot, tc.volMode, tc.tags, &logrus.Logger{}) + snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), nil, s.repoWriterMock, tc.sourcePath, "", tc.forceFull, tc.parentSnapshot, tc.volMode, shared.UploaderConfig{}, tc.tags, &logrus.Logger{}) } else { - snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), s.uploderMock, s.repoWriterMock, tc.sourcePath, "", tc.forceFull, tc.parentSnapshot, tc.volMode, tc.tags, &logrus.Logger{}) + snapshotInfo, isSnapshotEmpty, err = Backup(context.Background(), s.uploderMock, s.repoWriterMock, tc.sourcePath, "", tc.forceFull, tc.parentSnapshot, tc.volMode, shared.UploaderConfig{}, tc.tags, &logrus.Logger{}) } // Check if the returned error matches the expected error if tc.expectedError != nil { diff --git a/pkg/uploader/provider/kopia.go b/pkg/uploader/provider/kopia.go index 706393362..1ae69d21e 100644 --- a/pkg/uploader/provider/kopia.go +++ b/pkg/uploader/provider/kopia.go @@ -30,6 +30,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/uploader/kopia" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" repokeys "github.com/vmware-tanzu/velero/pkg/repository/keys" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" @@ -119,6 +120,7 @@ func (kp *kopiaProvider) RunBackup( forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, + uploaderCfg shared.UploaderConfig, updater uploader.ProgressUpdater) (string, bool, error) { if updater == nil { return "", false, errors.New("Need to initial backup progress updater first") @@ -158,7 +160,7 @@ func (kp *kopiaProvider) RunBackup( realSource = fmt.Sprintf("%s/%s/%s", kp.requestorType, uploader.KopiaType, realSource) } - snapshotInfo, isSnapshotEmpty, err := BackupFunc(ctx, kpUploader, repoWriter, path, realSource, forceFull, parentSnapshot, volMode, tags, log) + snapshotInfo, isSnapshotEmpty, err := BackupFunc(ctx, kpUploader, repoWriter, path, realSource, forceFull, parentSnapshot, volMode, uploaderCfg, tags, log) if err != nil { if kpUploader.IsCanceled() { log.Error("Kopia backup is canceled") diff --git a/pkg/uploader/provider/kopia_test.go b/pkg/uploader/provider/kopia_test.go index e9c4fb7ef..c1fc95724 100644 --- a/pkg/uploader/provider/kopia_test.go +++ b/pkg/uploader/provider/kopia_test.go @@ -34,6 +34,7 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" "github.com/vmware-tanzu/velero/internal/credentials/mocks" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/repository" udmrepo "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" @@ -68,34 +69,34 @@ func TestRunBackup(t *testing.T) { testCases := []struct { name string - hookBackupFunc func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) + hookBackupFunc func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) volMode uploader.PersistentVolumeMode notError bool }{ { name: "success to backup", - hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return &uploader.SnapshotInfo{}, false, nil }, notError: true, }, { name: "get error to backup", - hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return &uploader.SnapshotInfo{}, false, errors.New("failed to backup") }, notError: false, }, { name: "got empty snapshot", - hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return nil, true, errors.New("snapshot is empty") }, notError: false, }, { name: "success to backup block mode volume", - hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { + hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) { return &uploader.SnapshotInfo{}, false, nil }, volMode: uploader.PersistentVolumeBlock, @@ -108,7 +109,7 @@ func TestRunBackup(t *testing.T) { tc.volMode = uploader.PersistentVolumeFilesystem } BackupFunc = tc.hookBackupFunc - _, _, err := kp.RunBackup(context.Background(), "var", "", nil, false, "", tc.volMode, &updater) + _, _, err := kp.RunBackup(context.Background(), "var", "", nil, false, "", tc.volMode, shared.UploaderConfig{}, &updater) if tc.notError { assert.NoError(t, err) } else { diff --git a/pkg/uploader/provider/mocks/Provider.go b/pkg/uploader/provider/mocks/Provider.go index a15bd940f..54ba8ac95 100644 --- a/pkg/uploader/provider/mocks/Provider.go +++ b/pkg/uploader/provider/mocks/Provider.go @@ -7,6 +7,8 @@ import ( mock "github.com/stretchr/testify/mock" + shared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" + uploader "github.com/vmware-tanzu/velero/pkg/uploader" ) @@ -29,30 +31,30 @@ func (_m *Provider) Close(ctx context.Context) error { return r0 } -// RunBackup provides a mock function with given fields: ctx, path, realSource, tags, forceFull, parentSnapshot, updater -func (_m *Provider) RunBackup(ctx context.Context, path string, realSource string, tags map[string]string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, updater uploader.ProgressUpdater) (string, bool, error) { - ret := _m.Called(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, updater) +// RunBackup provides a mock function with given fields: ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater +func (_m *Provider) RunBackup(ctx context.Context, path string, realSource string, tags map[string]string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, uploaderCfg shared.UploaderConfig, updater uploader.ProgressUpdater) (string, bool, error) { + ret := _m.Called(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater) var r0 string var r1 bool var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, map[string]string, bool, string, uploader.ProgressUpdater) (string, bool, error)); ok { - return rf(ctx, path, realSource, tags, forceFull, parentSnapshot, updater) + if rf, ok := ret.Get(0).(func(context.Context, string, string, map[string]string, bool, string, uploader.PersistentVolumeMode, shared.UploaderConfig, uploader.ProgressUpdater) (string, bool, error)); ok { + return rf(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, map[string]string, bool, string, uploader.ProgressUpdater) string); ok { - r0 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, updater) + if rf, ok := ret.Get(0).(func(context.Context, string, string, map[string]string, bool, string, uploader.PersistentVolumeMode, shared.UploaderConfig, uploader.ProgressUpdater) string); ok { + r0 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater) } else { r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, string, string, map[string]string, bool, string, uploader.ProgressUpdater) bool); ok { - r1 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, updater) + if rf, ok := ret.Get(1).(func(context.Context, string, string, map[string]string, bool, string, uploader.PersistentVolumeMode, shared.UploaderConfig, uploader.ProgressUpdater) bool); ok { + r1 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater) } else { r1 = ret.Get(1).(bool) } - if rf, ok := ret.Get(2).(func(context.Context, string, string, map[string]string, bool, string, uploader.ProgressUpdater) error); ok { - r2 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, updater) + if rf, ok := ret.Get(2).(func(context.Context, string, string, map[string]string, bool, string, uploader.PersistentVolumeMode, shared.UploaderConfig, uploader.ProgressUpdater) error); ok { + r2 = rf(ctx, path, realSource, tags, forceFull, parentSnapshot, volMode, uploaderCfg, updater) } else { r2 = ret.Error(2) } @@ -60,13 +62,13 @@ func (_m *Provider) RunBackup(ctx context.Context, path string, realSource strin return r0, r1, r2 } -// RunRestore provides a mock function with given fields: ctx, snapshotID, volumePath, updater +// RunRestore provides a mock function with given fields: ctx, snapshotID, volumePath, volMode, updater func (_m *Provider) RunRestore(ctx context.Context, snapshotID string, volumePath string, volMode uploader.PersistentVolumeMode, updater uploader.ProgressUpdater) error { ret := _m.Called(ctx, snapshotID, volumePath, volMode, updater) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, uploader.ProgressUpdater) error); ok { - r0 = rf(ctx, snapshotID, volumePath, updater) + if rf, ok := ret.Get(0).(func(context.Context, string, string, uploader.PersistentVolumeMode, uploader.ProgressUpdater) error); ok { + r0 = rf(ctx, snapshotID, volumePath, volMode, updater) } else { r0 = ret.Error(0) } diff --git a/pkg/uploader/provider/provider.go b/pkg/uploader/provider/provider.go index 09ff3f162..5cffc4b32 100644 --- a/pkg/uploader/provider/provider.go +++ b/pkg/uploader/provider/provider.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/uploader" ) @@ -49,6 +50,7 @@ type Provider interface { forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, + uploaderCfg shared.UploaderConfig, updater uploader.ProgressUpdater) (string, bool, error) // RunRestore which will do restore for one specific volume with given snapshot id and return error // updater is used for updating backup progress which implement by third-party diff --git a/pkg/uploader/provider/restic.go b/pkg/uploader/provider/restic.go index 6c6cf5992..9c19782b9 100644 --- a/pkg/uploader/provider/restic.go +++ b/pkg/uploader/provider/restic.go @@ -27,6 +27,7 @@ import ( v1 "k8s.io/api/core/v1" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/uploader" @@ -122,6 +123,7 @@ func (rp *resticProvider) RunBackup( forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, + uploaderCfg shared.UploaderConfig, updater uploader.ProgressUpdater) (string, bool, error) { if updater == nil { return "", false, errors.New("Need to initial backup progress updater first") @@ -144,6 +146,10 @@ func (rp *resticProvider) RunBackup( "parentSnapshot": parentSnapshot, }) + if uploaderCfg.ParallelFilesUpload > 0 { + log.Warnf("ParallelFilesUpload is set to %d, but restic does not support parallel file uploads. Ignoring.", uploaderCfg.ParallelFilesUpload) + } + backupCmd := resticBackupCMDFunc(rp.repoIdentifier, rp.credentialsFile, path, tags) backupCmd.Env = rp.cmdEnv backupCmd.CACertFile = rp.caCertFile diff --git a/pkg/uploader/provider/restic_test.go b/pkg/uploader/provider/restic_test.go index 038657cf9..62f289968 100644 --- a/pkg/uploader/provider/restic_test.go +++ b/pkg/uploader/provider/restic_test.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/restic" @@ -149,9 +150,9 @@ func TestResticRunBackup(t *testing.T) { } if !tc.nilUpdater { updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()} - _, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, &updater) + _, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, shared.UploaderConfig{}, &updater) } else { - _, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, nil) + _, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, shared.UploaderConfig{}, nil) } tc.rp.log.Infof("test name %v error %v", tc.name, err) diff --git a/pkg/util/podvolume/pod_volume.go b/pkg/util/podvolume/pod_volume.go index 542e15297..94e969b3b 100644 --- a/pkg/util/podvolume/pod_volume.go +++ b/pkg/util/podvolume/pod_volume.go @@ -46,7 +46,7 @@ func GetVolumesByPod(pod *corev1api.Pod, defaultVolumesToFsBackup bool) ([]strin if pv.Secret != nil { continue } - // don't backup volumes mounting config maps. Config maps will be backed up separately. + // don't backup volumes mounting ConfigMaps. ConfigMaps will be backed up separately. if pv.ConfigMap != nil { continue } diff --git a/pkg/util/podvolume/pod_volume_test.go b/pkg/util/podvolume/pod_volume_test.go index 67ce9bb57..b4898e6b4 100644 --- a/pkg/util/podvolume/pod_volume_test.go +++ b/pkg/util/podvolume/pod_volume_test.go @@ -222,7 +222,7 @@ func TestGetVolumesByPod(t *testing.T) { }, }, { - name: "should exclude volumes mounting config maps", + name: "should exclude volumes mounting ConfigMaps", defaultVolumesToFsBackup: true, pod: &corev1api.Pod{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/volume/volume_info_common.go b/pkg/volume/volume_info_common.go index cfca31df9..14ede0c6b 100644 --- a/pkg/volume/volume_info_common.go +++ b/pkg/volume/volume_info_common.go @@ -51,6 +51,8 @@ type VolumeInfo struct { SnapshotDataMoved bool `json:"snapshotDataMoved"` // Whether the local snapshot is preserved after snapshot is moved. + // The local snapshot may be a result of CSI snapshot backup(no data movement) + // or a CSI snapshot data movement plus preserve local snapshot. PreserveLocalSnapshot bool `json:"preserveLocalSnapshot"` // Whether the Volume is skipped in this backup. @@ -69,6 +71,7 @@ type VolumeInfo struct { SnapshotDataMovementInfo SnapshotDataMovementInfo `json:"snapshotDataMovementInfo,omitempty"` NativeSnapshotInfo NativeSnapshotInfo `json:"nativeSnapshotInfo,omitempty"` PVBInfo PodVolumeBackupInfo `json:"pvbInfo,omitempty"` + PVInfo PVInfo `json:"pvInfo,omitempty"` } // CSISnapshotInfo is used for displaying the CSI snapshot status @@ -76,7 +79,7 @@ type CSISnapshotInfo struct { // It's the storage provider's snapshot ID for CSI. SnapshotHandle string `json:"snapshotHandle"` - // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. + // The snapshot corresponding volume size. Size int64 `json:"size"` // The name of the CSI driver. @@ -91,7 +94,7 @@ type SnapshotDataMovementInfo struct { // The data mover used by the backup. The valid values are `velero` and ``(equals to `velero`). DataMover string `json:"dataMover"` - // The type of the uploader that uploads the snapshot data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover. + // The type of the uploader that uploads the snapshot data. The valid values are `kopia` and `restic`. UploaderType string `json:"uploaderType"` // The name or ID of the snapshot associated object(SAO). @@ -111,9 +114,6 @@ type NativeSnapshotInfo struct { // It's the storage provider's snapshot ID for the Velero-native snapshot. SnapshotHandle string `json:"snapshotHandle"` - // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. - Size int64 `json:"size"` - // The cloud provider snapshot volume type. VolumeType string `json:"volumeType"` @@ -129,19 +129,32 @@ type PodVolumeBackupInfo struct { // It's the file-system uploader's snapshot ID for PodVolumeBackup. SnapshotHandle string `json:"snapshotHandle"` - // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot. + // The snapshot corresponding volume size. Size int64 `json:"size"` - // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover. + // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. UploaderType string `json:"uploaderType"` // The PVC's corresponding volume name used by Pod // https://github.com/kubernetes/kubernetes/blob/e4b74dd12fa8cb63c174091d5536a10b8ec19d34/pkg/apis/core/types.go#L48 VolumeName string `json:"volumeName"` - // The Pod name mounting this PVC. The format should be /. + // The Pod name mounting this PVC. PodName string `json:"podName"` + // The Pod namespace + PodNamespace string `json:"podNamespace"` + // The PVB-taken k8s node's name. NodeName string `json:"nodeName"` } + +// PVInfo is used to store some PV information modified after creation. +// Those information are lost after PV recreation. +type PVInfo struct { + // ReclaimPolicy of PV. It could be different from the referenced StorageClass. + ReclaimPolicy string `json:"reclaimPolicy"` + + // The PV's labels should be kept after recreation. + Labels map[string]string `json:"labels"` +} diff --git a/site/content/docs/main/file-system-backup.md b/site/content/docs/main/file-system-backup.md index 63cc544ce..0580b55e9 100644 --- a/site/content/docs/main/file-system-backup.md +++ b/site/content/docs/main/file-system-backup.md @@ -185,7 +185,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using FSB with the exception of: -- Volumes mounting the default service account token, Kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` diff --git a/site/content/docs/v1.10/file-system-backup.md b/site/content/docs/v1.10/file-system-backup.md index b9549ae7a..0ba10c618 100644 --- a/site/content/docs/v1.10/file-system-backup.md +++ b/site/content/docs/v1.10/file-system-backup.md @@ -186,7 +186,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using FSB with the exception of: -- Volumes mounting the default service account token, Kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` diff --git a/site/content/docs/v1.11/file-system-backup.md b/site/content/docs/v1.11/file-system-backup.md index 2502ae569..4104bebeb 100644 --- a/site/content/docs/v1.11/file-system-backup.md +++ b/site/content/docs/v1.11/file-system-backup.md @@ -186,7 +186,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using FSB with the exception of: -- Volumes mounting the default service account token, Kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` diff --git a/site/content/docs/v1.12/custom-plugins.md b/site/content/docs/v1.12/custom-plugins.md index bac500db5..351d8ff30 100644 --- a/site/content/docs/v1.12/custom-plugins.md +++ b/site/content/docs/v1.12/custom-plugins.md @@ -37,7 +37,7 @@ When naming your plugin, keep in mind that the full name needs to conform to the - have two parts, prefix + name, separated by '/' - none of the above parts can be empty - the prefix is a valid DNS subdomain name -- a plugin with the same prefix + name cannot not already exist +- a plugin with the same prefix + name cannot already exist ### Some examples: diff --git a/site/content/docs/v1.12/file-system-backup.md b/site/content/docs/v1.12/file-system-backup.md index ab87a3bef..6799a4006 100644 --- a/site/content/docs/v1.12/file-system-backup.md +++ b/site/content/docs/v1.12/file-system-backup.md @@ -199,7 +199,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using FSB with the exception of: -- Volumes mounting the default service account token, Kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` diff --git a/site/content/docs/v1.7/restic.md b/site/content/docs/v1.7/restic.md index 8873c1ff5..6fa1543db 100644 --- a/site/content/docs/v1.7/restic.md +++ b/site/content/docs/v1.7/restic.md @@ -183,7 +183,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using restic with the exception of: -- Volumes mounting the default service account token, kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` annotation on the pod. diff --git a/site/content/docs/v1.9/restic.md b/site/content/docs/v1.9/restic.md index 492738af1..c1cd8aeb3 100644 --- a/site/content/docs/v1.9/restic.md +++ b/site/content/docs/v1.9/restic.md @@ -186,7 +186,7 @@ The following sections provide more details on the two approaches. In this approach, Velero will back up all pod volumes using Restic with the exception of: -- Volumes mounting the default service account token, Kubernetes secrets, and config maps +- Volumes mounting the default service account token, Kubernetes Secrets, and ConfigMaps - Hostpath volumes It is possible to exclude volumes from being backed up using the `backup.velero.io/backup-volumes-excludes` annotation on the pod. diff --git a/site/content/posts/2020-09-16-Velero-1.5-For-And-By-Community.md b/site/content/posts/2020-09-16-Velero-1.5-For-And-By-Community.md index 0d6938ebf..aa19bd536 100644 --- a/site/content/posts/2020-09-16-Velero-1.5-For-And-By-Community.md +++ b/site/content/posts/2020-09-16-Velero-1.5-For-And-By-Community.md @@ -33,7 +33,7 @@ With the release of 1.5, Velero now has the ability to backup all pod volumes us 1. Volumes mounting the default service account token 1. Hostpath volumes -1. Volumes mounting Kubernetes secrets and config maps. +1. Volumes mounting Kubernetes Secrets and ConfigMaps. You can enable this feature on a per backup basis or as a default setting for all Velero backups. Read more about this feature on our [restic integration](https://velero.io/docs/v1.5/restic/) page on our documentation website. diff --git a/test/e2e/backup/backup.go b/test/e2e/backup/backup.go index 52dc7ad8f..923781e25 100644 --- a/test/e2e/backup/backup.go +++ b/test/e2e/backup/backup.go @@ -31,15 +31,33 @@ import ( . "github.com/vmware-tanzu/velero/test/util/velero" ) +type BackupRestoreTestConfig struct { + useVolumeSnapshots bool + kibishiiPatchSubDir string + isRetainPVTest bool +} + func BackupRestoreWithSnapshots() { - BackupRestoreTest(true) + config := BackupRestoreTestConfig{true, "", false} + BackupRestoreTest(config) } func BackupRestoreWithRestic() { - BackupRestoreTest(false) + config := BackupRestoreTestConfig{false, "", false} + BackupRestoreTest(config) } -func BackupRestoreTest(useVolumeSnapshots bool) { +func BackupRestoreRetainedPVWithSnapshots() { + config := BackupRestoreTestConfig{true, "overlays/sc-reclaim-policy/", true} + BackupRestoreTest(config) +} + +func BackupRestoreRetainedPVWithRestic() { + config := BackupRestoreTestConfig{false, "overlays/sc-reclaim-policy/", true} + BackupRestoreTest(config) +} + +func BackupRestoreTest(backupRestoreTestConfig BackupRestoreTestConfig) { var ( backupName, restoreName, kibishiiNamespace string @@ -48,25 +66,34 @@ func BackupRestoreTest(useVolumeSnapshots bool) { veleroCfg VeleroConfig ) provideSnapshotVolumesParmInBackup = false + useVolumeSnapshots := backupRestoreTestConfig.useVolumeSnapshots BeforeEach(func() { veleroCfg = VeleroCfg + + veleroCfg.KibishiiDirectory = veleroCfg.KibishiiDirectory + backupRestoreTestConfig.kibishiiPatchSubDir veleroCfg.UseVolumeSnapshots = useVolumeSnapshots veleroCfg.UseNodeAgent = !useVolumeSnapshots if useVolumeSnapshots && veleroCfg.CloudProvider == "kind" { Skip("Volume snapshots not supported on kind") } + var err error flag.Parse() UUIDgen, err = uuid.NewRandom() kibishiiNamespace = "k-" + UUIDgen.String() Expect(err).To(Succeed()) + DeleteStorageClass(context.Background(), *veleroCfg.ClientToInstallVelero, KibishiiStorageClassName) }) AfterEach(func() { if !veleroCfg.Debug { By("Clean backups after test", func() { DeleteAllBackups(context.Background(), *veleroCfg.ClientToInstallVelero) + if backupRestoreTestConfig.isRetainPVTest { + CleanAllRetainedPV(context.Background(), *veleroCfg.ClientToInstallVelero) + } + DeleteStorageClass(context.Background(), *veleroCfg.ClientToInstallVelero, KibishiiStorageClassName) }) if veleroCfg.InstallVelero { ctx, ctxCancel := context.WithTimeout(context.Background(), time.Minute*5) @@ -106,6 +133,9 @@ func BackupRestoreTest(useVolumeSnapshots bool) { }) It("should successfully back up and restore to an additional BackupStorageLocation with unique credentials", func() { + if backupRestoreTestConfig.isRetainPVTest { + Skip("It's tested by 1st test case") + } if veleroCfg.AdditionalBSLProvider == "" { Skip("no additional BSL provider given, not running multiple BackupStorageLocation with unique credentials tests") } diff --git a/test/e2e/basic/namespace-mapping.go b/test/e2e/basic/namespace-mapping.go index ea2a8f53a..dbf98c1f9 100644 --- a/test/e2e/basic/namespace-mapping.go +++ b/test/e2e/basic/namespace-mapping.go @@ -102,7 +102,7 @@ func (n *NamespaceMapping) Verify() error { n.kibishiiData.Levels = len(*n.NSIncluded) + index By(fmt.Sprintf("Verify workload %s after restore ", ns), func() { Expect(KibishiiVerifyAfterRestore(n.Client, ns, - n.Ctx, n.kibishiiData)).To(Succeed(), "Fail to verify workload after restore") + n.Ctx, n.kibishiiData, "")).To(Succeed(), "Fail to verify workload after restore") }) } for _, ns := range *n.NSIncluded { diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index b4cb6b22a..7bf3a2e97 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -102,6 +102,10 @@ var _ = Describe("[Basic][Restic] Velero tests on cluster using the plugin provi var _ = Describe("[Basic][Snapshot] Velero tests on cluster using the plugin provider for object storage and snapshots for volume backups", BackupRestoreWithSnapshots) +var _ = Describe("[Basic][Snapshot][RetainPV] Velero tests on cluster using the plugin provider for object storage and snapshots for volume backups", BackupRestoreRetainedPVWithSnapshots) + +var _ = Describe("[Basic][Restic][RetainPV] Velero tests on cluster using the plugin provider for object storage and snapshots for volume backups", BackupRestoreRetainedPVWithRestic) + var _ = Describe("[Basic][ClusterResource] Backup/restore of cluster resources", ResourcesCheckTest) var _ = Describe("[Scale][LongTime] Backup/restore of 2500 namespaces", MultiNSBackupRestore) diff --git a/test/e2e/migration/migration.go b/test/e2e/migration/migration.go index a1a5e895c..da808ba92 100644 --- a/test/e2e/migration/migration.go +++ b/test/e2e/migration/migration.go @@ -273,15 +273,16 @@ func MigrationTest(useVolumeSnapshots bool, veleroCLI2Version VeleroCLI2Version) } By(fmt.Sprintf("Install Velero in cluster-B (%s) to restore workload", veleroCfg.StandbyCluster), func() { + //Ensure workload of "migrationNamespace" existed in cluster-A ns, err := GetNamespace(context.Background(), *veleroCfg.DefaultClient, migrationNamespace) Expect(ns.Name).To(Equal(migrationNamespace)) - Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("get namespace in cluster-B err: %v", err)) + //Ensure cluster-B is the target cluster Expect(KubectlConfigUseContext(context.Background(), veleroCfg.StandbyCluster)).To(Succeed()) _, err = GetNamespace(context.Background(), *veleroCfg.StandbyClient, migrationNamespace) Expect(err).To(HaveOccurred()) strings.Contains(fmt.Sprint(err), "namespaces \""+migrationNamespace+"\" not found") - fmt.Println(err) veleroCfg.ClientToInstallVelero = veleroCfg.StandbyClient @@ -335,7 +336,7 @@ func MigrationTest(useVolumeSnapshots bool, veleroCLI2Version VeleroCLI2Version) By(fmt.Sprintf("Verify workload %s after restore ", migrationNamespace), func() { Expect(KibishiiVerifyAfterRestore(*veleroCfg.StandbyClient, migrationNamespace, - oneHourTimeout, &KibishiiData)).To(Succeed(), "Fail to verify workload after restore") + oneHourTimeout, &KibishiiData, "")).To(Succeed(), "Fail to verify workload after restore") }) // TODO: delete backup created by case self, not all diff --git a/test/e2e/pv-backup/pv-backup-filter.go b/test/e2e/pv-backup/pv-backup-filter.go index d8de42dd2..556dfeb70 100644 --- a/test/e2e/pv-backup/pv-backup-filter.go +++ b/test/e2e/pv-backup/pv-backup-filter.go @@ -180,7 +180,7 @@ func fileContent(namespace, podName, volume string) string { } func fileExist(ctx context.Context, namespace, podName, volume string) error { - c, err := ReadFileFromPodVolume(ctx, namespace, podName, podName, volume, FILE_NAME) + c, _, err := ReadFileFromPodVolume(ctx, namespace, podName, podName, volume, FILE_NAME) if err != nil { return errors.Wrap(err, fmt.Sprintf("Fail to read file %s from volume %s of pod %s in %s ", FILE_NAME, volume, podName, namespace)) @@ -195,7 +195,7 @@ func fileExist(ctx context.Context, namespace, podName, volume string) error { } } func fileNotExist(ctx context.Context, namespace, podName, volume string) error { - _, err := ReadFileFromPodVolume(ctx, namespace, podName, podName, volume, FILE_NAME) + _, _, err := ReadFileFromPodVolume(ctx, namespace, podName, podName, volume, FILE_NAME) if err != nil { return nil } else { diff --git a/test/e2e/resourcepolicies/resource_policies.go b/test/e2e/resourcepolicies/resource_policies.go index 6f98c5ebd..df96bc3d9 100644 --- a/test/e2e/resourcepolicies/resource_policies.go +++ b/test/e2e/resourcepolicies/resource_policies.go @@ -24,7 +24,6 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/pkg/errors" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -164,7 +163,7 @@ func (r *ResourcePoliciesCase) Verify() error { if vol.Name != volName { continue } - content, err := ReadFileFromPodVolume(r.Ctx, ns, pod.Name, "container-busybox", vol.Name, FileName) + content, _, err := ReadFileFromPodVolume(r.Ctx, ns, pod.Name, "container-busybox", vol.Name, FileName) if i%2 == 0 { Expect(err).To(HaveOccurred(), "Expected file not found") // File should not exist } else { diff --git a/test/e2e/upgrade/upgrade.go b/test/e2e/upgrade/upgrade.go index 6fd4c40ed..c9e9af90b 100644 --- a/test/e2e/upgrade/upgrade.go +++ b/test/e2e/upgrade/upgrade.go @@ -29,7 +29,6 @@ import ( . "github.com/vmware-tanzu/velero/test" . "github.com/vmware-tanzu/velero/test/util/k8s" . "github.com/vmware-tanzu/velero/test/util/kibishii" - . "github.com/vmware-tanzu/velero/test/util/providers" . "github.com/vmware-tanzu/velero/test/util/velero" ) @@ -256,7 +255,7 @@ func BackupUpgradeRestoreTest(useVolumeSnapshots bool, veleroCLI2Version VeleroC By(fmt.Sprintf("Verify workload %s after restore ", upgradeNamespace), func() { Expect(KibishiiVerifyAfterRestore(*veleroCfg.ClientToInstallVelero, upgradeNamespace, - oneHourTimeout, DefaultKibishiiData)).To(Succeed(), "Fail to verify workload after restore") + oneHourTimeout, DefaultKibishiiData, "")).To(Succeed(), "Fail to verify workload after restore") }) }) }) diff --git a/test/util/csi/common.go b/test/util/csi/common.go index e96e865b0..932646f0c 100644 --- a/test/util/csi/common.go +++ b/test/util/csi/common.go @@ -21,14 +21,12 @@ import ( "fmt" "strings" - "github.com/pkg/errors" - snapshotterClientSet "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - . "github.com/vmware-tanzu/velero/test/util/k8s" ) @@ -128,6 +126,7 @@ func GetCsiSnapshotHandleV1(client TestClient, backupName string) ([]string, err } return snapshotHandleList, nil } + func GetVolumeSnapshotContentNameByPod(client TestClient, podName, namespace, backupName string) (string, error) { pvcList, err := GetPvcByPVCName(context.Background(), namespace, podName) if err != nil { diff --git a/test/util/k8s/common.go b/test/util/k8s/common.go index ed579cb77..da439f24c 100644 --- a/test/util/k8s/common.go +++ b/test/util/k8s/common.go @@ -104,7 +104,6 @@ func GetPvcByPVCName(ctx context.Context, namespace, pvcName string) ([]string, Args: []string{"{print $1}"}, } cmds = append(cmds, cmd) - return common.GetListByCmdPipes(ctx, cmds) } @@ -279,15 +278,30 @@ func CreateFileToPod(ctx context.Context, namespace, podName, containerName, vol fmt.Printf("Kubectl exec cmd =%v\n", cmd) return cmd.Run() } -func ReadFileFromPodVolume(ctx context.Context, namespace, podName, containerName, volume, filename string) (string, error) { +func FileExistInPV(ctx context.Context, namespace, podName, containerName, volume, filename string) (bool, error) { + stdout, stderr, err := ReadFileFromPodVolume(ctx, namespace, podName, containerName, volume, filename) + + output := fmt.Sprintf("%s:%s", stdout, stderr) + if strings.Contains(output, fmt.Sprintf("/%s/%s: No such file or directory", volume, filename)) { + return false, nil + } else { + if err == nil { + return true, nil + } else { + return false, errors.Wrap(err, fmt.Sprintf("Fail to read file %s from volume %s of pod %s in %s", + filename, volume, podName, namespace)) + } + } +} +func ReadFileFromPodVolume(ctx context.Context, namespace, podName, containerName, volume, filename string) (string, string, error) { arg := []string{"exec", "-n", namespace, "-c", containerName, podName, "--", "cat", fmt.Sprintf("/%s/%s", volume, filename)} cmd := exec.CommandContext(ctx, "kubectl", arg...) fmt.Printf("Kubectl exec cmd =%v\n", cmd) stdout, stderr, err := veleroexec.RunCommand(cmd) - fmt.Print(stdout) - fmt.Print(stderr) - return stdout, err + fmt.Printf("stdout: %s\n", stdout) + fmt.Printf("stderr: %s\n", stderr) + return stdout, stderr, err } func RunCommand(cmdName string, arg []string) string { diff --git a/test/util/k8s/persistentvolumes.go b/test/util/k8s/persistentvolumes.go index f4c800594..441c1bd10 100644 --- a/test/util/k8s/persistentvolumes.go +++ b/test/util/k8s/persistentvolumes.go @@ -22,10 +22,9 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/util/retry" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" ) func CreatePersistentVolume(client TestClient, name string) (*corev1.PersistentVolume, error) { @@ -93,3 +92,16 @@ func ClearClaimRefForFailedPVs(ctx context.Context, client TestClient) error { return nil } + +func GetAllPVNames(ctx context.Context, client TestClient) ([]string, error) { + var pvNameList []string + pvList, err := client.ClientGo.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to List PV") + } + + for _, pvName := range pvList.Items { + pvNameList = append(pvNameList, pvName.Name) + } + return pvNameList, nil +} diff --git a/test/util/kibishii/kibishii_utils.go b/test/util/kibishii/kibishii_utils.go index f9d2b00d8..de30dccf4 100644 --- a/test/util/kibishii/kibishii_utils.go +++ b/test/util/kibishii/kibishii_utils.go @@ -51,6 +51,7 @@ type KibishiiData struct { var DefaultKibishiiWorkerCounts = 2 var DefaultKibishiiData = &KibishiiData{2, 10, 10, 1024, 1024, 0, DefaultKibishiiWorkerCounts} +var KibishiiPodNameList = []string{"kibishii-deployment-0", "kibishii-deployment-1"} var KibishiiPVCNameList = []string{"kibishii-data-kibishii-deployment-0", "kibishii-data-kibishii-deployment-1"} var KibishiiStorageClassName = "kibishii-storage-class" @@ -107,6 +108,8 @@ func RunKibishiiTests(veleroCfg VeleroConfig, backupName, restoreName, backupLoc } fmt.Printf("VeleroBackupNamespace done %s\n", time.Now().Format("2006-01-02 15:04:05")) + + // Checkpoint for a successful backup if useVolumeSnapshots { if providerName == "vsphere" { // Wait for uploads started by the Velero Plugin for vSphere to complete @@ -165,11 +168,49 @@ func RunKibishiiTests(veleroCfg VeleroConfig, backupName, restoreName, backupLoc } } + // Modify PV data right after backup. If PV's reclaim policy is retain, PV will be restored with the origin resource config + fileName := "file-" + kibishiiNamespace + fileBaseContent := fileName + fmt.Printf("Re-poulate volume %s\n", time.Now().Format("2006-01-02 15:04:05")) + for _, pod := range KibishiiPodNameList { + // To ensure Kibishii verification result is accurate + ClearKibishiiData(oneHourTimeout, kibishiiNamespace, pod, "kibishii", "data") + + fileContent := fileBaseContent + pod + err := CreateFileToPod(oneHourTimeout, kibishiiNamespace, pod, "kibishii", "data", + fileName, fileContent) + if err != nil { + return errors.Wrapf(err, "failed to create file %s", fileName) + } + } + fmt.Printf("Re-poulate volume done %s\n", time.Now().Format("2006-01-02 15:04:05")) + + pvList := []string{} + if strings.Contains(veleroCfg.KibishiiDirectory, "sc-reclaim-policy") { + // Get leftover PV list for PV cleanup + for _, pvc := range KibishiiPVCNameList { + pv, err := GetPvName(oneHourTimeout, client, pvc, kibishiiNamespace) + if err != nil { + errors.Wrapf(err, "failed to delete namespace %s", kibishiiNamespace) + } + pvList = append(pvList, pv) + } + } + fmt.Printf("Simulating a disaster by removing namespace %s %s\n", kibishiiNamespace, time.Now().Format("2006-01-02 15:04:05")) if err := DeleteNamespace(oneHourTimeout, client, kibishiiNamespace, true); err != nil { return errors.Wrapf(err, "failed to delete namespace %s", kibishiiNamespace) } + if strings.Contains(veleroCfg.KibishiiDirectory, "sc-reclaim-policy") { + // In scenario of CSI PV-retain-policy test, to restore PV of the backed up resource, we should make sure + // there are no PVs of the same name left, because in previous test step, PV's reclaim policy is retain, + // so PVs are not deleted although workload namespace is destroyed. + if err := DeletePVs(oneHourTimeout, *veleroCfg.ClientToInstallVelero, pvList); err != nil { + return errors.Wrapf(err, "failed to delete PVs %v", pvList) + } + } + // the snapshots of AWS may be still in pending status when do the restore, wait for a while // to avoid this https://github.com/vmware-tanzu/velero/issues/1799 // TODO remove this after https://github.com/vmware-tanzu/velero/issues/3533 is fixed @@ -191,10 +232,12 @@ func RunKibishiiTests(veleroCfg VeleroConfig, backupName, restoreName, backupLoc return errors.New(fmt.Sprintf("PVR count %d is not as expected %d", len(pvrs), pvCount)) } } + fmt.Printf("KibishiiVerifyAfterRestore %s\n", time.Now().Format("2006-01-02 15:04:05")) - if err := KibishiiVerifyAfterRestore(client, kibishiiNamespace, oneHourTimeout, DefaultKibishiiData); err != nil { + if err := KibishiiVerifyAfterRestore(client, kibishiiNamespace, oneHourTimeout, DefaultKibishiiData, fileName); err != nil { return errors.Wrapf(err, "Error verifying kibishii after restore") } + fmt.Printf("kibishii test completed successfully %s\n", time.Now().Format("2006-01-02 15:04:05")) return nil } @@ -309,6 +352,15 @@ func waitForKibishiiPods(ctx context.Context, client TestClient, kibishiiNamespa return WaitForPods(ctx, client, kibishiiNamespace, []string{"jump-pad", "etcd0", "etcd1", "etcd2", "kibishii-deployment-0", "kibishii-deployment-1"}) } +func KibishiiGenerateData(oneHourTimeout context.Context, kibishiiNamespace string, kibishiiData *KibishiiData) error { + fmt.Printf("generateData %s\n", time.Now().Format("2006-01-02 15:04:05")) + if err := generateData(oneHourTimeout, kibishiiNamespace, kibishiiData); err != nil { + return errors.Wrap(err, "Failed to generate data") + } + fmt.Printf("generateData done %s\n", time.Now().Format("2006-01-02 15:04:05")) + return nil +} + func KibishiiPrepareBeforeBackup(oneHourTimeout context.Context, client TestClient, providerName, kibishiiNamespace, registryCredentialFile, veleroFeatures, kibishiiDirectory string, useVolumeSnapshots bool, kibishiiData *KibishiiData) error { @@ -338,16 +390,12 @@ func KibishiiPrepareBeforeBackup(oneHourTimeout context.Context, client TestClie if kibishiiData == nil { kibishiiData = DefaultKibishiiData } - fmt.Printf("generateData %s\n", time.Now().Format("2006-01-02 15:04:05")) - if err := generateData(oneHourTimeout, kibishiiNamespace, kibishiiData); err != nil { - return errors.Wrap(err, "Failed to generate data") - } - fmt.Printf("generateData done %s\n", time.Now().Format("2006-01-02 15:04:05")) + KibishiiGenerateData(oneHourTimeout, kibishiiNamespace, kibishiiData) return nil } func KibishiiVerifyAfterRestore(client TestClient, kibishiiNamespace string, oneHourTimeout context.Context, - kibishiiData *KibishiiData) error { + kibishiiData *KibishiiData, incrementalFileName string) error { if kibishiiData == nil { kibishiiData = DefaultKibishiiData } @@ -357,6 +405,18 @@ func KibishiiVerifyAfterRestore(client TestClient, kibishiiNamespace string, one if err := waitForKibishiiPods(oneHourTimeout, client, kibishiiNamespace); err != nil { return errors.Wrapf(err, "Failed to wait for ready status of kibishii pods in %s", kibishiiNamespace) } + if incrementalFileName != "" { + for _, pod := range KibishiiPodNameList { + exist, err := FileExistInPV(oneHourTimeout, kibishiiNamespace, pod, "kibishii", "data", incrementalFileName) + if err != nil { + return errors.Wrapf(err, fmt.Sprintf("fail to get file %s", incrementalFileName)) + } + + if exist { + return errors.New("Unexpected incremental data exist") + } + } + } // TODO - check that namespace exists fmt.Printf("running kibishii verify\n") @@ -365,3 +425,11 @@ func KibishiiVerifyAfterRestore(client TestClient, kibishiiNamespace string, one } return nil } + +func ClearKibishiiData(ctx context.Context, namespace, podName, containerName, dir string) error { + arg := []string{"exec", "-n", namespace, "-c", containerName, podName, + "--", "/bin/sh", "-c", "rm -rf /" + dir + "/*"} + cmd := exec.CommandContext(ctx, "kubectl", arg...) + fmt.Printf("Kubectl exec cmd =%v\n", cmd) + return cmd.Run() +} diff --git a/test/util/velero/velero_utils.go b/test/util/velero/velero_utils.go index a106cf5b5..fd0d919e4 100644 --- a/test/util/velero/velero_utils.go +++ b/test/util/velero/velero_utils.go @@ -1561,3 +1561,62 @@ func InstallTestStorageClasses(path string) error { } return InstallStorageClass(ctx, tmpFile.Name()) } + +func GetPvName(ctx context.Context, client TestClient, pvcName, namespace string) (string, error) { + + pvcList, err := GetPvcByPVCName(context.Background(), namespace, pvcName) + if err != nil { + return "", err + } + + if len(pvcList) != 1 { + return "", errors.New(fmt.Sprintf("Only 1 PV of PVC %s pod %s should be found under namespace %s", pvcList[0], pvcName, namespace)) + } + + pvList, err := GetPvByPvc(context.Background(), namespace, pvcList[0]) + if err != nil { + return "", err + } + if len(pvList) != 1 { + return "", errors.New(fmt.Sprintf("Only 1 PV of PVC %s pod %s should be found under namespace %s", pvcList[0], pvcName, namespace)) + } + + return pvList[0], nil + +} +func DeletePVs(ctx context.Context, client TestClient, pvList []string) error { + for _, pv := range pvList { + args := []string{"delete", "pv", pv, "--timeout=0s"} + fmt.Println(args) + err := exec.CommandContext(ctx, "kubectl", args...).Run() + if err != nil { + return errors.New(fmt.Sprintf("Deleted PV %s ", pv)) + } + } + return nil +} + +func CleanAllRetainedPV(ctx context.Context, client TestClient) { + + pvNameList, err := GetAllPVNames(ctx, client) + if err != nil { + fmt.Println("fail to list PV") + } + for _, pv := range pvNameList { + args := []string{"patch", "pv", pv, "-p", "{\"spec\":{\"persistentVolumeReclaimPolicy\":\"Delete\"}}"} + fmt.Println(args) + cmd := exec.CommandContext(ctx, "kubectl", args...) + stdout, errMsg, err := veleroexec.RunCommand(cmd) + if err != nil { + fmt.Printf("fail to patch PV %s reclaim policy to delete: stdout: %s, stderr: %s", pv, stdout, errMsg) + } + + args = []string{"delete", "pv", pv, "--timeout=60s"} + fmt.Println(args) + cmd = exec.CommandContext(ctx, "kubectl", args...) + stdout, errMsg, err = veleroexec.RunCommand(cmd) + if err != nil { + fmt.Printf("fail to delete PV %s reclaim policy to delete: stdout: %s, stderr: %s", pv, stdout, errMsg) + } + } +}