From 2b8bb871d3890c4955b05728b58f4063dd496f7b Mon Sep 17 00:00:00 2001 From: allenxu404 Date: Fri, 19 Jan 2024 14:49:07 +0800 Subject: [PATCH] Add the finalization phase to the restore workflow Signed-off-by: allenxu404 --- changelogs/unreleased/7377-allenxu404 | 1 + config/crd/v1/bases/velero.io_restores.yaml | 2 + config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/restore_types.go | 15 +- pkg/cmd/server/server.go | 14 ++ pkg/controller/constants.go | 2 + pkg/controller/restore_controller.go | 10 +- .../restore_finalizer_controller.go | 213 ++++++++++++++++++ .../restore_finalizer_controller_test.go | 204 +++++++++++++++++ .../restore_operations_controller.go | 24 +- .../restore_operations_controller_test.go | 8 +- pkg/persistence/mocks/backup_store.go | 25 ++ pkg/persistence/object_store.go | 21 ++ pkg/persistence/object_store_test.go | 30 +++ 14 files changed, 544 insertions(+), 27 deletions(-) create mode 100644 changelogs/unreleased/7377-allenxu404 create mode 100644 pkg/controller/restore_finalizer_controller.go create mode 100644 pkg/controller/restore_finalizer_controller_test.go diff --git a/changelogs/unreleased/7377-allenxu404 b/changelogs/unreleased/7377-allenxu404 new file mode 100644 index 0000000000..27b82a972d --- /dev/null +++ b/changelogs/unreleased/7377-allenxu404 @@ -0,0 +1 @@ +Add the finalization phase to the restore workflow \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_restores.yaml b/config/crd/v1/bases/velero.io_restores.yaml index ba01b67b2b..8e53be6426 100644 --- a/config/crd/v1/bases/velero.io_restores.yaml +++ b/config/crd/v1/bases/velero.io_restores.yaml @@ -477,6 +477,8 @@ spec: - Completed - PartiallyFailed - Failed + - Finalizing + - FinalizingPartiallyFailed type: string progress: description: Progress contains information about the restore's execution diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index ace3e7db6a..212837b734 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -36,7 +36,7 @@ var rawCRDs = [][]byte{ []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\x1b7\xf2\xbf\xebSt9\a\xfdSe\x0ec\xff\xb7\xb6\xb6x\xb3\xe5͖v\x13Yeʾ\xa4r\x00\a=3\x88f\x00\x04\xc0P\xe2\xa6\xf2ݷ\x1a\x0fr\x1e )\xa9\xd6\u07b9H\x04\x1a\x8d\x1f\x1a\xfd\xc6b\xb1\xb8`Z|Ac\x85\x92+`Z\xe0\xa3CI\xbflq\xff7[\b\xb5ܾ\xb9\xb8\x17\x92\xafષNu\x9fЪޔ\xf8\x01+!\x85\x13J^t\xe8\x18g\x8e\xad.\x00\x98\x94\xca1\x1a\xb6\xf4\x13\xa0T\xd2\x19նh\x165\xca\xe2\xbe\xdf\xe0\xa6\x17-G㙧\xad\xb7?\x14o\xde\x16?\\\x00H\xd6\xe1\n\xb4\xe2[\xd5\xf6\x1dnXy\xdfk[l\xb1E\xa3\n\xa1.\xacƒx\xd7F\xf5z\x05\x87\x89\xb06\xee\x1b0\xdf*\xfeųy\xef\xd9\xf8\x99VX\xf7\xaf\xdc\xecO\xc2:O\xa1\xdbްv\x0e\xc2OZ!\xeb\xbeef6}\x01`K\xa5q\x057\x04C\xb3\x12\xf9\x05@<\xa2\x87\xb5\x00ƹ\x17\x1ako\x8d\x90\x0e\xcd\x15qH\xc2Z\x00G[\x1a\xa1\x9d\x17ʭ\xe2\x10\x00B@\b\xd61\xd7[\xb0}\xd9\x00\xb3p\x83\x0f\xcbkykTm\xd0\x06x\x00\xbfY%o\x99kVP\x04\xf2B7\xccb\x9c\r\xe2]\xfb\x898\xe4v\x04\xda:#d\x9d\x83q':\x84\x87\x06%\xb8FX\b\xa7\x85\af\t\x8eq\xfe\x94\xf9\x8d\xfd<-\xb7\x8euz\x84\xe0\xca ;,\r\x108s\x98\x03\xb0\x97'\xa8\n\\\x83$y\xafXLH!k?\x14n\x02\x9c\x82\rz\x88ȡ\xd7\x19d\x1a\xcbB+^\xc8\xc4t\x04\xebf2zN6D\xff\xdfF5\x02t\xab\xf8\v\xa0\x98\x9d\xb8\x01\ng ,\xb0\xb84\x9c\xe2 \xe8\xe4\x8e>\xfd}}\aik\x7f\x19S\xe9{\xb9\x1f\x16\xda\xc3\x15\x90\xc0\x84\xacȬ\xe9\x12+\xa3:\xcf\x13%\xd7JH\xe7\x7f\x94\xad@9\x15\xbf\xed7\x9dpt\xef\xbf\xf7h\x1d\xddU\x01W>S \xf7\xd4k\xd2\\^\xc0\xb5\x84+\xd6a{\xc5,~\xf5\v I\xdb\x05\t\xf6iW0Lr\xa6\xc4Aj\x83\x89\x94\xa2\x1c\xb9\xafIޱ\xd6X\xd2\xed\x91\x00i\xa5\xa8D\xf4P\x952\xc0\xa6\xe4ňq\xdep\xe9\xcbz\xa7)\xd1\x04\xd9\xfbܚ\x84M\x0e|jr\x98\x81r\xc6\x14\xa0\x9dz\xd9\xfd\x1a\x83ZY\xe1\x94\xd9\x11\xe3\xe0`\x8b\x19\x87#\xd7@\x9fT\x1cϜ\xe3Fq\xcc\xc1\xa6\xa5\xe0\x1a\x16\xb4\x95\xf2+\xf2G\xbd\x94\xf3]\xe8S\xf2Y\xc0\xb4\xe2gp\xc5\x1d\x19\x18\xacР,19\xaeS\xc9C\x06\xd90\xac\xcf1\x1eW\n8\xe1ճ\x88\xdf\xdd^'O\x9e\x84\x18\xb1\xbb\xf9\xbeg\xe4C_%\xb0\xe5>Н\xdf\xfb\xf2\xba\n\x9by\x9f\xe6\x140\xd0\x02C\x1a\xb8\x0f\x12 \xa4u\xc88\xa8*ˑj\x12 \xc37\x18W\xbc\x0e\x1e,\xba\xcaCh!\xd9\x03#\xdf)8\xfcs\xfd\xf1f\xf9\x8f\x9c\xe8\xf7\xa7\x00V\x96h}\x16\xec\xb0C\xe9^\xef\x13s\x8eV\x18\xe4\x94fc\xd11)*\xb4\xae\x88{\xa0\xb1\xbf\xbc\xfd5/=\x80\x1f\x95\x01|d\x9dn\xf15\x88 \xf1\xbd[NJ#l\x10Ǟ#<\b\u05c8i0\xddK\x80\xd4+\x1e\xfb\xc1\x1fױ{\x04\x15\x8f\xdb#\xb4\xe2\x1eW\xf0ʧ5\a\x98\x7f\x90\xed\xfc\xf9\xea\b\xd7\xff\v\xa6\xfd\x8a\x88^\x05p\xfb8<4\xba\x03\xc8`yF\xd45\x1e\xb2\xaa\xe9\xe7\x83\n\xb9\xea\xefA\x19\x92\x80T\x03\x16\x9e1\xdd^p\x94\xc8g\xa0\x7fy\xfb\xebQ\xc4cy\x81\x90\x1c\x1f\xe1-\x88X\xdahſ/\xe0\xcek\xc7N:\xf6H;\x95\x8d\xb2xL\xb2J\xb6\xbb\x90\xe7n\x11\xac\xa2B\t\xdbv\x11\xf2 \x0e\x0flGRH\x17G\xfa\xc6@3\xe3Njk\xca~\xee>~\xf8\xb8\n\xc8H\xa1j\xef\x89)jV\x82\xb2\x19JcB,\xf6\xda8\v\xe6\xe9\xb3}P\x1f\xa7\xa0l\x98\xac1\x9c\x17\xa1\xea):\x16\x97/\xb1\xe3yJ\x92\xbeLj2u\x1c\xff\xb3\xe0\xfe\xc4\xc3\xf9\f\xfa\t\x87\x1bV\x19'\x0fw\xdfo\xd0Ht\xe8\xcf\xc7Ui\xe9h%jg\x97j\x8bf+\xf0a\xf9\xa0̽\x90\xf5\x82Ts\x11t\xc0.}\x99\xba\xfc\xce\xffy\xf1Y|E\xfb\xd4\x03\x8d*\xed\xafy*\xda\xc7._t\xa8\x94\xc3>=\x8e]\xaecf5]Kf\xf1Ј\xb2I\xc5I\xf4\xb1G\x8cIP&̃kfr\xf7\xd5U\x99\x04\xda\x1bB\xb4[\xc4^ڂIN\xff[a\x1d\x8d\xbfH\x82\xbdx\x92\xf9~\xbe\xfe\xf0m\x14\xbc\x17/\xb2\xd5#\tx\xf8\x1e\x17\aX\x8b\x8e\xe9E\xa0fNu\xa2\x9cPSVz\xcdI\xf0\x95@s&\x8d\xfb4\"N\x89f&\xbf\xdd\xd3<+\x8ft\xac\xce$n\xc3\xd6\xe1\xa9\xf4\ue93cƍ\x1bV[`\x06\x81A\xc74\xdd\xf3=\xee\x16!!\xd0LP4\xa7\x80\xbd\xef\x8a\x00Ӻ\x15\xd9\xc0\x1d\xc3~LY\xa3$\xa8,g\xb5=v\xf6쭥.\xd0\x1a\x1d\x95\xad\xdfD\x0e\x9f'{>Y&\xf9\xc4z$\x8a\xaa\x90\xbb\xaa\xbdoy\x96\xa9\x8e\x9f\xb4\xcfA\x1d\x11\x9f\x89B\xf11=\x17\x83֨\x99!K\xf7\xaf.W\xd3g\xc1\xd7`\x85o%Sf\x1aR\xd5\xd0\xe8\xb1\x14\x9c(\xb5R\x063.\x13\xe6ae\x14D\xc6\xf0\xbfe\xfc\xc8\xea\xc9l\xd0#\xe7\x03\xde\xf19b8\xd2o\xf6Om+\xf8\xe3ϋ\xff\x04\x00\x00\xff\xff{ŋW\xf4\"\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs#\xb7\x11\xbe\xf3Wt\xad\x0f\x8a\xabv\x86\xdeM*\x95\xe2mW\x8aSJl\xadj\xa9\u074b\xcb\apМ\x819\x03\xc0x\x90b\\\xfe\xef\xa9\x06\x06\xe4\xbcHJ\xaaȞˮ\x80F\xe3Ç~\xa1\x99eٌi\xf1\x15\x8d\x15J.\x80i\x81\x8f\x0e%\xfde\xf3\xcd?l.\xd4|\xfbn\xb6\x11\x92/\xe0\xda[\xa7\x9a\xcfh\x957\x05\xde\xe0ZHᄒ\xb3\x06\x1d\xe3̱\xc5\f\x80I\xa9\x1c\xa3aK\x7f\x02\x14J:\xa3\xea\x1aMV\xa2\xcc7~\x85+/j\x8e&(O[o\xbf\xcb߽Ͽ\x9b\x01H\xd6\xe0\x02\xb4\xe2[U\xfb\x06\rZ\xa7\f\xda|\x8b5\x1a\x95\v5\xb3\x1a\vR^\x1a\xe5\xf5\x02\x8e\x13qq\xbbq\x04}\xaf\xf8נ\xe7s\xd4\x13\xa6ja\xdd\x7f&\xa7\x7f\x10\xd6\x05\x11]{\xc3\xea\t\x1ca\xd6\nY\xfa\x9a\x99\xf1\xfc\f\xc0\x16J\xe3\x02\xee\b\x8af\x05\xf2\x19@{\xce\x00-\x03\xc6y`\x8e\xd5\xf7FH\x87\xe6\x9aT$\xc62\xe0h\v#\xb4\v\xcc\x1c\xf4\x80Z\x83\xab\x90\xb6\f\xac2!\x85,\xc3P\x84\x00N\xc1\n\xa1E\u00832\x80_\xac\x92\xf7\xccU\vȉ\xb8\\+\x9eˤ\xb3\x95\x89\x9c\xdf\rFݞ\xcea\x9d\x11\xb2<\x85\xec\xff\f\xaa\x87\xe7^\xf1'\"y\xa80\xc8$4^\u05caq4\xb4y\xc5$\xaf\x11\xc8@\xc1\x19&\xed\x1a\xcd\t\x14i\xd9\xc3^\xf7\x91|I\xfa:3\xcfa\xe79TD\xd9\xde\xf6_\xbbC\x97\xf6\xbdW\xbc]\x00\xadQ\x83u\xccy\v\xd6\x17\x150\vw\xb8\x9b\xdf\xca{\xa3J\x83\xd6N\xc0\b\u2e6e\x98\xed\xe3X\x86\x89\xd7űV\xa6an\x01B\xba\xbf\xff\xed4\xb6vQ\xee\x94c\xf5ǽC\xdbC\xfa0\x1c\x8eh\xc9\xd9\xca\xf6\xfa\xff\x14\xb8+\x82t\xa3d\x9f\u05cf\x83\xd1)\xb0\x1d\xa5)\xde\xe6\x85\xc1\x10j\x1fD\x83ֱF\xf7\xb4~(\xfb\xfa8sq No\xdf\xc5PVTذE+\xa94\xca\x0f\xf7\xb7_\xff\xba\xec\r\x03h\xa34\x1a'Rt\x8d_'ytF\xa1\xcf\xec\x15)\x8cR\xc0)k\xa0\x8dN\x11ǐ\xb7\x18\xa2\xb3\b\v\x06\xb5A\x8b2摞b !&A\xad~\xc1\xc2\xe5\xb0DCj\xc0V\xca\xd7!\x02m\xd180X\xa8R\x8a\xff\x1et[\xf2=ڴf\x0e\xdb\x10\x7f\xfcB\f\x96\xac\x86-\xab=\xbe\x05&94l\x0f\x06i\x17\xf0\xb2\xa3/\x88\xd8\x1c~$\v\x11r\xad\x16P9\xa7\xedb>/\x85KI\xb3PM\xe3\xa5p\xfby\xc8\x7fb\xe5\x9d2v\xceq\x8b\xf5܊2c\xa6\xa8\x84\xc3\xc2y\x83s\xa6E\x16\xa0ː8\xf3\x86\x7fc\xda4k\xafzXGN\x17\xbf\x90\xeb\xce\xdc\x00%;\x10\x16X\xbb4\x9e\xe2Ht\nٟ\xff\xb9|\x80\xb4u\xb8\x8c!\xfb\x81\xf7\xe3B{\xbc\x02\"L\xc85\x05]\xbaĵQMЉ\x92k%\xa4\v\x7f\x14\xb5@9\xa4\xdf\xfaU#\x1c\xdd\xfb\xaf\x1e\xad\xa3\xbb\xca\xe1:T\x12\x14/\xbd&\xcb\xe59\xdcJ\xb8f\r\xd6\xd7\xcc\xe2\xab_\x001m3\"\xf6iW\xd0-\x82\x86\u0091\xb5\xceD\xaa`N\xdcװ*Yj,\xe8\xfa\x88AZ*֢\b\xbeA\xe1\a\xd8H>艹v]\xfaV\xac\xd8x\xbdtʰ\x12\x7fPQ\xe7Ph\x80\xed\xe3Ԛ\x04Nvr^T\x0e6J\x8e\x94\x02\xd4i\xf1\xaeB\x83\xdd5\x06\xb5\xb2\xc2)\xb3'\xc51[\xe6#\r'.\"\x1cY\xf1\vǠp\x1f\x1c\xc2\xe0\x1a\r\xca\x02S\x848W\xc9L\x9c\xa2\x93\xd0\xc7\x10OS\x0fg\xa2\xe7$\xe0\x0f\xf7\xb7)b&\x86[\xe8n\xbc\xef\x05z\xe8[\v\xacyH(\x97\xf7\xbe\xba]\xc7\xcdB\xecp\n\x18h\x81\xb1\"=\x04c\x10\xd2:d\x1c\xd4zR#\xbd\r\x80\x1c\xcc`\xbb\xe2m\x8c\x14mH:\x86p\xa2\x1e\x18\xc5(\xc1\xe1\xdf\xcbOw\xf3\x7fM1\x7f8\x05\xb0\xa2@kC\xbe\xc6\x06\xa5{{\xc8\xd9\x1c\xad0ȩp\xc1\xbcaR\xacѺ\xbc\xdd\x03\x8d\xfd\xe9\xfd\xcf\xd3\xec\x01|\xaf\f\xe0#kt\x8doAD\xc6\x0f\xe1/ٌ\xb0\x91\x8e\x83F\xd8\tW\x89a\xd2:0@\xd6\xd5\x1e{\x17\x8e\xeb\xd8\x06A\xb5\xc7\xf5\b\xb5\xd8\xe0\x02ބJ\xf0\b\xf37r\xac\xdfߜ\xd0\xfa\x97\xe8@oH\xe8M\x04w\xc8w]\x8f<\x82t\x15s\xe0\x8c(K<\x16\xa2\xc3/\x04o\n\x89߂2ĀT\x1d\x15A1\xdd^\x8cG\xc8G\xa0\x7fz\xff\xf3I\xc4}\xbe@H\x8e\x8f\xf0\x1e\x84\x8c\xdchſ\xcd\xe1!X\xc7^:\xf6H;\x15\x95\xb2x\x8aY%\xeb}\xac\xf6\xb7\bV5\b;\xac\xeb,\xd6\x1b\x1cvlO,\xa4\x8b#{c\xa0\x99qg\xad5U\x19\x0f\x9fn>-\"22\xa82\xc4;\xcaNkAU\x03\x95\v1\xe7\x05k\x1c%\xcd\xf4Y\x1f\xcd\xc7)(*&K\x8c\xe7EX{\xcaB\xf9\xd5K\xfcx\x9c\xfa\xd37Q\x02\f\x03ǟ\x96D\x9fx\xb8P\xa9>\xe1pݷ\xd6\xd9\xc3m\xfc\n\x8dD\x87\xe1|\\\x15\x96\x8eV\xa0vv\xae\xb6h\xb6\x02w\xf3\x9d2\x1b!ˌL3\x8b6`\xe7\xe1\xc9<\xff&\xfc\xf3Ⳅ\xd7\xf5S\x0f\xd4{\xf4\xbf\xe6\xa9h\x1f;\x7fѡR\xad\xf8\xf4\x8bE\x96f\x82*$*\x82b\xd59]\xd73\xadk*l\xdaR\xea\xd0T,\x94\\\x8bқ\xf0z\x19\x93\"}]\xb3U\x8d\vpƏ\xe9<\xe3)\xdd\xfe\xe3\x85\xcb\xfd\xd2\x11M7{\xa1\x03ꪩ\xbb\xed\xf5ELJA\xe9\x9b1\x94\f6J\v61Nv=\xf2i\x9ax3\xae\x05\xcf\\lt\x9a\v\x1c\xb4\xed\xba\x89\xb7g\xebs\xb1\x16\x0f#\xf4\xde\v\x9e7\x9dĞ\xeb\x8b\x06\x7f\xf5\xf4\xb0\xe8#̦_\xd4\x03\x19\xad\xf8lHZ7\x8c\r&\x8fAh8\xd1\xf7\xef\xc1l\xaf\x8d\xdc=\u0378\x19\x11z\x94\xcfiGľh\xcb{̊.uK\xe9I\xf8\xe2\x86D\xa1\xe8\xb9\xd5ki^\xb0\x81\xeb\xf1\x8a\xd0\xfd3\xbc\xf5\t\xd1`x\xe5ǖ\xee\x8eٴ\xc9\xd4}CG_\\\x1a*\x11R\x87<<\x86譶f\xa2F\x0e\x87_\xa6\xc2\xcf\x0f6\xb4\xc1\xae\xa6j\xff\xa4\xc8[\xe4!\xd6N\x80\x1e\xafK\x9de\xce\x1cf\xa4\xe2e\x81fҽ\x1a\xb4\x96\x95\x97\xfc\xeb\xc7(\x15\xfb$\xed\x12`+\xe5ݡQ\xd2:ZKŕm\xad\xe0y͚\x8a\xd9KP\xeeIf\xca\xe2\x0e.\x7f\xde\xe4\xe0L(\xbb\xc3\xdd\xc4\xe8\xa8\xd3ߝ\xbcN&41\xf7}\xb0\x8eg\x11\xd0nt\x89\x83V\f*U'\xebV\x8e\x12\xb9oVh\x88\x88\xf0\xf3Bb$\x05\x8e\xa9\xceSx\xb1\x1e\x99\x88\x99\xc1P\xe2\xbe}\xc1MC\xa0\x01t7\xfa\x1b\xd0j\xb5zCk\xf6\x1d\x94fR\xdc\x12Z3\xf8a@ؿ\xf4\xfa\xe9\xdf\xf4\x9a\xc9\xf7\xc7\x0fo\x9e\x98(o\xc9\xc7F\x1bY=\x80\x96\x8d*\xe0'\xd81\xc1\f\x93\xe2M\x05\x86\x96\xd4\xd0\xdb7\x84P!\xa4\xa1\xf6\xb3\xb6\x7f\x12RHa\x94\xe4\x1c\xd4j\x0fb\xfd\xd4la\xdb0^\x82B\xe0a\xea\xe3\x9f\xd6\x1f\xfey\xfd\xa77\x84\bZ\xc1-Q\xa0\x8dT\xa0\xd7G\xe0\xa0\xe4\x9a\xc97\xba\x86\xc2\xc2\xdc+\xd9Է\xa4\xfd\xc1\x8d\xf1\xf3\xb9\xb5>\xb8\xe1\xf8\x853m\xfe\xdc\xfd\xfa\x17\xa6\r\xfeR\xf3FQ\xdeN\x86\x1f5\x13\xfb\x86S\x15?\xbf!D\x17\xb2\x86[\xf2\xc5NS\xd3\x02\xca7\x84\xf8\xa5\xe3\xb4+\xbf\xea\xe3\a\a\xa28@E\xddz\b\x915\x88\xbb\xfb\xcd\xf7\x7fy\xec}&\xa4\x04](V\x1bD\x80_\x1ba\x9aP\xf2\x1d\xf7f\x17\x80\xb8&\xe6@\rQP+\xd0 \x8c&\xe6\x00\x84\xd65g\x05\xa2:B$D\xee\xe2(MvJV-\xb4--\x9e\x9a\x9a\x18I(1T\xed\xc1\x90?7[P\x02\fhR\xf0F\x1bP\xeb\b\xabV\xb2\x06eX@\xack\x1dv\xe9|\x1d\xec\xe5\x9dݮ\xebEJ\xcb'\xe0\x96\xecQ\x06\xa5ǐ]\xad90\xddnm\xb8\x1d\xbf%*\x88\xdc\xfe\x15\n\xb3&\x8f\xa0,\x18\xa2\x0f\xb2\xe1\xa5e\xaf#(\x8b\x9cB\xee\x05\xfb\xef\b[ۍ\xdaI95\xe0\xe9\xdd6&\f(A99R\xde\xc0\r\xa1\xa2$\x15=\x11\x05v\x16҈\x0e<\xec\xa2\xd7\xe4\x17$\x8f\xd8\xc9[r0\xa6ַ\xef\xdf\xef\x99\tǤ\x90U\xd5\bfN\xef\x91\xe3ٶ1R\xe9\xf7%\x1c\x81\xbf\xd7l\xbf\xa2\xaa80\x03\x85i\x14\xbc\xa75[\xe1\xd2\x05\x1e\x95uU\xfeC$ۻ\xdeZ\xcd\xc9r\x9e6\x8a\x89}\xe7\ad\xf3\t\nX\x86w\xbc䆺]\xb4\x88\xb6\x9f,v\x1e>=~\xeb\xf2\x19\xd3C\xec#\xde;\xccג\xc0\"\x8c\x89\x1d(GD\xe46\v\x13DYK&\f\xfeQp\x06b\x88~\xddl+f,\xdd\x7fk@[\x86\x96k\xf2\x11e\a\xd9\x02i\xea\x92\x1a(\xd7d#\xc8GZ\x01\xffH5\\\x9d\x00\x16\xd3ze\x11\x9bG\x82\xae\xd8\x1bvvX\xeb\xfc\x10\x84\xd7\b\xbd\xfc\xe9\x7f\xac\xa1\xe8\x9d\x18;\x8c\xed\xfc1';\xa9z\xc2\xc1\x0eY\xf7\x80\xa6\x0f\xadm\xee\xf4[\t6\xfce\xb0\x94\x7f\x8f\x1d-\xff\xd8E4\x82\xfd\xd6\x00\x8a8wb\xe1L\xa4\x9c\x81$a}\xc8\x16\xeb\xb3\xdfGpj\x1b\xfc(xSB\x19\xa5\xed\xd9^\x06+\xfet6\x00\xb5\x0ee\xc2\xf2\xbf\x15\xffv٢\xfdՊ\xd3Ċ\xa9\x02b9\x90\t\a\x8f0\x81\x9bMb\xda6f\xa0J,nrw\x84\x88\x86s\xba\xe5pK\x8cj`\x043T)z\x1aALP\xc1\xb9x\x89\xfd\xbd@\u0b00\xae\xa2p\xa8qJ\x86\xaa\xf3\x15\x91?8V\x98\xb6\xe2,\xec\xf2^rV\x9cfQ\x93\x1a\x14\x8e\x9b?|\x81\x83\xb7p\xa0G&UbK\xf6Dڮ\x1dE\xda\nSie\x99\aR^\xb6\xe1$\xb2\x0eR>\xcd\xd1\xfegۧ\x95ڤ@\xe3-n\xc5S\xdb+\xd1-\x10\xf8\x01Ec\x12\xcb$\xa4lP\x81HEj\xa9\xcd8\xdd\xc7e\x0fq\xe2`\x8ci\xc9\x14Ӝ\xed̋\xca@9\xbbўؔ\x02\xecZ+K\xb9\xb6\xaf\x92\x8d\xeb;\xd4o\x1d\x8c\xa71B\xb6TCI\xa4\xe7\xfa\x86\x83\xf6s\x95H\xfeV\xae܌\x82\x8e\x9bw\x96\x06\xa7[\xe0D\x03\x87\xc2Hu\x8e\xc9\x1c|\xba\x96#+G𘐚}\xf6o76\x01\x92X6\x7f>\xb0\xe2\xe0\x8c\x00˛\b\x87\x94\x124\n\x0ek\xa8\x9e\xc66I\xe6h\xef'\x99\x12\x1dm\x9b9SCx)qҶ\fq۶\x19\xc1{&X\xfc\xf7\xa4\xe6l\xdb\xffO\xc4\x06Mr\x01\xd3nΆ\xbe.ӢSe\x8d\xfd͎@U\x9b\xd3\ra&|\x9d\x83H9\xef\xcc\xffwL\x98\xe5\x1c\xbf\x19\x8e|U\x8e\x9f\xa4\xca\x1cDK\x958\xfd\xdf!QPY\x942;:\x97\x15\\{\x82\x84\xb9\x9fj=\x04\xda5y\a\xd7a\xd2~@D\xa0㝏<\x82a\xb1 \x8b\xe67G\xf2\x05Ih\x01\xf7\x17l3\x92\xad\x13>D¾ӎD\xf6\x14\x1cX\x9d\xb9Q\x8c\x1ej\xc0\xd3\x12\x02c\xdf)ge\x9c\xc8\xf1\xfdF\x8c[\xc3\xfd\xf6E\x9a\x8d\xb8q\x1e\x99F.\xf9I\x82\xfe\"\r~\xb9\n:\xdd\xc2/@\xa6\x1b\x88\xc7K8\xb1m\xf1Ѝ\xb0e0\xb7k\x1b\x17H\x89\xe4a\x9al\x84u\\<>0^ꦛ\xd6\x0f\xfdV5\x1aChB\x8a\x15\xaa\xcauj&\x87\xecL\x90R\xf5(r\xbe\xb48\xa9\x9b0\x13\xec7\xabI\xdcx\x17\x01洀2x\x9b\x18\xb7\xa4\x06\xf6\xac \x15\xa8\xfd\x94\xe2\xe8\xb6\xda\xca\xf7\xbc%dJ]\xd7\x16rX\x9ej\x0f͋\xeer~1+{r3z\x05b\xcfv\x1d\tW\x8ew\x9d\xdf\x11\xaaX\xb4?f\xb1K\xcb\x12SH\x94\xdf/\x90\xf8\vhq\xae\xfb\xdd\u009c\x86\xachm\xcf\xef\xffX5\x87\f\xfd\xbf\xa4\xa6Le\x9c\xe1;L\x13q\xe8\x8d\xf5\x81\xb1\xee4v\x06\xa6\x89\xa5\xef\x91\xf2\xf3@xbs\xd2\xca\x16\xe0N\x91\xcbݙ\xc5rC\x9e\x0fR;\x9d\xbac\xc0S!\x9b~c\x9a\xbc}\x82\xd3ۛ39\xf0v#\xde:\x05\xbfX\xdcDkA\n~\"oq\xecۗ\x18A\x99\x9c\x98\xd9\xed\xc7\xea)\x86\xe4V\x15\xadW\x9e{\x8d\xacX1:N$\xc3\xe3m\xeb\xb1S7D\xde\xc6ƽy<\xb5\xdb,\xfe\xad\xa56?\xa7\x03}#\xeb\xb9\x0f#\xfa6m\"^6k\xeb\xfb\xd8W\x14\xc6\xd6\x02\xdc\x19P>\xf8\xe7\x04t\xf0\x1c^\xe8S\xcd\x05\xf7b`\x8fƀ\xacE\xf0\f7\xb9TI\xce\x12\x97X\x9b\x16/\v\xed\xf4O?:\xb1I{\xb2\xed\xdfݍ\xbc\xb65\\Ȫ\xa2\xc3\xe4`\xd6R?\xba\x91\x81\xa7= G}\xb5o\xf0<盉\x81\x870-\xf8\xcć\tB\x83\xd8\x00\xe5\x19\x8a\x92Z\xceK0\xd7\x0eT\x93-\x80\x881\xf5?\x82\x9e\xaf\x98\xd8\xe0\x04\xe4ë\xdb\x05\xa4E\xd7E\xe4\f\xa8\x8e\x04\x8d\x1fPS\xe5\x9aT\xb2$\xcf\aP\xd0\xe3\x8a\xf3@\xb9\xb543A\ni\xba\xf1\b\v\xb7\x96\xe5;MvLi\xd3]h.\xc35:\x97\x1d\x16R\xd8\xee\xee\x1b\xab@6\xe6\x02\x1a|jG\xf7\xf2\xba\x15\xfd\xc1\xaa\xa6\"\xb4\x92M\x86Q\xe0\x9a\xd5/\xac\x8a\xc9WO\x81g\xcaL\xccCad\xc6HK\xa5\x9a\x83\xc9%\xf1\x16vV\x1c\x15RhV\x82\n\xc5\x01\x8e\xb2Lڃ\xbb\xa3\x8c7\xa9\xb4O\xaa-uo\xc5'\xa5.\xf2n\xbf\xba\x91\x9dh\xe3A>\xf7\x11\x94\x8d\x82\x03=\x02a;\xc2\f\x01QX\xba\x80r\"\x1b\xa7\xf0\xc8@\xd4d\xb3e\x9e\x80\xb7\rDS\xe5!`\x85'\x9b\x89\xc9`Z\xb7\xfbg\xca\xf85\xc8f9\xef\xb3T\x0f@\xcbK\x020\xbfv\x86\x13\x10\xbaQ\x98\xb8w\xe2\xe5\x99\xf1\xbc5[\xca\x11N\x1bQ\x1c\x00\xe5\x94\xe8\x89\x0f\xe2\xc03\xa1\r\xd0\\^\xb0VS#\x04\x13\xfb<\xdae\x878\xdb\xe6P\xbd\x95\x92\x03\x1d\x16<\xa5\x9a\xc5\xf5\xe5b\xe8\xd7v\xf4\xef\"\x86\"\x05\xf2ͅ-xRyYD\x8d\x81\xaav\xe7M\x12Ո\xae\xf6\xb9\x82\x14Z\xe2\x83\xfbU\xbc\xa6s\xcd\x04\xcb \xec \xe7\xc2Lײ\xb4 \xaejY\xda\t\xa2QqI\xf8l\xd3\x03`OgpRp\xed\x91k\x16X\x99[ \xb4,\xa1t\x81Ik\xaax\x9fŕ\x97\x8d\x94*$w\xb7\xdcL̢lh=\x8f\x14C\xb1\xea\b\xabF<\t\xf9,V\xe8\xc9\xeb\xc5\x02$?4\xf8\xaaӛ\x8b%\xd1\xef)\x85\xfa\xfc\x9a\xcfS\xc1x\xba\x82\x94\xc9\xe6\x9bEѐ).\x98\x93k\xaety\xe4\xc7\xd9UL\xcd?1\xd8'\x9a?\xba\x9a\xe3\xdcz\xb6MzT\xc7\xf8{>\x809\x80\n\xc5\xcc+\xac\xdbN\xc9\xe96\x1f\xdd\xfa1\xb1\xc0\xcd\xf2O0\x85]\xe1\xe5\xa0\xe4-\xed\xe8X+\xe0\xc626m\xb8q\xe5ǪI0QV\xe1W\xda2ȩ\x9c\x98\xab\x97\xe8\xd7\x00\xc6z\x85P\x04(\xc3$\x89\x1d:Z\xbaJ\xdfn2\xbe_\xf8\x80!\xbf\xb0ҿyy`FM\xc3L%\xc3t\xd1\xe4\x14\xbe\xce٦\x8b\xb1\x96\a}?_M\xfb\xc7B\x9f\x81\xeak\xed\xcf\xc1\xa8\x01\xda\xc7`bȠ\x1c\x04%\xb7uٱ\xb0\x80\xb2\x94p\xb1\xa7Ї\x03-Ļ\x02O\xa2\f\x805\x86\x9a\xfdi\xf3\xd5\xedL\x93\x7f%\a\xd9$J\xea&\xb03S`1^V\xe1\x93\b`\xe8\xf1ú\xff\x8b\x91\xbe\xc8\x02#_\x89ݡ\xa3\xd2FS\x99(ّ\x95\r\xe5\xbdC\xd6a\x8b\x96{\x88TD0\x9eʯZ\xb6\n\xe3{lD\xbe\xd6.ϲX\x1cM\x9b\x88y\xb5\x18\x17W`\xf4+,F\x94\xd4ҔC~\xa9i~\x8d\xc5tQĒʊa\xdd\xc4(\xd0\xf9z\x8a\x1c\xeb~\xa6v₊\x89\xccj\xb9\x17'Hrj\".\xaa\x84\x98-(ˬ\x7f\xe8W6L\x83\\P\xf5\x90\x85\x9c\xf9\n\x87\xc5u\r\xbe\x8e`r\x1f\xd9\xd5\f\x89:\x85I\xc0\xa35\fS\xd5\t\xd3(OT.\xe4\xd7$L\x82\xc6z\x85\xf9J\x84\u05eb7|\r/`\\\xd4\xccV\x13\xbc\xc8KȨ\x17XR%0\x8b\xb1\v+\x02b\xc6\x7fdޥu\x00\xfd<\xff\bМ\xec\xffHv\x7f\x04\xe2d\xce?7\xa7?\x02{F\xedNr\xc9\xe4\x8fKr\xf9\xd1\r\xf9\x85\xd65\x13\xfbs>\xc9\xe5\xa6IN:+\x04\xe8\xce\xd9c\xa5\xae\xb7\xd0\xf3\xb3RS\xba[\xb9\t\x9f,\x84\xf5\x980rM\xee\xc4\xe9\f.^\tH\xfa \xfdk[vYό\xf3\xee\xdd$\x04\xdb\x05\xe5o\xf9\xe9td\xc0v\x1c\xb3\xb0\x93$\x94\xaag\x1dϹ`_\aݻ\x81\xc2ik;eh3s\xb8\xd0ڮ\x1anX\x9d<\xf2\xb5\x92G\x86a\xc7\x03\x9c\">\xff*\xf1V\xd0\xf6\x84\x90\xbe>\xc4Ӹ\x1e8\x0e4u\x86\x9e\x81sB\xf5\xf9\xf6\vw1\xb6\x90+\xbc\xebf)\x19\xf8\xc1_\xa0\xbd\xc1\x13\x9b\xf2\xd8E\xb8\xb2YY0x\xb9V'\"\"\xa3\xbah\xda\x1ev\xa6;~\xfb\xad\x01u\"\xf2\x889}o ͔\xdd;\xb9\xa2\xad\xff\x16$\x9d\x17\x97\xee:\xf6\xc0Oh\xe5\v\xb9\x13Nc'\xc1\x0eֈp\xac\x88k}#+ͭ\xdb3\xd25\tU\xc88:\xcd\x0f\x93\x8a)\xb7f\xfd\xba\x9e\xd2r_i\xd6J\xb9\x8a\xbft\xb9\xc74\x012\xb7\x06=/'2[s~-\xcfi\xcew\xca6\x1a\xf3jʯQK\xbe\xa0\x86|\x81\x0f\xb5̋\xcaFSN\xad\xf8U|\xa9+zS\xd7\xf0\xa7.\xf3\xa8f@\x0ej\xc0s\xaa\xbb\xb3\xd2x\xd99\x9b\x9c,\xdb|\xe6x\xbaj;\xa3Z;#\x1b4\xb7Ҍ\xaa\xece\xd5\xd8\x198\xbc\x92\xafu%o\xeb\x1a\xfe\xd6u=\xaeY\x9fk\x96sf~^VE}q\x92!\xa4\xa3\xbf\xc8\x12\xee\xa52s\x0e\xc2\xfd\xb0\x7f\"\x05\xd8q\x9a$/\x89\b]S\x99\x06k\xfb{\xbb\xff\xb2M\xa5\xb3u\xc1\xfc\xfdE\x96vms\xb9\x85\x87A\xf7\xb3+\xb4;P \xdc\xc3\x12\xff\xf9\xf8\xf5K\x84\x9f\xb2G\xbd\xd1;x\xd3\xc0\x19\x18\xa5G\x8e\xcf>\xf9\x82\x1b\x87-\xd4ᯜ$\xa05\xfb\x0f|\xb3k> sw\xbf\xc1\xae\xc1Z·\xbebB?\xe6\u07b6`\xb5G\xc4\xc8(\xf7ov=\x88\x89\xb2\xd3\xf8'\xc1\x17\x93\x82\xf6bc5Y\xae\bɞ\xba\xfb\x8d[ݚ|\xb6\xa6\x9b8\x11\xe9\x18\xef\xc0T\xb9\xaa\xa92'\xe4\x0e}\x13\xd70\x1e\x94\t:d*t2*j\xcf߂J\xe26<\t\x85\t\xb8S\xdd\xcff\x0e1z\xc9:\xc6oO\xccޛx\xc5u\x8c\xab\xe3\x15b*\xf19Y\x01\xf1j!)/\x86\xee\xbfω\xb5\x87\xd8qZ\x9eYO6\x84u\x12\xf8\xb1\xe3Q\xa4iAk}HD\x85^&\xd3\xf0\xa1*CM\x93\xb9\x1f\u05f7\xb7%V\x1c:\x02\xe8\x19\x82\x88R\x9dg\xfb\x06k\xb2g\xd5\x01B5\x8c&\xb4`\xfc\xa6\xe3\x98\xff>)\xcf\xccgA.~\x10ġgDT`\xa4Ɋ\xb1\xc0\v-^.HvΚp\x19\x85\xad\xd3vg\xe6\x8b\x12\x17\xbf%1\x8f\xac\x04\xa2ƞ\x91\xc8y*\xe2o\x8a\xcf\t\x91\xa4\x8b\x03\x94\r\x87\x8c\a\xde\x1e;]\xe7\x9fx\v\x80SgR\xf6\x1fy\xb3x\xed\xa8Wk\xf0\xf6\x1f\x93\xf3H\xf7\x90GJ\xbc\xbb \x9de\xef^\x9d*\xac\x1d\xaf\x9b\xa2\x00\xadw\r\x0f\xd52\x85\x02j\xa0\fݓ\x95\xf9a\x0f\x8b\xcaB\x9a\x9aKZ\x82\xfa(Ŏ%\xf2\b=\x9c\xfeW\xaf\xf3\x80a\v\xfcب\xf6\x11\xbf\xc9\xf7\xd3^$\x9d\x9e\x153\xf0XS\xa5\xe13\xe3Y\xe7\xed\xd7\xc1\x10\xe7\x9d\xed8ݻꦒ\x15\xd4@TD8\xc3\xc8\x01\xd8\xe1x\x8d\xb0\xb8+T\x91#a\x98\xec\xa30V#?z\x18\xd2\x06\xc0\xaa\xf3\x02\xe2\x9b\f8:\xa1\xe1&\xb4[Ak\x83\x17\x18\x90\xe0\x8dRȭ\xee7\xb9;{\xb5\xb1\av\x9c\xa2\xbe\x12\xd5\xd7QiC\xab\x84\x11=\xbc\xc37\x1c\x81o\xa3\xaa\xb2Sy\xd5}\xdc.\x16T\xa5\x98\x89\xeaX\f[\xae;\xb0\x1d\x18\xb4[-h(\t\x1cA\x10\xcbݔq(\xa7*\xaf\xbfa\xc4S\x1dA\xbd\xd3\x11\x0eւٓ\xf1h\xa82q\xe9\xe7\x87y'UE\xcd-)\xa9\x81\x95\x1d}\x99q\x93~xR\xa9\xf9\xe4\x14\xde7\xf2g\x04/\t!y9\xf7\xb7\x84*К\xee\x83\xe7\xf5\f\n\xc8\x1e\x84E\xf1\xd4\xdbz\xedE+/|cɠ\xc5\x16-LC\xfd\x04\xceȉi\xbb\x04H\xff`+:s\xfbQ\x91DŽ\x81\xfdY\xc2\xcc_\xf2z\x00\xaa\x87\x0f\xfc\x9e!\xe2s\xb7\xaf\x0fw:\x1c\xb8\xd7d\xa8\xab\xef\xc3\xf7`\r\x8b\x0e\xe6\x88\"\xb13/\x12\xcf\a)\x9f\xb2\xacПc\xc76\xda\u0084\xe3#\xbcڶ\x95\x8d\xe9ڗr,6\x8b\xef.\xbe\xb2\xbcF\x98w\xee\xae\xcbX\b\xf1\xfca\xc78 \xfas\xd2PNDSmA!C\xc6\x0e\x87\x89\xfbޏ\xe1uY\xceO7Cȃ\xe7\xa6[\xd8S\x10\x91\xf4^\x06t\xee\x00\x87\xd8\xd7\x00\x88\xeb>\xf1\xf4$\xc1\xb7\x82\x83\xca\x1f{\xecn\x8a\xa3#\x8e?\xe3\xa2r\x11\xecz\x8fa\xd7-\u07b91 \xd2^\x18\t\xb9\xe7p,.X\xfa\x84\xbdW\x1f\xa8\x9e3\xf4\xeem\x9fx\xf9\xb7\xa3\x93\xa2\x8d\xf70r$ӗ\x11W\xe4\v<'\xbe:da\x06'\xadIVd#\xee\x95\xdc+\xd0猳\xc2\xdbiL\xec?Kuϛ=\x13\xb1xxY\xe7{\xaa\f\xb3\xac\xec֓\x18\xfb1\xe8\xb2\xc4o\xf3\xa3G~\x98\x90Q\xb5\xdf\xf3l8\xd3u\x9b\x93O^\x80\xbeӭ\xbeH\x85\xfe<\xb45\xf9\"\r\x84,#\xeb\x03e\x9alA\x9b\x15\xecvR\x19\x17}^\xad\b\xdby;%\x15%\xa5\x8c\xa3\x97\xe4^綮S\xac\x8b\x8b6\xbb\x8f\x84)T\n\xe8^U\xf4\xe4\x02j\xb4(\xac\t\fﵡ)S\xfcEb\x14\xdd2\xcf\xcd9\x87|\xd3\xed\x1f\x83Q\xf1\x80#8\x87:\xbc\x15\xec4p\xb2\u0082\xe0\r\xd4Σ\x04D[mv\xc9q'N\xd0l\xc6]\xcc\xfeU\xaa\xd8yLN\xf9m\xf4\xde!\x1e\xaf\xa2f:\f\xb54+\x0eT\xec-\xfb(\xd9\xec\x0f\x81\x05\xc7\f\x95\x11\xa0e\x83\xd1\xea\x1aO\xaa\x0e\xd9z\xd3(\xd1I6\xf9\xfc}\xd9.w\n\xe8\xc5\x12S\xb5w\tZ\x991\xa1yS\xc6\xfe\xc8\xe0\x11\xfc\xa7B\xe1q\b\xd5'QL_pp\x9e\"\x9b\xb8\xc78\x85\x8c\xe4~\xa3\x04\xbcd\xbfqp\xfe~\xbbʻu%\x96l~|\xed\xf2\x19\xd3C\xec#\xde;\xccג\xc0\"\x8c\x89\x1d(GD\xe46\v\x13DYK&\f\xfeQp\x06b\x88~\xddl+f,\xdd\x7fk@[\x86\x96k\xf2\x01e\a\xd9\x02i\xea\x92\x1a(\xd7d#\xc8\aZ\x01\xff@5\xbc:\x01,\xa6\xf5\xca\"6\x8f\x04]\xb17\xec\xec\xb0\xd6\xf9!\b\xaf\x11z\xf9\xd3\xffPC\xd1;1v\x18\xdb\xf9cNvR\xf5\x84\x83\x1d\xb2\xee\x01M\x1fZ\xdb\xdc\xe9\xb7\x12l\xf8\xcb`)\xff\x19;Z\xfe\xb1\x8bh\x04\xfb\xad\x01\x14q\xee\xc4\u0099H9\x03I\xc2\xfa\x90-\xd6g\xbf\x8f\xe0\xd46\xf8^\xf0\xa6\x842J۳\xbd\fV\xfc\xf1l\x00j\x1dʄ\xe5\x7f+\xfe\xed\xb2E\xfb\xab\x15\xa7\x89\x15S\x05\xc4r \x13\x0e\x1ea\x027\x9bĴm\xcc@\x95X\xdc\xe4\xee\b\x11\r\xe7t\xcb\xe1\x86\x18\xd5\xc0\bf\xa8R\xf44\x82\x98\xa0\x82s\xf1\x12\xfb{\x81\xc0Y\x01]E\xe1P\xe3\x94\fU\xe7+\"?8V\x98\xb6\xe2,\xec\xf2NrV\x9cfQ\x93\x1a\x14\x8e\x9b?|\x81\x83\xb7p\xa0G&UbK\xf6Dڮ\x1dE\xda\nSie\x99\aR^\xb6\xe1$\xb2\x0eR>\xce\xd1\xfegۧ\x95ڤ@\xe3-n\xc5S\xdb+\xd1-\x10\xf8\x0eEc\x12\xcb$\xa4lP\x81HEj\xa9\xcd8\xdd\xc7e\x0fq\xe2`\x8ci\xc9\x14Ӝ\xed̋\xca@9\xbbўؔ\x02\xecZ+K\xb9\xb6\xaf\x92\x8d\xeb;\xd4o\x1d\x8c\xa71B\xb6TCI\xa4\xe7\xfa\x86\x83\xf6s\x95H\xfeV\xae\\\x8f\x82\x8e\x9bw\x96\x06\xa7[\xe0D\x03\x87\xc2Hu\x8e\xc9\x1c|\xba\x96#+G𘐚}\xf6o76\x01\x92X6\x7f:\xb0\xe2\xe0\x8c\x00˛\b\x87\x94\x124\n\x0ek\xa8\x9e\xc66I\xe6h\xef'\x99\x12\x1dm\x9b9SCx)qҶ\fq۶\x19\xc1{&X\xfc\xf7\xa4\xe6l\xdb?&b\x83&\xb9\x80i7gC_\x96iѩ\xb2\xc6\xfefG\xa0\xaa\xcd\xe9\x9a0\x13\xbe\xceA\xa4\x9cw\xe6\xff\x03\x13f9\xc7o\x86#_\x94\xe3'\xa92\a\xd1R%N\xff\a$\n*\x8b\a\xaf+\xb2\t\xf2\x97\xee\xa8k\xc2v\x91 \xe55\xd91n@\r(\xf3\xac\xf3\xf2\x12\xc8\xc8\xd1w\xb6U\xd4\x14\x87\x8f߭e\xa3\xdb8S&^\x86\x83\x9dI\x1c|\x84\xbeb\x9e\x81K\xd0}e\n*\xe7\x16\x7fEl\xb6_П\xb8\xfd\xfc\x13\x94S\xe8!y\x9cw\xb6\x91\xdb\xc1b\xbbS{;?w\x1b\xde\xf4\x89>\x93\vx\\\x13J\x1e\xe1\xe4,\x16*\x88%\x0e5h\xef&\xbd\xa7s\xe4`\xe4\x05\x99\xec\x11N\bƇRfG粂k\x8f\x900\xf7S\xad\x87@\xbb&\xef\xe0:L\xda\x0f\x88\bt\xbc\xf3\x91G0,\x16d\xd1\xfc\xe6H\xbe \t-\xe0\xfe\x82mF\xb2u\u0087HطڑȞ\x82\x03\xab37\x8a\xd1C\rxZB`\xec\x1b嬌\x139\xbe߈qk\xb8\xdf>K\xb3\x11\xd7\xce#\xd3\xc8%?IП\xa5\xc1/\xaf\x82N\xb7\xf0\v\x90\xe9\x06\xe2\xf1\x12Nl[\xf6\x15\x85\xb1\xb5\x00w\x06\x94\x0f\xfe9\x01\x1d<\x87g\xfaTs\xc1\xbd\x18أ1 k\x11<\xc3M.U\x92\xb3\xc4%֦\xc5\xcbB;\xfd\xe3\xf7NlҞl\xfbww#/m\r\x17\xb2\xaa\xe809\x98\xb5\xd4\x0fnd\xe0i\x0f\xc8Q_\xed\x1b<\xcf\xf9fb\xe0!L\v>1s`\x82\xd0 6@y\x86\xa2\xa4\x96\xf3\x12̵\x03\xd5d\v bL\xfdG\xd0\xf3\x15\x13\x1b\x9c\x80\xbc\x7fq\xbb\x80\xb4躈\x9c\x01Ց\xa0\xf1\x03j\xaa\\\x93J\x96\xe4\xe9\x00\nz\\q\x1e(\xb7\x96f&H!M7\x1ea\xe1ֲ|\xabɎ)m\xba\v\xcde\xb8F\xe7\xb2\xc3B\n\xdb\xdd}e\x15\xc8\xc6\\@\x83\x8f\xed\xe8^^\xb7\xa2\xdfY\xd5T\x84V\xb2\xc90\n\\\xb3\xfa\x85U1\xf9\xea)\xf0D\x99\x89y(\x8c\xcc\x18i\xa9Ts0\xb9$\xde\xc2Ί\xa3B\n\xcdJP\xa18\xc0Q\x96I{pw\x94\xf1&\x95\xf6I\xb5\xa5\xee\xad\xf8\xa8\xd4E\xde\xed\x177\xb2\x13m<ȧ>\x82\xb2Qp\xa0G lG\x98! \nK\x17PNd\xe3\x14\x1e\x19\x88\x9al\xb6\xcc\x13\xf0\xb6\x81h\xaa<\x04\xac\xf0d31\x19L\xebv\xffD\x19\x7f\r\xb2Y\xce\xfb$\xd5=\xd0\xf2\x92\x00̯\x9d\xe1\x04\x84n\x14&\xee\x9dxyb\x8fv\xd9!ζ9To\xa5\xe4@\x87\x05O\xa9fq}\xb9\x18\xfa\xb5\x1d\xfd\xbb\x88\xa1H\x81|sa\v\x9eT^\x16Qc\xa0\xaa\xddy\x93D5\xa2\xab}^A\n-\xf1\xc1\xfd*^ҹf\x82e\x10v\x90sa\xa6kYZ\x10\xafjY\xda\t\xa2QqI\xf8l\xd3\x03`OgpRp\xed\x91k\x16X\x99[ \xb4,\xa1t\x81Ik\xaax\x9fŕ\x97\x8d\x94*$w\xb7\xdcL̢lh=\x8f\x14C\xb1\xea\b\xabF<\n\xf9$V\xe8\xc9\xeb\xc5\x02$?4\xf8\xa2ӛ\x8b%\xd1\xef)\x85\xfa\xfc\x9a\xcfS\xc1xz\x05)\x93\xcd7\x8b\xa2!S\\0'\xd7\\\xe9\xf2ȏ\xb3\xab\x98\x9a\x7fb\xb0O4\x7fp5ǹ\xf5l\x9b\xf4\xa8\x8e\xf1\xf7t\x00s\x00\x15\x8a\x99WX\xb7\x9d\x92\xd3m>\xba\xf5cb\x81\x9b\xe5\x9f`\n\xbb\xc2\xcbA\xc9[\xdaѱV\xc0\xb5el\xdap\xe3ʏU\x93`\xa2\xac¯\xb4e\x90S91W/ѯ\x01\x8c\xf5\n\xa1\bP\x86I\x12;t\xb4t\x95\xbe\xddd|\xbf\xf0\x01C~a\xa5\x7f\xf7\xf2\xc0\x8c\x9a\x86\x99J\x86\xe9\xa2\xc9)|\x9d\xb3M\x17c-\x0f\xfa~\xbe\x9a\xf6\xc7B\x9f\x81\xeaK\xed\xcf\xc1\xa8\x01\xda\xc7`bȠ\x1c\x04%\xb7uٱ\xb0\x80\xb2\x94p\xb1\xa7Ї\x03-\xc4\xdb\x02O\xa2\f\x805\x86\x9a\xfdi\xf3\xd5\xedL\x93\x7f'\a\xd9$J\xea&\xb03S`1^V\xe1\x93\b`\xe8\xf1\xfd\xba\xff\x8b\x91\xbe\xc8\x02#_\x89ݡ\xa3\xd2FS\x99(ّ\x95\r\xe5\xbdC\xd6a\x8b\x96{\x88TD0\x9eʯZ\xb6\n\xe3{lD\xbe\xd4.ϲX\x1cM\x9b\x88y\xb5\x18\x17W`\xf4+,F\x94\xd4ҔC~\xa9i~\x8d\xc5tQĒʊa\xdd\xc4(\xd0\xf9z\x8a\x1c\xeb~\xa6v₊\x89\xccj\xb9g'Hrj\".\xaa\x84\x98-(ˬ\x7f\xe8W6L\x83\\P\xf5\x90\x85\x9c\xf9\n\x87\xc5u\r\xbe\x8e`r\x1f\xd9\xd5\f\x89:\x85I\xc0\xa35\fS\xd5\t\xd3(OT.\xe4\xd7$L\x82\xc6z\x85\xf9J\x84\x97\xab7|\t/`\\\xd4\xccV\x13<\xcbKȨ\x17XR%0\x8b\xb1\v+\x02b\xc6\x7fdޥu\x00\xfd<\xff\bМ\xec\xffHv\x7f\x04\xe2d\xce?7\xa7?\x02{F\xedNr\xc9\xe4\x8fKr\xf9\xd1\r\xf9\x85\xd65\x13\xfbs>\xc9\xe5\xa6IN:+\x04\xe8\xce\xd9c\xa5\xae\xb7\xd0\xf3\xb3RS\xba[\xb9\t\x9f,\x84\xf5\x980rMn\xc5\xe9\f.^\tH\xfa \xfdk[vYO\x8c\xf3\xee\xdd$\x04\xdb\x05\xe5o\xf9\xe9td\xc0v\x1c\xb3\xb0\x93$\x94\xaag\x1dϹ`_\x06ݻ\x81\xc2ik;eh3s\xb8\xd0ڮ\x1anX\x9d<\xf2\xb5\x92G\x86a\xc7\x03\x9c\">\xff*\xf1V\xd0\xf6\x84\x90\xbe\xdc\xc7Ӹ\x1e8\x0e4u\x86\x9e\x80sB\xf5\xf9\xf6\vw1\xb6\x90+\xbc\xebf)\x19\xf8\xc1_\xa0\xbd\xc6\x13\x9b\xf2\xd8E\xb8\xb2YY0x\xb9V'\"\"\xa3\xbah\xda\x1ev\xa6;~\xfb\xad\x01u\"\xf2\x889}o ͔\xdd;\xb9\xa2\xad\xff\x16$\x9d\x17\x97\xee:\xf6\xc0Oh\xe5\v\xb9\x15Nc'\xc1\x0eֈp\xac\x88k}#+ͭ\xdb3\xd25\tU\xc88:\xcd\x0f\x93\x8a)\xb7f\xfdu=\xa5\xe5\xbeҬ\x95\xf2*\xfe\xd2\xe5\x1e\xd3\x04\xc8\xdc\x1a\xf4\xbc\x9c\xc8l\xcd\xf9kyNs\xbeS\xb6јWS\xfe\x1a\xb5\xe4\vj\xc8\x17\xf8P˼\xa8l4\xe5Ԋ\xbf\x8a/\xf5\x8a\xde\xd4k\xf8S\x97yT3 \a5\xe09\xd5\xddYi\xbc\xec\x9cMN\x96m>s<]\xb5\x9dQ\xad\x9d\x91\r\x9a[iFU\xf6\xb2j\xec\f\x1c\xbe\x92\xaf\xf5J\xde\xd6k\xf8[\xaf\xebq\xcd\xfa\\\xb3\x9c3\xf3\xf3\xb2*ꋓ\f!\x1d\xfdY\x96p'\x95\x99s\x10\xee\x86\xfd\x13)\xc0\x8e\xd3$yID\xe8\x9a\xca4X\xdb\xdf\xdb\xfd\x97m*\x9d\xad\v\xe6\xef/\xb2\xb4k\x9b\xcb-\xdc\x0f\xba\x9f]\xa1݁\x02\xe1\x1e\x96\xf8\xef\x87/\x9f#\xfc\x94=\xea\x8d\xde\xc1\x9b\x06\xce\xc0(=r|\xf6\xc9\x17\xdc8l\xa1\x0e\x7f\xe1$\x01\xad\xd9\x7f\xe1\x9b]\xf3\x01\x99ۻ\rv\r\xd6\x12\xbe\xf5\x15\x13\xfa1\xf7\xb6\x05\xab=\"FF\xb9\x7f\xb3\xebAL\x94\x9d\xc6?\t\xbe\x98\x14\xb4\x17\x1b\xab\xc9rEH\xf6\xd4\xddm\xdc\xea\xd6\xe4\x935\xddĉH\xc7x\a\xa6\xcaUM\x959!w\xe8븆\xf1\xa0L\xd0!S\xa1\x93QQ{\xfe\x16T\x12\xb7\xe1I(L\xc0\x9d\xea~6s\x88\xd1K\xd61~{b\xf6\xde\xc4\v\xaec\\\x1d\xaf\x10S\x89\xcf\xc9\n\x88\x17\vIy1t\xf7mN\xac\xddǎ\xd3\xf2\xccz\xb2!\xac\x93\xc0\x8f\x1d\x8f\"M\vZ\xebC\"*\xf4<\x99\x86\x0fU\x19j\x9a\xcc\xfd\xb8\xbe\xbd-\xb1\xe2\xd0\x11@O\x10D\x94\xea<\xdb7X\x93=\xab\x0e\x10\xaaa4\xa1\x05\xe3\xd7\x1d\xc7\xfc\xf7Iyf>\vr\xf1\x83 \x0e=#\xa2\x02#MV\x8c\x05^h\xf1rA\xb2sք\xcb(l\x9d\xb6;3_\x94\xb8\xf8-\x89yd%\x105\xf6\x8cD\xceS\x11\x7fW|N\x88$]\x1c\xa0l8d<\xf0\xf6\xd0\xe9:\xff\xc4[\x00\x9c:\x93\xb2\xffț\xc5kG\xbdZ\x83\xb7\xff\x98\x9cG\xba\x87\xf0{\x86\x88Oݾ>\xdc\xe9p\xe0^\x93\xa1\xae\xbe\x0f߃5,:\x98#\x8a\xc4μH<\x1f\xa4|̲B\x7f\x8e\x1d\xdbh\v\x13\x8e\x8f\xf0j\xdbV6\xa6k_ʱ\xd8,\xbe\xbb\xf8\xc2\xf2\x1aa\u07ba\xbb.c!\xc4\xf3\x87\x1d\xe3\x80\xe8\xcfIC9\x11M\xb5\x05\x85\f\x19;\x1c&\xee{?\x84\xd7e9?]\x0f!\x0f\x9e\x9bnaOAD\xd2{\x19й\x03\x1cb_\x03 \xae\xfb\xc4ӓ\x04\xdf\n\x0e*\x7f챻)\x8e\x8e8\xfe\x84\x8b\xcaE\xb0\xeb=\x86]\xb7x\xe7ƀH{a$\xe4\x9eñ\xb8`\xe9\x13\xf6^}\xa0z\xceл\xb3}\xe2\xe5ߎN\x8a6\xde\xfdȑL_F\\\x91\xcf\xf0\x94\xf8ꐅ\x19\x9c\xb4&Y\x91\x8d\xb8Sr\xaf@\x9f3\xce\no\xa71\xb1\xff$\xd5\x1do\xf6L\xc4\xe2\xe1e\x9d\xef\xa82̲\xb2[Ob쇠\xcb\x12\xbf͏\x1e\xff\x81\t\xca\xd9\xdfRF\x7f\xf7ǹ\x19&\x84]\xed\x917\x1b\x17u\xdd\xe6\x04\x9d\x97\xc4ou\xabxR1D\x0fmM>K\x03!]\xc9\xfa@\x99&[\xd0f\x05\xbb\x9dTƅ\xb1W+\xc2v\xde\xe0I\x85[)\xe3\xe8n\xb9g\xbe\xad\x0f\x16\v\xec\xa2\xf1\xefCj\n\xb5\v\xfai\x15=\xb9\xc8\x1c-\nkK\xc3;mhʦ\x7f\x96aW\x15\xcf \xbf`\xcc\xe3O]\xaf\x18\xcfs\xa2\b/n\xb5\x90\x16\xf4\x8d*\xea2Pb\xc5r0\x99\x16\x95\xa5\x11\xdfYnk\xc3Ԏ\xd9=t\xfb\xc1\xf2\x8bQ\xf2\x96\xdb\xfd5[\x1b\xaa\xb7\xae\xf6܄_\x1d\x89\x1c\x00\xff\xc9\x1e\x117c\xb5\x90\x0fc\xbd\xbdg7ZI\x06\xdf*\r\x06Qf91P>\xb0\xa7=Hf\x15ӵ$T\xfe\x83g\x8fu5\x82H\x05\xd9z\x80\xa7Ǥ\xffq\x0e\x97\xfb=\xb0\x82\x1bˬ(\x81q\xdf!{\xe2\x86p\xd8)\xcd\xec^\x98y\x9a \x90\x1e\xb6\x0e\x9d\x9f\x86\x9f\x1dB9\xb7\xe0\xd1\xe9\x80\n»\xce4\x90\xdcދ\x12\x8c\xe5e\x1f\xe6\xfb\aH\x00F$\xaaxmH8\xdaַ\xddO\x0e\xc0V\xa9\x02\xb8\xbch+\x1d\xde9\xd9\xcb\xf6P\xf2k_YU \xdf\xdfn\xbe\xfe\xeb]\xef3\x1bȒ\xa7\x14\x13\x86q\xf6\x95&\x06\xd3~\xa62\xbb\xe7\x96i@\u0383\xb4X\xa3Ұ\n\xd4\xcd\x1b\x90\x8c)\xcd*\xd0B\xe5\"\v\\\xa1\xc6f\xaf\xea\"g[@\x06\xad\x9b\x06\x95V\x15h+\xc2\xd4s\xa5\xa3Q:_\a\x18\xbf\xc1A\xb9ZN\x12\xc1\x90\xf0\xf9\t\x05\xb9\xa7\x83\x9b\x1f´\xf8\x13\x93z\x80\x19V⒩\xed/\x90\xd95\xbb\x03\x8d`\x02֙\x92\a\xd0H\x81L=H\xf1?\rl\x83RoI\x18-x}\xd0\x16\x9a\xc0\x92\x17\xec\xc0\x8b\x1a\xae\x18\x979+\xf9\x91i\xc0^X-;\xf0\xa8\x8aY\xb3\x9f\x95\x06&\xe4N]\xb3\xbd\xb5\x95\xb9~\xfb\xf6AؠI3U\x96\xb5\x14\xf6\xf8\x96\x94\xa2\xd8\xd6Vi\xf36\x87\x03\x14o\x8dxXq\x9d텅\xcc\xd6\x1a\xde\xf2J\xac\buI\xdat]\xe6\xff\x148j\xde\xf4p=\x99o\xae\x90\"\x9c\xe0\x00jD'0\xae\xa9\x1bEKh\xfc\x84\xd4\xf9\xf2\xf1\xee\xbe+L\xc2\f\xa9Ot\xefHX\xcb\x02$\x98\x90;\xf03z\xa7UI0A\xe6\x95\x12\xd2\xd2\x1fY!@\x0e\xc9o\xeam),\xf2\xfd\xaf5\x18\x8b\xbcZ\xb3\x1b2/(\x87u\x8530_\xb3\x8dd7\xbc\x84\xe2\x86\x1bxq\x06 \xa5\xcd\n\t\x9bƂ\xaee\x1cVvT\xeb\xfc\x10\xcc[\x84_a\x8e\xdfU\x90\xf5\xa6\f\xb6\x13;\x91\xd1\xc4 \xed٨\x80\x81\x06ue|\xd6\xd2/\xa4\xa6\x86_\ax8]\x16z\x05\x83\xf6\xc3\xee\x89í\x19C\xb9r\xd0P\xa7H5\xe4\xee\x98\x16\xecP\xc2C\x99\xc1\xa4\xaf\xf5R\xed\xdb\tL\xe6U\xdd:\x82\xe3\tW\t\xc5GQm\xca\x12r\xc1-\x14\xc7\x19L\xdf\xdc\xf5\xab\x8fQO\x11L\xb6ujW\xecF\xf0\xecR7\xaf\x81\x89\x0eD\x9aZ\x7f\t5N-\xe4_\xc8\xdav\r[\xb7\x10\x8d\xba\xe0kٲO\xecz?IxZ\xb3͎Y\x8djq\xdb5\xb4=\x90\xa2(p\xa6\xe2\xa8*\xc8{\xc8ƻ\x13;&\xac\x1f\xdf\b\xd0-'\x9c$[;\xefg\xdd\xda\xfa\xc6n#\xca\x03|\x9d\xf6F\x8cF`\xa2\\p\xcb$|\xb3m;$\x16\x8dr\xc7\v\xd3\f\xd3\rʫ ?\xb0\x11\x88IC\xbdb\xdb\xda:\x80c\x18\x8c\x80mp\x82\xb2\xb2\xc7+\xd7v\xa7\x8aB=1C\n\x17m\xddN<\xd4\xda\xe9\x82?\xe4\xb0\xe3ua\xaf\xdd(\xfe\xb8~\xb3h\x1aZ(+4\x8d3\xc2}\xef\xab\xe1\xe0P\a\xe4Md\x10\x9c\xdb\xe0J(\xefA\xb0\x13\x03N\xdd\xed\x01u\xd3A\xe4\xdeB\x8f\x92!\xae\xbd\xb0dF\xdcI^\x99\xbd\xb2(\x0f\xaa\xb6c\xb5\x06\x03\xb8\xb9\xdb\f\x1au\xe6'bE~*\x89\xa7U쉋Sm\xe6\n\xeaޛ\xbb\r\xfb\x8an?\x04\x98\xcc\xcdEfk-\xc9\xe5\xf8\x02\xbe\xec\xd2\x16]\x17`<*\u0381m\xf5\xceU\xcb)\x17\xc6\x17|\v\x053P@f\x95\x8e\x93'E\b\\I՟\x11ʎh\xd2~\x104\xabDۂ\x01\xe6^d{\xe7n\xa2\x94\x11,\x96+0\xa41xU\x15\x11+ԖY\xc9\xf0\x9d\xcd)\x8d\xb6$\xa8\x8f!ܘ\"iK\xa2\x0enˌ6\xeeS\xbd\x11\x9bW\xa2\xf7Д\xdf%웓\xe6\xcf/\xecHn\x01\xa6\xbb\xd6%l\xf8\x9a\x02\xb5\xe7\a\x9a\x7f0Ɲ7[6\xc3\xd6\xcf>[\x9e\x85k\r\x1a\xff L#cu\xe7m\xd5\"\x86\xfd\xd4myE\x8b\xf0\x9ea\xf9\x15ۉ\xc2\x02\xf9Rs\x88v\x1c\x9dY\xce='\x81Rm/\x96\x92\xdbl\xff\xb1ٺIh1\xa0\xd5\x10\x80\xf3\xcbC\fC)\x8b\xff|\xfc&\f\xa2(s\xf6A\x81\xf9\xa4,}yQ\x12\xbbA\x9cI`ט\xa6\xa5tf\x01鲨\xff\x16\a\xb7\xbd\xba\x87\x96m°\x8d\xc4\xf8\xcc\xd1g\t\x9b\xf6\x10\x90sh\x95\xb5\xa1\xedK\xa9\xe4\xca-i\xf9\xde\x16\x00\xed\xe2\xe5Y\xa5t\x8fSW\v!\x8e\xa2\xe8ѻGk\xe5~9\xc9=\x99*\x1a\xaa\x82g\x90\x87]6Jt\xe1\x16\x1eD\xc6J\xd0\x0f\xc0*\xb4\x1b\xe9B\xb5@\x93\xbbr\x86\x14\xa6\xbb\x16\xa1x\xb30\x92\xb71VV8\xeb\x13k\x066'U\x8fd\xb5LWO\x1b%\x99w\U000874a8\xdfM\xc3\\fY\x16\xf2\xeb\xd4\aqH:\xf7\xa3\xe4\xb4\xf1\xf474\xaf$\xde\x7fO\xb3\x86\\h\xb3f\xef)\t\xb5\x80n\xfb\xb0J\xd8\xe9*\t$b\"\fC99\xf0\x02\xdd\aTޒA\xe1\x9c\t\xb5;\xf1\xa0\xd2T\xcc\xd3^\x19g\U000db371\xcbG8\xfa\xcdٮ\x96\xb8\xdc\xc8\xe8\xaa}\xbf\xa0\xce?QZ\x8dעdqd\x97\xf4\xdb%9fK\xa6\xc8\x19\xce\xdb\x02\xa9^P\xf5\xdb\xea\xb1ނ\x96`\xc1\xacJ^\xad\xfcl\xb0\xaa\x8c\xeeq\xbaB\xa9\xa2K\xc2\b\x8cӃǃ\x8d\x9b\x84Jt\xff\xe7(\x90<\x1f*e\"\x99\x16\x11\xb4n\x95\xb1n\xf1\xb0窏\xac.\xa6D\x8e~ő\xf1\x9d\x05͌U:$/\xa2\xca\x1e,\xae\xa3Ԙy\xb9q\xfbD~%\xd3\x01\xc6\x00\xf5\xb2\xd5.\xce\x1e\\\xba\xbd*\xfc\xff<̌\x1c-\x82]i\x95\x81\x89f#\xb4%\xd1\xea\xcc,\xf66\v\xbd\xdc\x05~\xbb$\xb5\x9e\xb2\f\x1d\xca27\x1eI{FP\xf4\xf1[g\xcd\x1aU\x18\xfe\x9d\"\xca\xe7\xe0\xc8\xe8\xfcBY\xf2a\"m2\xba7\xaeu\x98\x80\x1e\x98\v\xb6\xf4CM\ni\x99\xcf\xedE\xf2\xb7洔Bn\xa8#\xf6\xee\xc5\x1c\x1d\x16\xcc@,#i\xac\f\xd8\xe1۷\fi>\xa4ƾ,\xa4\xaa)\xda\xe7\xd1\xd0\xe3\xec\xe9.H:\xa7\x18:\xe2R\xd9\xeeB\x8f\xef\xe9\x8da;\xa1\x8dm\x11^\x00U\x98\x89\xac\xa7\xd1\xe1\x9d\x11\x9fʏZ\x9f\x1d\x9e~v\xad;K\x92{\xf5䓘\x97\x04\xe5\x81\xf8{~\x00\x9fJ\n2S\xb5\xa4\xc52T\x17\xd8\xcd\x02\x88\x8e\x89Θ$\xda\xccNcY\x97\xe9\x04Y\x91t\n9\xbb\xb2\xd6m\xf2#\x17i+[\xec<\xb6ک$ʱ\xd2\xcf\f\xf5ٔ\xddl\xf5\x92\x7f\x13e]2^\"[\x96Ĝ;\x97\x87\x19R\xdb\x1d\xaf\x9f\xb8\xb0\xfeĐ۔]\xa6M3UV\x05X\b\x19\x96\x99\x92F\xe4и\x0f\x9e\xff\xa3\xf9\xaa\xb1\xc2َ\x8b\xa2\xd6\vt\xf4b\xce,\x8d\xf9\xbczz\xfe@.\x1d\x91\x15\x113q\xc1~\x81\xc3=o?*\xbd\xcce\xbe\xd5\xf0\xfc\xaei\xa5\x85rY\xfe\xd3\xde\xe9,L\xf2^\xfbީ\x17^.\x8f1\xf7t\x16*a\xf2\xea\x9e6\xe5\xd5=}uO_\xdd\xd3AyuO_\xdd\xd3W\xf7t\xbc\xbc\xba\xa7\x9d\xf2\xea\x9e&ۏ\x14\fW\xb4r;Q!\t\xab\xc4\xf4\x8d9\xb4g\xfa\xf2YJ\xfe,Ȓ\xec\xea\xcdxˑ\xb3@\x8bΐ\x98\x8e\xd1kҭqJ\x86\xc9\xe4\xceJ&x\xe1\xcfp\xd6& p\xf6Y\x9b\xcd$\x80g\xf0\x9d\xdf\x19\xf9\xae\xec\xd4\xe7?\a\xf3\x9c\xeb\x16ӹ\xa6I\x19\xa6Ik\x1b\xf38'\xe5\x90.\xcd\x1cM\xa2\xea\xd2,\xd1&\x03t\xa2\xe3\xa4\xdc\xd0Ӽϩ\xa1\xccf\x84Ƴ=\xa7\xc0\x8e\xe5\x81&\xe4xN\x80\xecf\x7f.v\x03f\xa5i\xb6\xc2\xd2\xdc\xcd\xf1;\x00C\x99\xb7\xceů!\xb3\xdfK&\xa5{NsJp\xf7y\xd0\x04\xa5%\xf8\x89c\x8ex\xc5\xfb^r\xfc\xf1%\x82\xab\xe7\b\xaf\x92\x1c\x91\x97\b\xb1^*\xc8Z\x1af-I\xdeH:\xbe\xf8\x12\xc1֒pk\x91Ϙ~<\xf1\xa5\x8e%\xbe@\xd8uvൈt\xa9\xc7\x0e\x17\x87_\t\xe3\x9b9fx\xe2\xa3%\x80\x8c\x1e/\x1c\x0f\xc1\x12 \x9e\x1c+\x9c\r\xc2R\xe6\xc10L\xfb\xeeC\x82ɉL\x8bv\xd3S\x93\x90\xd26\xba\xe7\x0f\xff%\x1e\xfaK\xdc\x06O\xc1>\xf1p\xdf\xf2C}\x89t>3<\x9b\xec:\xf1\xf0ޢ\x00\xed\xcc\x10m\x12\xe2\xd4a\xbd\xe9 mz\x01nxH\xef\fw\"A\xc2\x12\xaa,?h\xf7ݛ1J\xe7\xa0g\xf7\xb5\x96\x88\xf3\xac \x0f\xe2\xa8~\xff\x83\x1d\x9dp#*\xd6\xea\xee\x99\xc58\xaa\x9a{G2\xf6'!\xfdn=\nn\xc7'\xe9m\xbc\xb5\x0eS|_\xa7\xf5R\xfd\xe3\x01n\xc7\xce@\xc55\xed\xe3o\x8f.)Ȭ\xd9G\x9e\xed\x9b\x1e\" \xa9\xdf=7l\xa7t\xc9-\xbbl\xb6Bߺ\x0e\xf0\xef\xcb5c?\xaa&}\xa4s\xafX\x04\xaa\x11eU\x1c1bb\x97]0\xdf'8Q\x81\r\xf8ܪBd\x11\x0fq\xf4J:\xd7\xe0\xe4B\x1f\xba4/\xebdA\xc4b\x14l.\xfc\x85\x85\x83K\x85\xdd\xf5\xe2g.]\xf0J\xfc'\xbdՓ\xb66\xf6\xfevCՃT\xd1;?M\xf6\\#c[\x98V\xe8\xed\xc0\xc9\xf5\xe8B\x1d\xc9^m\xfe\x9c\x80H/e\x04?ë\xf1L\xa1\x16\xbb\xdd8,\xd7$X\\\x1e\x99\xf2o!\b\x9d\xaf*\xae\xa3\x9bz\xcc˃\xb9\xeaa\x18\xec\xf8\xdc\n֤Y;}\xf9\xa3[z4\x0f\x8f\x80\xd0f\xef\xb1\xeao\xa3\x13\xa5;\xf4\xfc\x1e\x9c\xa6\x0f.\xcf\x1eY~\x01\x9c\xa6]\xa6\x15Q1\xf2S4\x1d\xef\xd9W\x0f\x8d\xbfD\xfegu\x80\x0f\xd1U\xc4\xfe\xb3\x18\x83&#\tt\x01\xeaԵ\xe9m\xd6\\\xfc:\xebgȈ\v\xa8\xf8\x8b\xaf\x17\x8cϷ\x18\x7fK\x83\xee\xff\x0e\xb0'l\x1bN\xd9ۯ\x14n6\xea\xd2\xcfp\x1fL\x86\xa5\xc2\xc1U\xad\x11\x90\xb1\xa7D\x9e\x8bZVi\xfe\x00?)\xf7\xdaK\n\xb5\xfa-z\x0f\xfexg.d\x13\xfb\xb9\x16S\xe6~lC\x80\xed!\x83\x93\xeb\xe8\x11\xdb3o۷\xb6H\x18\xdc\xfd\xfdOn@\xf44\xc6\a\xff\xee\x05\xea]\x03H\xe90P\xd7h\x1b\xd7N{\xf5D\xf7\xb9w\x9f\xab\xe83\x1a\xfe\xd0{\xfa#L\x92\x14\xb5\xf6u\xbce\xc7[\xeeLשdA\xb5\x8b\xc2\xe2ƨL\x90\x83M\xbb#tj\xe4\xe5\ue71e\n\x94&\xe8X\x1b\xf8\xfc$A\x7f\t*\xd9ld쭍\xfe\xac9i\x18}g\xc3*r\xeb\a\xd5\xc7\xc29\xe9\td\xdc+-a\x9bG\x98\xe6\xfd\xa9S\xd2\xcd̫\xb8\x96\x1f\xf7IV\xe3\xcfᬚ\x17z.\x12(\xeb^\xa1\xe9\x03\x1e\x7f`\xcc=W\x93\xf1\xca\xd6:\xa8\x9cZ\xd3\x05\xdc\b\x04\xdc\xfd\xd4\xe7=1\xd6>H5\xc3\xcb\xf6a\xa7v\xe1{\xf6\xc9\xc8\x11\xfe5\x8f\x84E_\x14r!\xa3{\xd2q\x85\xf0\xcfc\xe7\xe8<@\x9c\xef\xdc;Q\t\xe3\xbd\vOe\x9d\x0e\xb8\x19\x06\x0e9\xf6\xf2\xd4K\x8e\x84\xae^\x9f\x19\xc3-\xd6i\xce.z\x91\xa1\x86\xe1\xca\xf6\xbb\x18\x13\xc6\x0f\xa3\xad\xd8'8\r>W\xec\xa3\xc4A\x9c\x12\xc0\x9d8\x83\x9c\x16\xfe\xc7\x1e\x8a\x9c\x1c\xe2\xa1iE\xc7\xfdF\xf4^_a\x0f\xaa\x0f\x92\x91\xe9\t\x9e\xa6\x8a;\xda7&\xa0\x7f\x10;\xb7+\x93\xe1\x98\xfexR#\xaa\x82'\xd5oL\xf5\x8e*\x87\x93\x8f\xf4\x9aX\xde\x11\x12\xefwv\xbf\xd4\xdb\xf6\x06}\xf6\xb7\xbf_\xfco\x00\x00\x00\xff\xff}\xef\x8a\xf1Mx\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/pkg/apis/velero/v1/restore_types.go b/pkg/apis/velero/v1/restore_types.go index 82ed0e660c..2ad74d16e1 100644 --- a/pkg/apis/velero/v1/restore_types.go +++ b/pkg/apis/velero/v1/restore_types.go @@ -249,7 +249,7 @@ type InitRestoreHook struct { // RestorePhase is a string representation of the lifecycle phase // of a Velero restore -// +kubebuilder:validation:Enum=New;FailedValidation;InProgress;WaitingForPluginOperations;WaitingForPluginOperationsPartiallyFailed;Completed;PartiallyFailed;Failed +// +kubebuilder:validation:Enum=New;FailedValidation;InProgress;WaitingForPluginOperations;WaitingForPluginOperationsPartiallyFailed;Completed;PartiallyFailed;Failed;Finalizing;FinalizingPartiallyFailed type RestorePhase string const ( @@ -277,6 +277,19 @@ const ( // ongoing. The restore is not complete yet. RestorePhaseWaitingForPluginOperationsPartiallyFailed RestorePhase = "WaitingForPluginOperationsPartiallyFailed" + // RestorePhaseFinalizing means the restore of + // Kubernetes resources and other async plugin operations were successful and + // other plugin operations are now complete, but the restore is awaiting + // the completion of wrap-up tasks before the restore process enters terminal phase. + RestorePhaseFinalizing RestorePhase = "Finalizing" + + // RestorePhaseFinalizingPartiallyFailed means the restore of + // Kubernetes resources and other async plugin operations were successful and + // other plugin operations are now complete, but one or more errors + // occurred during restore or async operation processing. The restore is awaiting + // the completion of wrap-up tasks before the restore process enters terminal phase. + RestorePhaseFinalizingPartiallyFailed RestorePhase = "FinalizingPartiallyFailed" + // RestorePhaseCompleted means the restore has run successfully // without errors. RestorePhaseCompleted RestorePhase = "Completed" diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index da135e3148..8a36d33c8e 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -694,6 +694,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string controller.RestoreOperations: {}, controller.Schedule: {}, controller.ServerStatusRequest: {}, + controller.RestoreFinalizer: {}, } if s.config.restoreOnly { @@ -983,6 +984,19 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string } } + if _, ok := enabledRuntimeControllers[controller.RestoreFinalizer]; ok { + if err := controller.NewRestoreFinalizerReconciler( + s.logger, + s.namespace, + s.mgr.GetClient(), + newPluginManager, + backupStoreGetter, + s.metrics, + ).SetupWithManager(s.mgr); err != nil { + s.logger.Fatal(err, "unable to create controller", "controller", controller.RestoreFinalizer) + } + } + s.logger.Info("Server starting...") if err := s.mgr.Start(s.ctx); err != nil { diff --git a/pkg/controller/constants.go b/pkg/controller/constants.go index 6c9c1c6b44..1a769cf51f 100644 --- a/pkg/controller/constants.go +++ b/pkg/controller/constants.go @@ -32,6 +32,7 @@ const ( RestoreOperations = "restore-operations" Schedule = "schedule" ServerStatusRequest = "server-status-request" + RestoreFinalizer = "restore-finalizer" ) // DisableableControllers is a list of controllers that can be disabled @@ -48,4 +49,5 @@ var DisableableControllers = []string{ RestoreOperations, Schedule, ServerStatusRequest, + RestoreFinalizer, } diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index bacd351eb5..a15db454b9 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -645,18 +645,16 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu r.logger.Debug("Restore WaitingForPluginOperationsPartiallyFailed") restore.Status.Phase = api.RestorePhaseWaitingForPluginOperationsPartiallyFailed } else { - r.logger.Debug("Restore partially failed") - restore.Status.Phase = api.RestorePhasePartiallyFailed - r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName) + r.logger.Debug("Restore FinalizingPartiallyFailed") + restore.Status.Phase = api.RestorePhaseFinalizingPartiallyFailed } } else { if inProgressOperations { r.logger.Debug("Restore WaitingForPluginOperations") restore.Status.Phase = api.RestorePhaseWaitingForPluginOperations } else { - r.logger.Debug("Restore completed") - restore.Status.Phase = api.RestorePhaseCompleted - r.metrics.RegisterRestoreSuccess(restore.Spec.ScheduleName) + r.logger.Debug("Restore Finalizing") + restore.Status.Phase = api.RestorePhaseFinalizing } } return nil diff --git a/pkg/controller/restore_finalizer_controller.go b/pkg/controller/restore_finalizer_controller.go new file mode 100644 index 0000000000..9e6c7b4cad --- /dev/null +++ b/pkg/controller/restore_finalizer_controller.go @@ -0,0 +1,213 @@ +/* +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 controller + +import ( + "context" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/clock" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/metrics" + "github.com/vmware-tanzu/velero/pkg/persistence" + "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" + kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" + "github.com/vmware-tanzu/velero/pkg/util/results" +) + +type restoreFinalizerReconciler struct { + client.Client + namespace string + logger logrus.FieldLogger + newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager + backupStoreGetter persistence.ObjectBackupStoreGetter + metrics *metrics.ServerMetrics + clock clock.WithTickerAndDelayedExecution +} + +func NewRestoreFinalizerReconciler( + logger logrus.FieldLogger, + namespace string, + client client.Client, + newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, + backupStoreGetter persistence.ObjectBackupStoreGetter, + metrics *metrics.ServerMetrics, +) *restoreFinalizerReconciler { + return &restoreFinalizerReconciler{ + Client: client, + logger: logger, + namespace: namespace, + newPluginManager: newPluginManager, + backupStoreGetter: backupStoreGetter, + metrics: metrics, + clock: &clock.RealClock{}, + } +} + +func (r *restoreFinalizerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&velerov1api.Restore{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=get;list;watch;update +// +kubebuilder:rbac:groups=velero.io,resources=restores/status,verbs=get +func (r *restoreFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.logger.WithField("restore finalizer", req.String()) + log.Debug("restoreFinalizerReconciler getting restore") + + original := &velerov1api.Restore{} + if err := r.Get(ctx, req.NamespacedName, original); err != nil { + if apierrors.IsNotFound(err) { + log.WithError(err).Error("restore not found") + return ctrl.Result{}, nil + } + return ctrl.Result{}, errors.Wrapf(err, "error getting restore %s", req.String()) + } + restore := original.DeepCopy() + log.Debugf("restore: %s", restore.Name) + + log = r.logger.WithFields( + logrus.Fields{ + "restore": req.String(), + }, + ) + + switch restore.Status.Phase { + case velerov1api.RestorePhaseFinalizing, velerov1api.RestorePhaseFinalizingPartiallyFailed: + default: + log.Debug("Restore is not awaiting finalization, skipping") + return ctrl.Result{}, nil + } + + info, err := fetchBackupInfoInternal(r.Client, r.namespace, restore.Spec.BackupName) + if err != nil { + if apierrors.IsNotFound(err) { + log.WithError(err).Error("not found backup, skip") + if err2 := r.finishProcessing(velerov1api.RestorePhasePartiallyFailed, restore, original); err2 != nil { + log.WithError(err2).Error("error updating restore's final status") + return ctrl.Result{}, errors.Wrap(err2, "error updating restore's final status") + } + return ctrl.Result{}, nil + } + log.WithError(err).Error("error getting backup info") + return ctrl.Result{}, errors.Wrap(err, "error getting backup info") + } + + pluginManager := r.newPluginManager(r.logger) + defer pluginManager.CleanupClients() + backupStore, err := r.backupStoreGetter.Get(info.location, pluginManager, r.logger) + if err != nil { + log.WithError(err).Error("error getting backup store") + return ctrl.Result{}, errors.Wrap(err, "error getting backup store") + } + + finalizerCtx := &finalizerContext{log: log} + warnings, errs := finalizerCtx.execute() + + warningCnt := len(warnings.Velero) + len(warnings.Cluster) + for _, w := range warnings.Namespaces { + warningCnt += len(w) + } + errCnt := len(errs.Velero) + len(errs.Cluster) + for _, e := range errs.Namespaces { + errCnt += len(e) + } + restore.Status.Warnings += warningCnt + restore.Status.Errors += errCnt + + if !errs.IsEmpty() { + restore.Status.Phase = velerov1api.RestorePhaseFinalizingPartiallyFailed + } + + if warningCnt > 0 || errCnt > 0 { + err := r.updateResults(backupStore, restore, &warnings, &errs) + if err != nil { + log.WithError(err).Error("error updating results") + return ctrl.Result{}, errors.Wrap(err, "error updating results") + } + } + + finalPhase := velerov1api.RestorePhaseCompleted + if restore.Status.Phase == velerov1api.RestorePhaseFinalizingPartiallyFailed { + finalPhase = velerov1api.RestorePhasePartiallyFailed + } + log.Infof("Marking restore %s", finalPhase) + + if err := r.finishProcessing(finalPhase, restore, original); err != nil { + log.WithError(err).Error("error updating restore's final status") + return ctrl.Result{}, errors.Wrap(err, "error updating restore's final status") + } + + return ctrl.Result{}, nil +} + +func (r *restoreFinalizerReconciler) updateResults(backupStore persistence.BackupStore, restore *velerov1api.Restore, newWarnings *results.Result, newErrs *results.Result) error { + originResults, err := backupStore.GetRestoreResults(restore.Name) + if err != nil { + return errors.Wrap(err, "error getting restore results") + } + warnings := originResults["warnings"] + errs := originResults["errors"] + warnings.Merge(newWarnings) + errs.Merge(newErrs) + + m := map[string]results.Result{ + "warnings": warnings, + "errors": errs, + } + if err := putResults(restore, m, backupStore); err != nil { + return errors.Wrap(err, "error putting restore results") + } + + return nil +} + +func (r *restoreFinalizerReconciler) finishProcessing(restorePhase velerov1api.RestorePhase, restore *velerov1api.Restore, original *velerov1api.Restore) error { + if restorePhase == velerov1api.RestorePhasePartiallyFailed { + restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed + r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName) + } else { + restore.Status.Phase = velerov1api.RestorePhaseCompleted + r.metrics.RegisterRestoreSuccess(restore.Spec.ScheduleName) + } + restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()} + + return kubeutil.PatchResource(original, restore, r.Client) +} + +// finalizerContext includes all the dependencies required by finalization tasks and +// a function execute() to orderly implement task logic. +type finalizerContext struct { + log logrus.FieldLogger +} + +func (ctx *finalizerContext) execute() (results.Result, results.Result) { //nolint:unparam //temporarily ignore the lint report: result 0 is always nil (unparam) + warnings, errs := results.Result{}, results.Result{} + + // implement finalization tasks + ctx.log.Debug("Starting running execute()") + + return warnings, errs +} diff --git a/pkg/controller/restore_finalizer_controller_test.go b/pkg/controller/restore_finalizer_controller_test.go new file mode 100644 index 0000000000..2e802ef164 --- /dev/null +++ b/pkg/controller/restore_finalizer_controller_test.go @@ -0,0 +1,204 @@ +/* +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 controller + +import ( + "context" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + testclocks "k8s.io/utils/clock/testing" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/stretchr/testify/mock" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/metrics" + persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks" + "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" + pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" + velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/results" +) + +func TestRestoreFinalizerReconcile(t *testing.T) { + defaultStorageLocation := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Result() + now, err := time.Parse(time.RFC1123Z, time.RFC1123Z) + require.NoError(t, err) + now = now.Local() + timestamp := metav1.NewTime(now) + assert.NotNil(t, timestamp) + + rfrTests := []struct { + name string + restore *velerov1api.Restore + backup *velerov1api.Backup + location *velerov1api.BackupStorageLocation + expectError bool + expectPhase velerov1api.RestorePhase + expectWarningsCnt int + expectErrsCnt int + statusCompare bool + expectedCompletedTime *metav1.Time + }{ + { + name: "Restore is not awaiting finalization, skip", + restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseInProgress).Result(), + expectError: false, + expectPhase: velerov1api.RestorePhaseInProgress, + statusCompare: false, + }, + { + name: "Upon completion of all finalization tasks in the 'FinalizingPartiallyFailed' phase, the restore process transit to the 'PartiallyFailed' phase.", + restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseFinalizingPartiallyFailed).Backup("backup-1").Result(), + backup: defaultBackup().StorageLocation("default").Result(), + location: defaultStorageLocation, + expectError: false, + expectPhase: velerov1api.RestorePhasePartiallyFailed, + statusCompare: true, + expectedCompletedTime: ×tamp, + expectWarningsCnt: 0, + expectErrsCnt: 0, + }, + { + name: "Upon completion of all finalization tasks in the 'Finalizing' phase, the restore process transit to the 'Completed' phase.", + restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseFinalizing).Backup("backup-1").Result(), + backup: defaultBackup().StorageLocation("default").Result(), + location: defaultStorageLocation, + expectError: false, + expectPhase: velerov1api.RestorePhaseCompleted, + statusCompare: true, + expectedCompletedTime: ×tamp, + expectWarningsCnt: 0, + expectErrsCnt: 0, + }, + { + name: "Backup not exist", + restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseFinalizing).Backup("backup-2").Result(), + expectError: false, + }, + { + name: "Restore not exist", + restore: builder.ForRestore("unknown", "restore-1").Phase(velerov1api.RestorePhaseFinalizing).Result(), + expectError: false, + statusCompare: false, + }, + } + + for _, test := range rfrTests { + t.Run(test.name, func(t *testing.T) { + if test.restore == nil { + return + } + + var ( + fakeClient = velerotest.NewFakeControllerRuntimeClientBuilder(t).Build() + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + backupStore = &persistencemocks.BackupStore{} + ) + + defer func() { + // reset defaultStorageLocation resourceVersion + defaultStorageLocation.ObjectMeta.ResourceVersion = "" + }() + + r := NewRestoreFinalizerReconciler( + logger, + velerov1api.DefaultNamespace, + fakeClient, + func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + NewFakeSingleObjectBackupStoreGetter(backupStore), + metrics.NewServerMetrics(), + ) + r.clock = testclocks.NewFakeClock(now) + + if test.restore != nil && test.restore.Namespace == velerov1api.DefaultNamespace { + require.NoError(t, r.Client.Create(context.Background(), test.restore)) + } + if test.backup != nil { + assert.NoError(t, r.Client.Create(context.Background(), test.backup)) + } + if test.location != nil { + require.NoError(t, r.Client.Create(context.Background(), test.location)) + } + + if test.restore != nil { + pluginManager.On("GetRestoreItemActionsV2").Return(nil, nil) + pluginManager.On("CleanupClients") + } + + _, err = r.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{ + Namespace: test.restore.Namespace, + Name: test.restore.Name, + }}) + + assert.Equal(t, test.expectError, err != nil) + if test.expectError { + return + } + + if test.statusCompare { + restoreAfter := velerov1api.Restore{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: test.restore.Namespace, + Name: test.restore.Name, + }, &restoreAfter) + + require.NoError(t, err) + + assert.Equal(t, test.expectPhase, restoreAfter.Status.Phase) + assert.Equal(t, test.expectErrsCnt, restoreAfter.Status.Errors) + assert.Equal(t, test.expectWarningsCnt, restoreAfter.Status.Warnings) + require.True(t, test.expectedCompletedTime.Equal(restoreAfter.Status.CompletionTimestamp)) + } + }) + } + +} + +func TestUpdateResult(t *testing.T) { + var ( + fakeClient = velerotest.NewFakeControllerRuntimeClientBuilder(t).Build() + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + backupStore = &persistencemocks.BackupStore{} + ) + + r := NewRestoreFinalizerReconciler( + logger, + velerov1api.DefaultNamespace, + fakeClient, + func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + NewFakeSingleObjectBackupStoreGetter(backupStore), + metrics.NewServerMetrics(), + ) + restore := builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result() + res := map[string]results.Result{"warnings": {}, "errors": {}} + + backupStore.On("GetRestoreResults", restore.Name).Return(res, nil) + backupStore.On("PutRestoreResults", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + err := r.updateResults(backupStore, restore, &results.Result{}, &results.Result{}) + require.NoError(t, err) +} diff --git a/pkg/controller/restore_operations_controller.go b/pkg/controller/restore_operations_controller.go index 417ef562bc..422135f6c3 100644 --- a/pkg/controller/restore_operations_controller.go +++ b/pkg/controller/restore_operations_controller.go @@ -128,10 +128,8 @@ func (r *restoreOperationsReconciler) Reconcile(ctx context.Context, req ctrl.Re info, err := r.fetchBackupInfo(restore.Spec.BackupName) if err != nil { - log.Warnf("Cannot check progress on Restore operations because backup info is unavailable %s; marking restore PartiallyFailed", err.Error()) - restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed - restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()} - r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName) + log.Warnf("Cannot check progress on Restore operations because backup info is unavailable %s; marking restore FinalizingPartiallyFailed", err.Error()) + restore.Status.Phase = velerov1api.RestorePhaseFinalizingPartiallyFailed err2 := r.updateRestoreAndOperationsJSON(ctx, original, restore, nil, &itemoperationmap.OperationsForRestore{ErrsSinceUpdate: []string{err.Error()}}, false, false) if err2 != nil { log.WithError(err2).Error("error updating Restore") @@ -178,15 +176,11 @@ func (r *restoreOperationsReconciler) Reconcile(ctx context.Context, req ctrl.Re // If the only changes are incremental progress, then no write is necessary, progress can remain in memory if !stillInProgress { if restore.Status.Phase == velerov1api.RestorePhaseWaitingForPluginOperations { - log.Infof("Marking restore %s completed", restore.Name) - restore.Status.Phase = velerov1api.RestorePhaseCompleted - restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()} - r.metrics.RegisterRestoreSuccess(restore.Spec.ScheduleName) + log.Infof("Marking restore %s Finalizing", restore.Name) + restore.Status.Phase = velerov1api.RestorePhaseFinalizing } else { log.Infof("Marking restore %s FinalizingPartiallyFailed", restore.Name) - restore.Status.Phase = velerov1api.RestorePhasePartiallyFailed - restore.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now()} - r.metrics.RegisterRestorePartialFailure(restore.Spec.ScheduleName) + restore.Status.Phase = velerov1api.RestorePhaseFinalizingPartiallyFailed } } err = r.updateRestoreAndOperationsJSON(ctx, original, restore, backupStore, operations, changes, completionChanges) @@ -216,8 +210,8 @@ func (r *restoreOperationsReconciler) updateRestoreAndOperationsJSON( removeIfComplete := true defer func() { // remove local operations list if complete - if removeIfComplete && (restore.Status.Phase == velerov1api.RestorePhaseCompleted || - restore.Status.Phase == velerov1api.RestorePhasePartiallyFailed) { + if removeIfComplete && (restore.Status.Phase == velerov1api.RestorePhaseFinalizing || + restore.Status.Phase == velerov1api.RestorePhaseFinalizingPartiallyFailed) { r.itemOperationsMap.DeleteOperationsForRestore(restore.Name) } else if changes { r.itemOperationsMap.PutOperationsForRestore(operations, restore.Name) @@ -226,8 +220,8 @@ func (r *restoreOperationsReconciler) updateRestoreAndOperationsJSON( // update restore and upload progress if errs or complete if len(operations.ErrsSinceUpdate) > 0 || - restore.Status.Phase == velerov1api.RestorePhaseCompleted || - restore.Status.Phase == velerov1api.RestorePhasePartiallyFailed { + restore.Status.Phase == velerov1api.RestorePhaseFinalizing || + restore.Status.Phase == velerov1api.RestorePhaseFinalizingPartiallyFailed { // update file store if backupStore != nil { if err := r.itemOperationsMap.UploadProgressAndPutOperationsForRestore(backupStore, operations, restore.Name); err != nil { diff --git a/pkg/controller/restore_operations_controller_test.go b/pkg/controller/restore_operations_controller_test.go index af1ac6dcd5..b4b70f03bb 100644 --- a/pkg/controller/restore_operations_controller_test.go +++ b/pkg/controller/restore_operations_controller_test.go @@ -94,7 +94,7 @@ func TestRestoreOperationsReconcile(t *testing.T) { backup: defaultBackup().StorageLocation("default").Result(), backupLocation: defaultBackupLocation, operationComplete: true, - expectPhase: velerov1api.RestorePhaseCompleted, + expectPhase: velerov1api.RestorePhaseFinalizing, restoreOperations: []*itemoperation.RestoreOperation{ { Spec: itemoperation.RestoreOperationSpec{ @@ -157,7 +157,7 @@ func TestRestoreOperationsReconcile(t *testing.T) { backupLocation: defaultBackupLocation, operationComplete: true, operationErr: "failed", - expectPhase: velerov1api.RestorePhasePartiallyFailed, + expectPhase: velerov1api.RestorePhaseFinalizingPartiallyFailed, restoreOperations: []*itemoperation.RestoreOperation{ { Spec: itemoperation.RestoreOperationSpec{ @@ -188,7 +188,7 @@ func TestRestoreOperationsReconcile(t *testing.T) { backup: defaultBackup().StorageLocation("default").Result(), backupLocation: defaultBackupLocation, operationComplete: true, - expectPhase: velerov1api.RestorePhasePartiallyFailed, + expectPhase: velerov1api.RestorePhaseFinalizingPartiallyFailed, restoreOperations: []*itemoperation.RestoreOperation{ { Spec: itemoperation.RestoreOperationSpec{ @@ -251,7 +251,7 @@ func TestRestoreOperationsReconcile(t *testing.T) { backupLocation: defaultBackupLocation, operationComplete: true, operationErr: "failed", - expectPhase: velerov1api.RestorePhasePartiallyFailed, + expectPhase: velerov1api.RestorePhaseFinalizingPartiallyFailed, restoreOperations: []*itemoperation.RestoreOperation{ { Spec: itemoperation.RestoreOperationSpec{ diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index dfa93408b8..6987a1520f 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -28,6 +28,8 @@ import ( "github.com/vmware-tanzu/velero/pkg/persistence" v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" volume "github.com/vmware-tanzu/velero/pkg/volume" + "github.com/vmware-tanzu/velero/pkg/util/results" + ) @@ -336,6 +338,29 @@ func (_m *BackupStore) GetBackupVolumeInfos(name string) ([]*internalVolume.Volu return r0, r1 } +// GetRestoreResults provides a mock function with given fields: name +func (_m *BackupStore) GetRestoreResults(name string) (map[string]results.Result, error) { + ret := _m.Called(name) + + r0 := make(map[string]results.Result) + if rf, ok := ret.Get(0).(func(string) map[string]results.Result); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]results.Result) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // IsValid provides a mock function with given fields: func (_m *BackupStore) IsValid() error { ret := _m.Called() diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 539daafce6..71dbac976e 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -36,6 +36,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/itemoperation" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/util" + "github.com/vmware-tanzu/velero/pkg/util/results" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -75,6 +76,7 @@ type BackupStore interface { GetCSIVolumeSnapshotContents(name string) ([]*snapshotv1api.VolumeSnapshotContent, error) GetCSIVolumeSnapshotClasses(name string) ([]*snapshotv1api.VolumeSnapshotClass, error) GetBackupVolumeInfos(name string) ([]*internalVolume.VolumeInfo, error) + GetRestoreResults(name string) (map[string]results.Result, error) // BackupExists checks if the backup metadata file exists in object storage. BackupExists(bucket, backupName string) (bool, error) @@ -514,6 +516,25 @@ func (s *objectBackupStore) GetBackupVolumeInfos(name string) ([]*internalVolume return volumeInfos, nil } +func (s *objectBackupStore) GetRestoreResults(name string) (map[string]results.Result, error) { + results := make(map[string]results.Result) + + res, err := tryGet(s.objectStore, s.bucket, s.layout.getRestoreResultsKey(name)) + if err != nil { + return results, err + } + if res == nil { + return results, nil + } + defer res.Close() + + if err := decode(res, &results); err != nil { + return results, err + } + + return results, nil +} + func (s *objectBackupStore) GetBackupContents(name string) (io.ReadCloser, error) { return s.objectStore.GetObject(s.bucket, s.layout.getBackupContentsKey(name)) } diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index e94237b530..f98dcef763 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -43,6 +43,7 @@ import ( providermocks "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/encode" + "github.com/vmware-tanzu/velero/pkg/util/results" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -1146,6 +1147,35 @@ func TestGetBackupVolumeInfos(t *testing.T) { }) } } +func TestGetRestoreResults(t *testing.T) { + harness := newObjectBackupStoreTestHarness("test-bucket", "") + + // file not found should not error + _, err := harness.GetRestoreResults("test-restore") + assert.NoError(t, err) + + // file containing invalid data should error + harness.objectStore.PutObject(harness.bucket, "restores/test-restore/restore-test-restore-results.gz", newStringReadSeeker("foo")) + _, err = harness.GetRestoreResults("test-restore") + assert.NotNil(t, err) + + // file containing gzipped json data should return correctly + contents := map[string]results.Result{ + "warnings": {Cluster: []string{"cluster warning"}}, + "errors": {Namespaces: map[string][]string{"test-ns": {"namespace error"}}}, + } + obj := new(bytes.Buffer) + gzw := gzip.NewWriter(obj) + + require.NoError(t, json.NewEncoder(gzw).Encode(contents)) + require.NoError(t, gzw.Close()) + require.NoError(t, harness.objectStore.PutObject(harness.bucket, "restores/test-restore/restore-test-restore-results.gz", obj)) + res, err := harness.GetRestoreResults("test-restore") + + assert.NoError(t, err) + assert.EqualValues(t, contents["warnings"], res["warnings"]) + assert.EqualValues(t, contents["errors"], res["errors"]) +} func encodeToBytes(obj runtime.Object) []byte { res, err := encode.Encode(obj, "json")