diff --git a/changelogs/unreleased/5867-ywk253100 b/changelogs/unreleased/5867-ywk253100 new file mode 100644 index 0000000000..3ce063a4ad --- /dev/null +++ b/changelogs/unreleased/5867-ywk253100 @@ -0,0 +1 @@ +Add restored resource list in the restore describe command \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_downloadrequests.yaml b/config/crd/v1/bases/velero.io_downloadrequests.yaml index 49e7f4d4b4..4c58a9d68e 100644 --- a/config/crd/v1/bases/velero.io_downloadrequests.yaml +++ b/config/crd/v1/bases/velero.io_downloadrequests.yaml @@ -50,6 +50,7 @@ spec: - BackupResourceList - RestoreLog - RestoreResults + - RestoreResourceList - RestoreItemOperations - CSIBackupVolumeSnapshots - CSIBackupVolumeSnapshotContents diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 99e05ae2e5..3809146e70 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -33,7 +33,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms\xdc:rw\xfd\x8a.\xe5\xe0\xdd*\xcd\xe8\xb9rHJ7?Y\xae\xa8ދ\xad\xb2\xb4\xdaC\x92\x03\x86\xec\x99\xc1\x13\bp\x01p\xe4I*\xff=\xd5\x00\xf8\r\x92\x18Y\xda}/e\\l\x91@\x03\xe8n\xf4\x17\x9a=g\xab\xd5ꌕ\xfc\x11\xb5\xe1J^\x01+9~\xb3(\xe9/\xb3~\xfaW\xb3\xe6\xea\xf2\xf0\xfe\xec\x89\xcb\xfc\n\xae+cU\xf1\x15\x8d\xaat\x86\x1fq\xcb%\xb7\\ɳ\x02-˙eWg\x00LJe\x19=6\xf4'@\xa6\xa4\xd5J\bԫ\x1d\xca\xf5S\xb5\xc1M\xc5E\x8e\xda\x01\xaf\xa7>\xfc\xb4\xfe\x97\xf5Og\x00\x99F7\xfc\x81\x17h,+\xca+\x90\x95\x10g\x00\x92\x15x\x05\x1b\x96=U\xa5Y\x1fP\xa0Vk\xae\xceL\x89\x19͵Ӫ*\xaf\xa0}ᇄu\xf8=\xfc\xecF\xbb\a\x82\x1b\xfbK\xe7\xe1\xaf\xdcX\xf7\xa2\x14\x95f\xa2\x99\xc9=3\\\xee*\xc1t\xfd\xf4\f\xc0d\xaa\xc4+\xf8LS\x94,\xc3\xfc\f l\xc7M\xb9\n\v>\xbc\xf7\x10\xb2=\x16̯\x05@\x95(?\xdc\xdd>\xfe\xf3}\xef1@\x8e&Ӽ\xb4\x0e)~a\xc0\r0xt\xdb\x02\x1d\xd0\x0fv\xcf,h,5\x1a\x94ր\xdd#d\xac\xb4\x95FP[\xf8\xa5ڠ\x96h\xd14\xa0\x012Q\x19\x8b\x1a\x8ce\x16\x81Y`P*.-p\t\x96\x17\b\u007f\xfapw\vj\xf3\x1bf\xd6\x00\x9390cTƙ\xc5\x1c\x0eJT\x05\xfa\xb1\u007f^7PK\xadJԖ\xd7x\xf6\xad\xc3U\x9d\xa7\x83\xed\xbd#\f\xf8^\x90\x13;\xa1\xdfF\xc0\"\xe6\x01i\xb4\x1f\xbb\xe7\xa6ݮ\xe3\x90\x1e`\xa0NL\x86ů\xe1\x1e5\x81\x01\xb3W\x95ȉ\v\x0f\xa8\ta\x99\xdaI\xfe\xdf\rl\x03V\xb9I\x05\xb3\x18\x18\xa0m\\ZԒ\t80Q\xe1\x85CI\xc1\x8e\xa0\x91f\x81Jv\xe0\xb9.f\r\xff\xae4\x02\x97[u\x05{kKsuy\xb9\xe3\xb6>M\x99*\x8aJr{\xbct\a\x83o*\xab\xb4\xb9\xcc\xf1\x80\xe2\xd2\xf0݊\xe9l\xcf-fD\xc8KV\xf2\x95[\xbat'j]\xe4\xffT3\x80y\xd7[\xab=\x123\x1a\xab\xb9\xdcu^8\xae\x9f\xa1\x00\x1d\x00\xcf_~\xa8\xdfE\x8bhzD\xd8\xf9zs\xff\xd0\xe5=n\x86\xd8wx\xef0dK\x02B\x18\x97[Ԟ\x88[\xad\n\a\x13e\xee\xb9ϱ\xae\xe0(\x87\xe87զ\xe0\x96\xe8\xfe\xb7\n\r1\xb9Zõ\x131\xb0A\xa8ʜ8s\r\xb7\x12\xaeY\x81\xe2\x9a\x19|s\x02\x10\xa6͊\x10\x9bF\x82\xaet\x1cv\xf6X뼨e\xd9\x04\xbd\xbc@\xb8/1\xeb\x1d\x18\x1aŷ\xd8V$K\xc6x\x06 ^\x9e\xe4\x01.\x8dE\x96\xaf\xcf_\x93@\xf8-\x13U\x8eyc\xb6\x8cd\xc0\x8087\xa3\x01Τc\\\x92\xd6 #\x8a\x90+۷d\x98D\xb6\xca4\x02\xc9m.=\x97\xf3\xeeGZ.͋3h\xfa\x192\x13\"\xfa\xd4+\xa3\xf4D\xe1\xf4\x1c\x99\xf9\xa4\x96S2c\x86y/\x93@\x97\xf3aR<Džܗ\x17d\xbc$f;~\xf7\xc5XJNˋ2Y\x16\x13\x02\x13\xf3W\xfa\x99)\xf3 O\xc8ZIB\xcer\x86\xca\xc9y)!\x0fdv\x1f\xc9\xd9(\x91<\x93Y\xc0\x939(s\xd9%\vQ\xa9q\xe6IzN\xc9,h\x97o\xb2\x9cI\xf2z\xf9\xa2\xafa\x03O\x8b\x9a\xc5l\x90E\x1by~}\x8b\xf9\x1e\xa7dy,b\xec\x85\x19\x1dM\xc6\xc6ļ\xa7\xe6q\xf4\xf34&\x80\xa6doLdgL@\x9c\xcd\xd9H\xcdɘ\x80\xbd\xa0vg\xb9d\xe6e\xfcC`X\xd4o\xe2\xef\xc5Q/ݘ\xd2=sq\xc9B\xff2\xe8N\xb4\xac\xad\xa6y\xf33fyr\xbb?\xdd\xfc,*ay)\\8\xff\xc0\xf3\xa8\xd3h\xf7xl>\xeb\xfcM\xb9Ϝ6G\a\xe9\xcb׆=\xd7\x03#\x9a\x19xF!\x80Řk\xb4\xf3\xcc\u007f˞\xa9\x15\x92̧\x03\x17>X\r\x9f\xbc_x\x0ev_r\xc5\"\x9ev\x8f\x05A\xa9\xbf|=\xc1\xfd\x987\x10\xbd-\xeb\x9e\xfd\xadB}\x04u@\xddZ\f\v\xdf\x11\xf8\x83f*\xd1&n\x05\xf9\xe1+(\f\f\xe7\xf6\xc0\xc1\a\xe9UX\x14\xec`\x8d\x0e\x0e\x9dy\xd1К\xc4\x1b\xf9\x01\x13]\xe3\x81\x0fՌ\x8e\xbc_\xb2=S\x93\xf0\xdf\xd6u8\xddyXT\xdbo\xe2@\xbc܅\x98\x01\x99\x9aT\x9fv\x01\xb5\x98D\xffV\xaeĒ3\x91lE\xa5%ɿEr\xfc\tI\xf1'8\x15\xa7\xb9\x15\xc9hJI~\u007f\x13\xe7\xe2\r\u074b\xb7p0^\xe6b,\x80\x1c$\xb5\xa7\xa4\xab']\xae&\xdf/\xa4\\\x8e._\x01̧\xa1'\xa4\x9f'\\\x0e,\xad4!\xcd\xfc\xb4\xf4\xf2\x04\x1c\xbe\x91\xf3\xf1F\xee\xc7[8 o\xeb\x82,:!\x8b\x9c3\xfb\xfa\xc5\xd1e\xa5sԳ\xc1\xf8TV\x9be\xb2\x81\xbfПs\xf0Em]\xe1\x85z\xf5L\xd3XH\xb9\xf9\xfa3\x83_\xb8\xcc==\x88\xa9:z\xbcwC\xd0\x1a\x16\xf1\x04\x81\xd6j\v\x15\xb0\xfc\xb5\x82\xc1\x92iW6ms\xf4W\x93f\r7,\xdb\x0f\xa0\xef\xa3~\xc2V\xe9\x82Y8o\xeed.=p\xfa\xfb|\r\xf0I5\x97^݊\n\x86\x17\xa58\x92\x1f\x10\x81y\xde\x05\xf12\x86\x882\x93\t\xe5\x9aB\xfd\x9a\x05\xdf\xef\xbe\xdf;r\x99W\x97\xee\xa9ᚸ\xe3\xc3\xe4\x11\xee\x1e\x9du\xe2\n\x86dm\xf1\x94`\u007f\xd4\xde߰\xb6\xcaϯ\u007f\xadg\xac\xd2l\x87\xbf*_\x81k\t\a\xfd\u07bd\xf2kAj\xd4\xd7\xec\xf5W\x181m\x1aj\x81\r\x80\xb5\xd93\xa3:P\xb4ʘ8\x999\x89֊\x85\xcd<<\xfc\xea7`y\x81돕\xbfB]\x95L\x1b$l\xd6\x1b\xf3\x836\xf4߽z\x8eE9T\xd8\xf3\xcf\xc3ukt\x19:\xee\xa6\xf6\xa4\xd5\x1fz\xf5\xc4j\x14-\xb1\xe8c|T\xc7E\xeb\x10ɟ\xf6(\x87N\xc1\xe9\x94Tt\xc1\v\xf7\x85\xcd\xeb\x16\xfc\x99\x92\xdfSE\xe7\\\xa1\xb5\xe5\xb2s\xbe\x1e[(2\x19\xf2\xbc*\xed\xaa\xf5\x84Zm\xae\xba\xcd\xcb*\xcf\xf9\xb4\x94^\xe1\xcfy:]\x8fG\xb8\xf2\x8e:\xefT\x9ek\n\x80=3Ӥ\xbeDUj\vΏt6-A\xc3\x1c\xf0\x80\x12\x94t\x99.\xae\x0e\x8e/A:\x1c\x13\x81څ\x12Ri\xaaR(\x96\xd7'\xbc\xd6^\xa1l僓_\xfa\x80\xfa\x9d\x99\x81ٔt\x8b a\xacP\xbc:\xb9\x82\x9cY\\E\x81&ɾ(\xb3e\x86\xf7\x19\xdd|\xb0\x96<\x84\x98\xd5<,\x1d85\xb2\xd6\xc4VY&@V\xc5ƫvVw\x88\xd1oT@Єܧ\x99\xe3\xe57ƥ\xc5\xdd(\xba8\xde\xd9u\xcd?'\xef\xac\x199\xb53Se\x19\x1a\xb3\xad\x84\x88\x19\xf9\r\xe7\xbe\xfe6]V\xdfb\xb53\xd7ɋ@\x97\x12X\x17\xd4\xf39\x81\x05\x1a\xc3vu\x99\xb3g\xd2@;\x94\xe8\f\xa0X\xe4ѻ\x88m\x0eY\xbfȗ\x8fe\xb1\xccV,LP\xe7\x00tz\xbd\x8b\xd9MB\xed|)D^\x17\x82\xadU\xf3\x898\xf9Vr\x9d\xa2\xcao\x9a\x8e\x84\x1b\x17\x86v\x84h\v\xf7\xa2\xe0;Nz\x90\x88\xb4cz\xc3v\xb8ʔ\x10\xe8\x12\xce\xc7\xebz\xcb\xc3\x1a2\xf5\xbe\"3\x8b[\xfb\xd4\xed\x1bb\x1e\x9eھF\x06\xf3\x85\x16]\x1dW\xcb5\xb6\x85\x91G\vRn\xe2\x93T\xb7\xc7B\xb4\x84\xf0x\xa5ݾ\xf5\x01\vr5Xҡ\xa2\xf0E0\x06\xe3~m\xc1~S\xfa\x02\n.\xe9\x1f\xb2\xfb]P\xa2\x1e|\xd2\xfa]\xf5\xba\x85u\xdfQ\x9f&a\xba\xa3H\xb1>\x10S\xa6jM\x92e\x19Y\xf5xi,\x13\x11A\xfa]iWξ%6\xc7\xfc/\x11\x83o\x84\xf0\xdbn\xff\xe6[\xf5F\x8d:p\x1es.\x8d\xdd+\x91\xa8J\x05\x97܌\x12\x9e5\xb7\x96\x04w\xf7\x96\x10,\x89j!\xc0\x90\xf0\x9a\xa8L8\xa7B\xdc{R\xf2\xb7\xd3Q˾#\xd5t\x9e\xb2\x11\xc2\xe6\x14\x91e\xe3P0\xb1-\xff\x99\x147\xf5X\"e\xb6grGL\xa5U\xb5\xdb\xd7|9\xa1\x82\xa7\x82~\x15-\nJw\xb0M}Cc+-;Q\x9fpg\x93w\x96˲\xa7ɕ\x86(t]\xb3\xff2\xd4\x1a\\m\xb5*V\x81\x16\xeeb\xe5\"Db4W\xe4h\xd8}\x14\xe5\xe0\x8b!\x87\xa2^\x8e\r\xca\x12%0\x13֓\xf0\r\xd7H\bvU\xb1\x9b.\xc1\xe0\x8c\xe0\xe3O|\xeb\xaf\xf12Z\xf5\x9f\xffቾ\x87$\xe3\xe8ݬ]\xe4L\x9e\xc6\xc0Y\xa8\x81}'\x90\f\x16\x83\xd87\xb9ޝd[\x1f^\xe6-\xbe\xa6\xabX\xff\x92\xc5\xeb8P\x87\x979\x89o\xe6!\xbe\xee\ue799\xab\x9e\xbft\xc6\xfe\x1a\xbaE\\\xc4\x00!\xe2$F\xb6Ѹ\x8d\x8bNb\xc7G\xac\xd78Q`{\xe07\xbe\x92\x97\x18\xd5\x03\xa3\x87N\x80杳\x1df\nO\xda\xc8\x1b\xcb2$v\xfd<\xfcŚs_\xfe\xbd\xfeQ\x1a\xf7g\xa6\xa4W\xb7\xe6\n\xfe\xe3\xbf\xce \x84v\x1f\xeb_\x9f\xa1\x87\xff\x17\x00\x00\xff\xffp,\xdd\xe3\xddg\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKo#\xb9\x11\xbe\xebW\x14f\x0f\xbe\x8cZ\xb3\xc9!\x81.\x81F\x93\x00\x83x\xd6\xc6\xc8q\x0eI\x80\xa5Ȓ\xc45\x9b\xec\xf0!\xad\x12\xe4\xbf\aŇ\xba\xd5ݲ\xe4A\xb2ˋ->\x8aU_\xbdٓ\xe9t:a\x8d|F\xeb\xa4\xd1s`\x8dğ=j\xfa媗\u07fbJ\x9a\xd9\xfe\xfbɋ\xd4b\x0e\xcb༩\xbf\xa23\xc1r\xfc\x84\x1b\xa9\xa5\x97FOj\xf4L0\xcf\xe6\x13\x00\xa6\xb5\xf1\x8c\xa6\x1d\xfd\x04\xe0F{k\x94B;ݢ\xae^\xc2\x1a\xd7A*\x816\x12/W\xef?T\xbf\xab>L\x00\xb8\xc5x\xfcI\xd6\xe8<\xab\x9b9\xe8\xa0\xd4\x04@\xb3\x1a\xe7\xb0f\xfc%4\xce\x1b˶\xa8\fOwU{ThM%\xcd\xc45\xc8\xe9\xea\xad5\xa1\x99C\xbb\x90(d\xb6\x92H\x1f#\xb1U\"v\x9f\x89\xc5u%\x9d\xff\xf3\xe5=\xf7\xd2\xf9\xb8\xafQ\xc12u\x89\xad\xb8\xc5\xed\x8c\xf5?\xb4WOa\xedTZ\x91z\x1b\x14\xb3\x17\x8eO\x00\x1c7\r\xce!\x9en\x18G1\x01ȘEjS`BD-0\xf5h\xa5\xf6h\x97F\x85Z\x9f\xee\x12踕\x8d\x8f('Y \v\x03E\x1ap\x9e\xf9\xe0\xc0\x05\xbe\x03\xe6`\xb1gR\xb1\xb5\xc2\xd9_4+\xffGz\x00?9\xa3\x1f\x99\xdf͡J\xa7\xaaf\xc7\\YM:z\xec\xcc\xf8#\t༕z;\xc6\xd2=s\xfe\x99))NZ\a\xe9\xc0\xef\x10\x14s\x1e\x8c\xb6e,\x1e?\x97ț\x1c(\xfb[ƪ\x82E\xf6\\\xb3\x81\x0f \xa4\xa3\x02\xc0E\xa2C\xb0\xa8<\xa3\xf59x\x1b\xde$>7z#\xb7C\xa1\xbb5\xcd%\x8b\xb9B\xba\x87\xdc2\xdeD\xa1\x89\xac\xa3\xb1f/\x05\xda)\xf9\x87\xdcH\x9e9\t6e\xae\x8dD%\xdcP\xd2\v^\x16E\xb1(ȫ\x99\xba\xa2\xc3\xe5ic,\x8d\x99\xd4ɂ[\x021\xd8\xd8:\xa7T\xedQ\x8bS5rƍ\x89Qˡ\x80\x83\xf4\xbb\x14\x0e\u0558\xdf\xc1\xab\xbeG\xe3\x05\x8fc\xd3=ޟvH;S\x02Ep\xc8-\xfahm\xa8\xc8|Ȕ*\x80/\xc1ŀڏ\x13e\xc4B\xad\x9c~\xc1\xe3\x10h\xb8\xa6\xdc\\\xc2\\g\xf9\x8eJ\xe7°\xc5\rZ\xd4~4\xa8Sgb5z\x8cq]\x18\xee(\xa4sl\xbc\x9b\x99=ڽ\xc4\xc3\xec`\xec\x8b\xd4\xdb)\x01>\xcd\x1e4\x8bm\xc5\xec\xbb\xf8\xe7\x82\xc8O\x0f\x9f\x1e\xe6\xb0\x10\x02\x8cߡ%\xadm\x82*\x86֩o\xde\xc7\x1c\xfb\x1e\x82\x14\u007f\xb8\xfb\x16\\L\x93<\xe7\x06lV\xd1\xfa\x8fT\xa8E\xa6\b\xa2UҊ\xb1@\x99\x92\x94]gm\xa6X3f\x88c\x15fwP`\xa2\f2\x16Q_p\x18L_q\xb3\\\xec^\xf1\xb1RHK-$\xa7B\xec\xdc7J\x83!\xce\xea\xed\x11\xc1\xfa\x15\xf8\xa5\x880.x\x12 \xe7\xc3+\x1c?t\xf7\xb6mY\nO9\xc79\xf4T@9\xd0H9\x90\xd9!r1(p\xa35y\xa37\xc0N\xa1\xee\xce\xf5c\xfc\x1b#\xc4:\xf0\x17\x1c\x01~ \xcaǸ\xb1`\x9c\x8e\x11/\xc1a\f\xbe\xd7\u0600\xeb6\xce\xd9\x12\xed-\xbc,\x17\xb4\xf1\x94&\x19,\x17\xb0\x0eZ(,\x1c\x1dv\xa8\xa9C\x90\x9b\xe3\xf8]4\x9e\xeeW\x05\xd5Xa\xe4\x1a\xbf`;.C\x8a\xe1sX\x1fGj\x82\x1b\x84l,n\xe4\xcf7\b\xf9\x187\x16\xc0\x1b\xe6w \xb5\x93\x02\x81\x8d\xc0\x9f\x8a\xb5\v\x82\x9e\xf2\xffC\x8e\"ߠ\x9e\u05fc=\xb1\xf3\x16\x87/\x18_\xf1\x9fǼ\xed\x84B\xf9\x9d#\xffy-xɏG%ڟ\x1e\f\xfe\x94*,>\x92*Ϙy\x1e\x9ex\xa5R+\xcf\x16c\xceLu\x81\xb1\x16]c\xb4\xa0\xe6\xe9\xb6:\xade\xf9\u007fW\xad\x8d\xabuz\x1e\xe5zkE\v7\xb5*\xf1\x89\xe6\xcd\xcdJz\xb8\xea\xb6\x02f\xed\xa8Sl\xfb\x95\x9e\x8c\xbfH\x9b\xf2\xaeӧP?\xac!\xe8X\xa9Ō_\xc1\xdf5|\xa2ޖ\xb2\x93\x98\x13\xdfv\xcc\x00\xa4\x03m\x0et\xbcC/\x92\x00\xa3S\xbe\xa6n\x8di\x91\x9b\xe1\xb8t\x90JQƶX\x9b\xfdhƦBӢ:\x02sd:\xfb\xdfT\x1f\xaaw\xbfZ\x17\xa4\x98\xf3\xd4Ԡ\xf8\x8a{9|\xe5\x19\xa2{?8Q\x1c\xff\xe4\x0e\xf4\xe3\xc7\xd2,\xcfl\xde\xf6\xe3\b\x18\x1b\xa9\xa8\x16\x1c\x89\x13m\xc50|\x8f\xfc\xb8\xba\xbfs\xb1\x84G\xed\xc7ʾ\x03Z\x8c\x1d\x13\n\xaa\xe2M~\x97\bΣ\x1d1\x80\x93\xf6\xa2\xceA\x19\xbd\xed9N\x1a\xf9\x95\x82*\xb4dPƂ@O\xa9Io\x81\xef\x98\xdeb\xfb\n\x95\xf9\u007f\x9dS2\x9f\x9eʹ\x16\"\xf5%\xf3\xb8I\xa3Or\xacL\x1f\xbc\x00\xb7\x9b\xc7_\u007f\v\xf7E\xb3\x17ۜ+\xb8\x0f\xf6\x97,M\xa0N}\xfb\"\u070eooo\x87\xcf\xcd7 \xf1ַ\xf0W\xde5\xe0\xc0\\\xfb*\xfe\xeb\xe1PS\xb5z\xb5\x04\xfe\x92v\xa5\xe7\xc3|\x04\xd8\xda\x04\xff\x9agލ\x19t~\xee\u007f\v\x8f\xf1#Ƶ\"\x83\xf6\x14\x8d\xf0`\xa9\x95l_\xc5bP\x18\xcb-\xb7?/-z\xdfZ\xbak\xc3/17\xc85\x9ak\a\x93)_v\xf4\x9aA\xee΄\xf5\xe9\xa5x\x0e\xff\xfeϤMה\x13\x1b\x8f\xe2\x87\xfeǵw)d\x94/d\xf1'\xa7:&}\x1d\x84\xbf\xfdc\x92\xaeB\xf1\\>i\xd1\xe4\u007f\x03\x00\x00\xff\xff\x1d\r\x93\v\x97\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\xc7\xef\xfa\x14\x98}\x0e{y$\xefN\x0f\xed\xe8\xd6\xcd\xee!\xd36\xe3I2\xb9tz\xa0I\xd8\xe2F\"Y\x00t\xeav\xfa\xdd;$%\xbf\xc8v6=\x947\x91 \xf0\xe7\x0f\x04Ī\xae\xebJ\x05\xfb\x84\xc4ֻ\x16T\xb0\xf8\x87\xa0K_\xdc<\xff\xc0\x8d\xf5\x8b\xed\xc7\xea\xd9:\xd3\xc2Md\xf1\xc3=\xb2\x8f\xa4\xf13\xae\xad\xb3b\xbd\xab\x06\x14e\x94\xa8\xb6\x02P\xceyQi\x9a\xd3'\x80\xf6N\xc8\xf7=R\xbdA\xd7<\xc7\x15\xae\xa2\xed\rRv>\x85\xde~h\xbeo>T\x00\x9a0o\u007f\xb4\x03\xb2\xa8!\xb4\xe0b\xdfW\x00N\r\u0602\xc1\x1e\x05WJ?\xc7@\xf8{D\x16n\xb6\xd8#\xf9\xc6\xfa\x8a\x03\xea\x14xC>\x86\x16\x0e\ve\xff(\xaa\x1c\xe8sv\xf5)\xbb\xba/\xae\xf2joY~\xbaf\xf1\xb3\x1d\xadB\x1fI\xf5\x97\x05e\x03\xb6n\x13{E\x17M*\x00\xd6>`\vwIVP\x1aM\x050\xf2\xc82kP\xc6dª_\x92u\x82t\xe3\xfb8Ldk0Țl\x90L\xf0\xb1\xc3|D\xf0k\x90\x0e\xa1\x84\x03\xf1\xb0\xc2Q\x81\xc9\xfb\x00\xbe\xb2wK%]\vM\xe2\xd5\x14\xd3$d4(\xa8?ͧe\x97\x04\xb3\x90u\x9bk\x12X\x94D\x9eD\xe4\xb8\xd6;\xa0#\xbe\xa7\x02\xb2}\x13:ŧ\xd1\x1f\xf2µ\xc8\xc5f\xfb\xb1\x90\xd6\x1d\x0e\xaa\x1dm}@\xf7\xe3\xf2\xf6黇\x93i8\xd5z!\xb5`\x19Ԥ4\x81+\xd4\xc0;\x04O0x\x9a\xa8r\xb3w\x1a\xc8\a$\xb1\xd3\xd5*㨪\x8efg\x12\xde'\x95\xc5\nL*'\xe4\fm\xbc\x04hƃ\x15\x98\x96\x810\x102\xbaR`'\x8e!\x19)\a~\xf5\x15\xb54\xf0\x80\x94\xdc\x00w>\xf6&U\xe1\x16I\x80P\xfb\x8d\xb3\u007f\xee}s:g\n\xda+9\xe4g\x1a\xf9\xd29\xd5\xc3V\xf5\x11\xff\x0f\xca\x19\x18\xd4\x0e\bS\x14\x88\xee\xc8_6\xe1\x06~I\x98\xac[\xfb\x16:\x91\xc0\xedb\xb1\xb12u\x13\xed\x87!:+\xbbEn\fv\x15\xc5\x13/\fn\xb1_\xb0\xddԊtg\x05\xb5D\u0085\n\xb6\xce\xd2]\xee(\xcd`\xfeGc\xff\xe1\xf7'Z\xcf.H\x19\xb9\xd0_\xc9@*\xf3\x92\xf6\xb2\xb5\x9c\xe2\x00:M%:\xf7_\x1e\x1ea\n\x9d\x931\xa7\x9f\xb9\x1f6\xf2!\x05\t\x98uk\xa4\x92\xc45\xf9!\xfbDg\x82\xb7N\xf2\x87\xee-\xba9~\x8e\xab\xc1\nOW2媁\x9b\xdcbSQ\xc7`\x94\xa0i\xe0\xd6\xc1\x8d\x1a\xb0\xbfQ\x8c\xffy\x02\x12i\xae\x13ط\xa5\xe0\xf8\xef07.Ԏ\x16\xa6\xf6}%_\x17\x8a\xf6!\xa0N\x19L\x10\xd3n\xbb\xb6:\x97\a\xac=\xc1Kgu7\x15\xed\x8c\xee\xbe\xc0\x9b\x93\x85\xcb\x05\x9dơM\xceW\xae\x1e\x1er\xee,\xe1\xec\x16\xd6p\xd6s_璛\xe1\xbf$S:\xf1\xc8FG\"trԟեMoe\x81D\x9e\xcefg\xa2\xbed\xa3\xfc\x04P\xd61(\xb7\x1b7\x82tJ\xe0\x05)\x95\x81\xf61\xf5\x194`\xe2\x19\xbf\x11\xcb\xf1\xbf$\x90\xd7\xc8ܜ\xd9Y\xc1ႦW\xb2\x93Fz^\xa8U\x8f-\bE\xbc\x92YE\xa4v\xb3\xb5\xfc\xcf\xfa\x06\x82e\xb2\xb9\x94\x83\xfd\u007f\xfa\x9bIȸ]\x1c\xce#\xd5p\x87/\x17foݒ\xfc\x86\x90\xe7W>-.\v\xbd\xfdc\xe0\r\x94.^ʳIN\xfd\xce\x1cQd\xf1\xa46\xc7\\9\xae\xf6\xfd\xbb\x85\xbf\xfe\xae\x0e\xf7Zi\x8dA\xd0\xdc\xcd_i\xefޝ<\xb7\xf2\xa7\xf6\xae\xbc\x8c\xb8\x85_\u007f\xabJ(4O\xd3\xeb)M\xfe\x13\x00\x00\xff\xff--\nM\xde\n\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x93۶\x0f\xbd\xfbS`\xf2;\xe4יHN\xa6\x87v|k79\xec4M3\xebt/\x9d\x1eh\n\x96إH\x96\x00\xbd\xd9~\xfa\x0eH\xc9\u007fdٻ9\x947\x81 \xf8\xf0\xf8\x00R\x8b\xaa\xaa\x16*\x98{\x8cd\xbc[\x81\n\x06\xbf2:\xf9\xa2\xfa\xe1G\xaa\x8d_\xee\xde-\x1e\x8ckVp\x93\x88}\u007f\x87\xe4S\xd4\xf8\x1e\xb7\xc6\x196\xde-zd\xd5(V\xab\x05\x80rγ\x123\xc9'\x80\xf6\x8e\xa3\xb7\x16cբ\xab\x1f\xd2\x067\xc9\xd8\x06c\x0e>n\xbd{[\xffP\xbf]\x00\xe8\x88y\xf9\x17\xd3#\xb1\xea\xc3\n\\\xb2v\x01\xe0T\x8f+h\xfc\xa3\xb3^5\x11\xffNHL\xf5\x0e-F_\x1b\xbf\xa0\x80Z6m\xa3Oa\x05\x87\x89\xb2v\x00T\x92y?\x84\xb9+a\xf2\x8c5Ŀ\xcc\xcd~4\x83G\xb0)*{\x0e\"O\x92qm\xb2*\x9eM/\x00H\xfb\x80+\xf8$0\x82\xd2\xd8,\x00\x86\xdc3\xacj\xc8n\xf7\xae\x84\xd2\x1d\xf6\xaa\xe0\x05\xf0\x01\xddO\x9fo\xef\xbf_\x9f\x98\x01\x1a$\x1dM\xe0\xcc\xe0\x043\x18\x02\x05\x03\x02`\xbf\a\x05ʁ\x8al\xb6J3l\xa3\xefa\xa3\xf4C\n\xfb\xa8\x00~\xf3\x17j\x06b\x1fU\x8bo\x80\x92\xee@I\xbc\xe2\nַ\xb05\x16\xeb\xfd\xa2\x10}\xc0\xc8fd\xb9\x8c#q\x1dY'\xc0_Kn\xc5\v\x1aQ\x15\x12p\x87#?\xd8\ft\x80\xdf\x02w\x86 b\x88H\xe8\x8a\xceN\x02\x838)7dP\xc3\x1a\xa3\x84\x01\xea|\xb2\x8d\x88q\x87\x91!\xa2\xf6\xad3\xff\xecc\x930$\x9bZţ\x1c\x0e\xc38\xc6蔅\x9d\xb2\t߀r\r\xf4\xea\t\"f\x9e\x92;\x8a\x97]\xa8\x86_}D0n\xebW\xd01\aZ-\x97\xadᱨ\xb4\xef\xfb\xe4\f?-s}\x98Mb\x1fi\xd9\xe0\x0e\xed\x92L[\xa9\xa8;è9E\\\xaa`\xaa\f\xdd\xe5ª\xfb\xe6\u007fq(Cz}\x82\x95\x9fDf\xc4Ѹ\xf6h\"k\xfe\xca\t\x88\xea\x8b`\xcaҒŁh1\t;w\x1f\xd6_`\xdc:\x1fƔ\xfd\xa2\x9c\xfdB:\x1c\x81\x10f\xdc\x16c9Ĭ<\x89\x89\xae\t\xde8\xce\x1f\xda\x1atS\xfa)mz\xc34\x8aYΪ\x86\x9b\xdci`\x83\x90B\xa3\x18\x9b\x1an\x1dܨ\x1e\xed\x8d\"\xfc\xcf\x0f@\x98\xa6J\x88}\xd9\x11\x1c7ɩsa\xedhb\xecd\x17\xcekR\xea\xeb\x80ZNO\b\x94\x95fkt.\r\xd8\xfa\b\xeaP\xf9\x03\x81\xf5I\xe4\xf9\xca\xcd\xe0Tl\x91\xa7\xd6\t\x96/\xd9I\xb6\u007f\xec\xd4i\xa3\xf9?\xd6m-\xbd\x82\x06 \xa5{|W\x9fE\xbc\x8c\x01f\xd5;\x8bd\x14\xb1\xd0 \xbcJ+\x90&u\x8c\xe9|k\x19\xe8R?\xbfA\x05?g\xcc\x1f}{u\xfe\xc6;\x16\xb9_u\xba\xf76\xf5\xb8v*P\xe7\x9f\xf1\xbde\xec\u007f\v\x18\xcbUz\xd5u\xbc\x91\xf7\xb7Թ\xe3\x1dJ/\xc7\xcbY\f\x0ewH\xc9^D68\xbd\b\xda\xcd\xfa\xf6[\x92\xbe\xe0~\x95\xd6\v\x856\x8e|\xa1>\xaf\x1a\xb9\x92G\xd5Ȓr\xcb \xc8C%:d\xa4C\xc3{4\xdc\xcdF\x04x\xec\x8c\xee\xf2\xc2,9\xe9\xa5D^\x9bܙ\xbe\x1d\xbeT\xaa\x898#\xfb*\x97ÌY\xc0\x9f\x99/\xf4\x97K\x1bTCͿ\xa8G\xb1\xe2D\xdfХ\xb2\xffH\xb5N1\xa2\xe3!J\xbe\xb5\xa7\v^ڦ\xc6\xda\xfe\xfd\xee\xe33\xbd\xea\xfd\xc13\xbfK\x95q\x05M\x88X\x91i\xe5\xad!sҭr\x179'\xa3\x8cӷ\xcf)Q\xb3'\x8a_\x83)\x05\xf3\f\xc4\x0f{\xc7\xd2Rѕ\xebr\xfa\xba\xcb\x01\x91\xf2SD\xab\xe9#H\xc6\x06\xa1A\x8b\x8c\rl\x9e\xca\xdd\xf0D\x8c\xfd9\ueb4f\xbd\xe2\x15\xc85Z\xb1\x99\x91\x91\xbc\xc0\xd5\xc6\xe2\n8\xa6K*\x9bM\x85\x15\x1c\x06\xca\xdc\x01P\xd9\xcc\xfb!\xcd]I\x93G\xac!\xfeen\xf4\xa3\x19\"\x82MQ\xd9s\x10y\x90\x8ck\x93U\xf1lx\x01@\xda\a\\\xc1'\x81\x11\x94\xc6f\x010\xec=ê\x86\xdd\xedޕT\xba\xc3^\x15\xbc\x00>\xa0\xfb\xe9\xf3\xed\xfd\xf7\xeb\x93n\x80\x06IG\x13838\xc1\f\x86@\xc1\x80\x00\xd8\xefA\x81r\xa0\"\x9b\xad\xd2\f\xdb\xe8{\xd8(\xfd\x90\xc2>+\x80\xdf\xfc\x85\x9a\x81\xd8G\xd5\xe2\x1b\xa0\xa4;P\x92\xaf\x84\x82\xf5-l\x8d\xc5z?)D\x1f0\xb2\x19Y.\xedH\\G\xbd\x13\xe0\xafeo%\n\x1aQ\x15\x12p\x87#?\xd8\ft\x80\xdf\x02w\x86 b\x88H\xe8\x8a\xceN\x12\x83\x04)7젆5FI\x03\xd4\xf9d\x1b\x11\xe3\x0e#CD\xed[g\xfe\xd9\xe7&aH\x16\xb5\x8aG9\x1c\x9aq\x8c\xd1)\v;e\x13\xbe\x01\xe5\x1a\xe8\xd5\x13D\xcc<%w\x94/\x87P\r\xbf\xfa\x88`\xdc֯\xa0c\x0e\xb4Z.[\xc3cQi\xdf\xf7\xc9\x19~Z\xe6\xfa0\x9b\xc4>Ҳ\xc1\x1d\xda%\x99\xb6RQw\x86Qs\x8a\xb8T\xc1T\x19\xba˅U\xf7\xcd\xff\xe2P\x86\xf4\xfa\x04+?\x89̈\xa3q\xed\xd1@\xd6\xfc\x95\x13\x10\xd5\x17\xc1\x94\xa9e\x17\a\xa2\xa5Kع\xfb\xb0\xfe\x02\xe3\xd2\xf90\xa6\xec\x17\xe5\xec'\xd2\xe1\b\x840\xe3\xb6\x18\xcb!f\xe5INtM\xf0\xc6q\xfe\xd0֠\x9b\xd2Oi\xd3\x1b\xa6Q\xccrV5\xdcd\xa7\x81\rB\n\x8dblj\xb8up\xa3z\xb47\x8a\xf0??\x00a\x9a*!\xf6eGpl\x92\xd3\xe0\xc2\xda\xd1\xc0\xe8d\x17\xcekR\xea\xeb\x80ZNO\b\x94\x99fkt.\r\xd8\xfa\b\xeaP\xf9\x03\x81\xf5I\xe6\xf9\xca\xcd\xe0Tl\x91\xa7\xbd\x13,_r\x90,\xffةS\xa3\xf9?\xd6m-^A\x03\x90\xe2\x1e\xdf\xd5g\x19/c\x80Y\xf5\xce\"\x19E,4\b\xafb\x05bRǘΗ\x96\x86.\xf5\xf3\vT\xf0s\xc6\xfcѷW\xc7o\xbcc\x91\xfbՠ{oS\x8fk\xa7\x02u\xfe\x99\xd8[\xc6\xfe\xb7\x80\xb1\\\xa5WC\xc7\x1by\u007fK\x9d\aޡx9^\xde\xc5\x10p\x87\x94\xecEd\x87\xa0\x17\xae\xf7\xa2Mܬo\xbf\x85\x9e\v\xe1W\x0f\xe0BI\x8e-_\xbd\xcf\xebK.\xefQ_2\xa5\xdcG\b\xf2\xa4\x89\x0e\x19\xe9`\x8d\x8f\x86\xbbٌ\x00\x8f\x9d\xd1]\x9e\x98\xc5)\xaeK\xe4\xb5\xc9\x1e\xf6\xed\xf0\xa5\xa6Mę\x02\xa9r\xe1\xcct\v\xf8\xb3\xee\vNti\x81jp\x87\x17\xb9\x19+N\xf4\r~\x96\xe3G\xaau\x8a\x11\x1d\x0fY\xf2\xfd>\x9d\xf0RC\x1b]\xe0\xf7\xbb\x8fϸ\xda\xfbCd~\xc1*\xe3\n\x9a\x10\xb1\"\xd3ʫD\xc6\xc4ײߜ\x93Q\xda\xe9+锨\xd9\x13ů\xc1\x94\x82y\x06\xe2\x87}`1_t\xe5b\x9d\xbe\x03sB\xa4\xfch\xd1j\xfa\\\x92\xb6Ah\xd0\"c\x03\x9b\xa7r\x8b<\x11c\u007f\x8e{\xebc\xafx\x05r\xe1Vlfd$ou\xb5\xb1\xb8\x02\x8e\xe9\x92\xcaf7\x1e:E3ex\xb2\xe7\xcf\x123'\x8c}1^U\x06\\\xf4\xfa\n>\xe1\xe3L\xef\xe7\xe85\x12\xe1y\x19]\xdc\xc9l\x11\x9cu\x92\xbc\x8a\x9a#\x96\x86\xc7\xf6\xd0s(\x19\xa55\x06\xc6\xe6\xd3\xf4\x0f\xe6ի\x93_\x92\xfc\xa9\xbdkL\xf9\xf9\x82?\xfe\\\x94\xac\xd8\u070f\u007f\x1a\xd2\xf9o\x00\x00\x00\xff\xff\xdb\xd9+\xab\xf6\r\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z\xdds\x1b\xb7\x11\u007f\xe7_\xb1\xe3<\xa8\x991\x8f\x89\xdbi;|s\xa4\xa6\xa36\x915\x96\xad\x17\x8f\x1f\xc0\xc3\xf2\x0e\xd1\x1d\x80\x028\xcal&\xff{g\xf1q\xbc\x0f\x90\x944ur/6\xf1\xb1\xf8a\xbfw\xa1\xc5r\xb9\\0-\xee\xd1X\xa1\xe4\x1a\x98\x16\xf8š\xa4_\xb6x\xf8\xbb-\x84Z\xed\xbe_<\b\xc9\xd7p\xd9Y\xa7\xda\xf7hUgJ\xbc\u00ad\x90\xc2\t%\x17-:ƙc\xeb\x05\x00\x93R9FÖ~\x02\x94J:\xa3\x9a\x06ͲBY\xf38\xfa\x01\xed\xe4\xfa\x8b\x99\xdb\x1e\xd1~[aޞ\xc3\xf4\xee\xfb\xe0@\xcb\x1a[\xb6\x8e+\x95F\xf9\xf6\xf6\xfa\xfe\xcfw\xa3a\x00m\x94F\xe3Dr\xe8\xe1\x1bı\xc1(\x8cY}A\x04\xc3*\xe0\x14\xc0\xd0\x06\xab\bc\xc8#\x86 \x0eaIu\rZ\x94nȒ\xf4\xa9-0\tj\xf3\v\x96\xae\x80;4D&\t\xa6Tr\x87Ɓ\xc1RUR\xfc\xb7\xa7mI\xd7\xe8І9\x8cq\xe5\xf0y\xd7/Y\x03;\xd6t\xf8\x1a\x98\xe4в=\x18\xa4S\xa0\x93\x03z~\x89-\xe0ge\x10\x84ܪ5\xd4\xcei\xbb^\xad*\xe1R\xfc.U\xdbvR\xb8\xfdʇb\xb1\xe9\x9c2v\xc5q\x87\xcdʊj\xc9LY\v\x87\xa5\xeb\f\xae\x98\x16K\x0f]\xfa\x18^\xb4\xfc\x1b\x13#\xbe\xbd\x18a\x9d)F\xf8|x=!\x01\n\xb0 ,\xb0\xb85\xdc\xe2\xc0\xe8\xe4 \xdf\xff\xe3\xee\x03\xa4\xa3\xbd0\xa6\xdc\xf7|?l\xb4\a\x11\x10Ä\xdcbt0[\xa3ZO\x13%\xd7JH\xe7\u007f\x94\x8d@9e\xbf\xed6\xadp$\xf7\xffth\x1dɪ\x80K\x9fԐ\xc3\xec4i./\xe0Z\xc2%k\xb1\xb9d\x16\xbf\xba\x00\x88\xd3vI\x8c}\x9a\b\x86\xf9\xd8tq\xe0\xda`\"%MG\xe45Ʉ\xee4\x96$=b \xed\x14[\x11=\x14\xb9s6]^\x8c\b\xe7\r\x97\xbe\xacw\x9a.\x82\\p\x99\xecI\xd8\xe4\xc0\xa7&\x87\x19VΈ\x024S/\xdb\xef\x19F.\x1b\x1dl1\xa3pD\f\xf4I\xc5\xf1\xcc=n\x14\xc7\x1cl\xda\n\xaefA[)\xe3#\u007f\xd4I9?\x85>%\x9f\x05L+~\x06W<\x91\x81\xc1-\x1a\x94%&\xc7u*\x9d\xc9 \x1b&\x1as\x8cǕ\x02Nx\xf5,ⷷ\xd7ɓ'&F\xecn~\xee\x19\xfeз\x15\xd8p\x1f\xe8Ο}q\xbd\r\x87y\x9f\xe6\x140\xd0\x02Cb\xda\a\t\x10\xd2:d\x1c\xd46K\x91\xca' \xc37\x18w\xbc\x0e\x1e,\xba\xcaCh!\xde\x03#\xdf)8\xfc\xeb\xee\xdd\xcd\xea\x9f9\xd6\xf7\xb7\x00V\x96h}^\xee\xb0E\xe9^\xf7\xa5\x02G+\frJ\xfc\xb1h\x99\x14[\xb4\xae\x88g\xa0\xb1\x9f\xde|\xces\x0f\xe0Ge\x00\xbf\xb0V7\xf8\x1aD\xe0x\uf593\xd2\b\x1b\xd8\xd1S\x84G\xe1j1\r\xa6=\aH\xbd\xe2\xb5\x1f\xfdu\x1d{@P\xf1\xba\x1dB#\x1ep\r\xaf|Zs\x80\xf9+\xd9\xceo\xaf\x8eP\xfdS0\xedW\xb4\xe8U\x00\xd7\xc7\xe1\xa1\xd1\x1d@\x06\xcb3\xa2\xaa\xf0\x90UM?\x1fT\xc8U\u007f\v\xca\x10\a\xa4\x1a\x90\xf0\x84Iz\xc1Q\"\x9f\x81\xfe\xf4\xe6\xf3Q\xc4c~\x81\x90\x1c\xbf\xc0\x1b\x10\xb1\xd8Ҋ\u007f[\xc0\a\xaf\x1d{\xe9\xd8\x17:\xa9\xac\x95\xc5c\x9cU\xb2ه\xf6\x881\tʄyp\xcdL\ueffa*\x13C;C\x88\xf6\xcb\xd8\xf6[2\xc9\xe9\xffVXG\xe3/\xe2`'\x9ed\xbe\x1f\xaf\xaf~\x1f\x05\xefċl\xf5H\x02\x1etd\xd8\xe58\x93\x98\xbd\x1f-N\xa9c&c\xed\xd7<+3t\xacʤb\xc3\xf6䩄\xed$\aƭ\x18VY`\x06\x81A\xcb4I\xee\x01\xf7\xcb\x10\xe25\x13\x14\x9f)\x04\xf7}\x0e`Z7\"\x1b\x8ac \x8fIh\xe4\x04\x15ڬ\xb2\xc7\ue795ð\xafsF\n\x1f\aK\x93\f\xcet\x96\\\x9d\xb3\xd4Q\xbfi\x8e\x16e\xd7Ρ,\xe1Ai\xc12\xe3\x06\xad\x13ef\xe2\xd5<\xd38!\xac\xc0\xcb3<\x88-\xe8L\xf1\x12E\x112\xbd\xbe\x80\xf1]\xc7\\\x85p\xbc<8\n\x91*t\xca[\xc7\x10\x97\xf9Rr\xb2\x86J\xabɐV|1ed\xa6\xf3\x98&G\x9d\xd1!\xd2y}\xed\x1b\xdeϨ\xb0C#?\xf24\xf8S\x97\xda\xfbTL\xbc\xb4\xc6.\x15\xe5\xe9㧕\xd3⽜\xef\xf0\xed,ã\xba\x8b\x96\xacw\xd0\xf6\x8fg\xe4\x8ad\x18\x90\v;}\x04#j\xc8}\x12M9\xfe\x96\x89\x069\xa4\xb7\x9d\xe9\x9e\f\xd5!\x95\rn\xc9\xdd\a\xd3K\xa5i\x84\xd7'\xaa5\x82\xf5}\xa2\v{\x82fg\x91\xfb\x9eF\x86\t\xf3\xe4u\xabL\xcb\\\xe8k.\xb3De\xd74l\xd3\xe0\x1a\x9c\xe9\xe6\xd3',\xb1EkYu\xce\x14\u007f\x0e\xabB\xc5\x1e\xb7\x00ۨ\xce\xf5%\xfb\xc8=^بS\xcf\xeb\x1ad\x8b\xe1\xb1:3*VlLڛ\xc6\xef\x19:\x82Ã\xa0G\xb5\xc1|\xd0\u007f\x89O\x00\xf0\x0fZ\xe7\x10Қ\x9c\x81\xf5\xde뤅\xc1\t\xa7|\x83\x8f\x99\xd1\xd9C\xdcp\xf22\x99Lf\xeeGo\rϺ\u007f<\xe8\x1c\v\xe22\xa8U\x93\x8cY9ր\xec\xda\r\x1a\xe2\xc3f\xefЎ\xddy\xae?\xe3\xeb\xba\x03\x1b\a\xfb\x93\xfc\x02\xa5X\xaa\x96L\xfa>*Y\x97S\xc0\x85\xd5\r\xdbg\b\xa7\x8b\xf8܍\x8c\x8b\\\xc0A\x9f\x93Qk4~\xea\xb9}%\x8f\xe9J\xc9#\x95F\xb2g!\xdd_\xffr\"\xd3\x13\xd2a5\t\x0eq\x9e\xd8\xf9\x03\x9d\xf2uN8\x91\xc4Xɴ\xad\x95\xbb\xbe:\xa3\x05w\xfd\xc2d\r\xb3\xe79\xec\xa9EUȉ\xaa\xf7-\xcf2\xd5\xf1\x13\xf09\xa8\xa3\xc5g\xa2P||\xceŠ;\xd4̐\xa5\xfb7\x81\xcb\xe9\xa3\xd5k\xb0\xc27:)\xf3\f\xa9hhCX\nN\x94Z)\x83\x19\x97\t\xf3\xb02\n\"c\xf8\xbfg\xfc\xc8\xea\xc9l\xd0#\xe7\x03ڱY>\x1c\xe96\xfdC\xd0\x1a~\xfdmqHlXI\xc5\x13\xf2\x9b\xe9\x1fYĔ3\xfdՄ\xffY*\x19*\t\xbb\x86O\x9f\x17\xe9\xd9\xf2>\xfd1\x04\r\xfe/\x00\x00\xff\xff\xb0\xddǼ\x99\"\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\x1b\xb9\x11\xbe\xf3Wti\x0f\xcaV\x99\xc3]'\x95\xa4x\xb3\xa5lJɮ\xac2e]\\>\x80\x83\xe6\f\x963\x00\x02`H3[\xfb\xdfS\x8d\a9/\x92\xa2*Z\xcf\xc5\x16\xd0h|\xf8\xd0/4'\xd3\xe9t´xBc\x85\x92s`Z\xe0W\x87\x92\xfe\xb2\xd9\xfa\xef6\x13j\xb6\xf9q\xb2\x16\x92\xcfᦱN\xd5\x1fѪ\xc6\xe4x\x8b+!\x85\x13JNjt\x8c3\xc7\xe6\x13\x00&\xa5r\x8c\x86-\xfd\t\x90+錪*4\xd3\x02e\xb6n\x96\xb8lD\xc5\xd1x\xe5i\xeb\xcd\x0f\xd9߲\x1f&\x00\xb9A\xbf\xfcQ\xd4h\x1d\xab\xf5\x1cdSU\x13\x00\xc9j\x9c\x83V|\xa3\xaa\xa6F\x83\xd6)\x836\xdb`\x85FeBM\xacƜv-\x8cj\xf4\x1c\x0e\x13aqD\x14N\xf3\xa0\xf8\x93\xd7\xf31\xe8\xf1S\x95\xb0\xeeߣ\xd3?\v뼈\xae\x1aê\x11\x1c~\xd6\nY4\x153\xc3\xf9\t\x80͕\xc69\xdc\x13\x14\xcdr\xe4\x13\x80H\x80\x876\x05ƹ\xa7\x94U\x0fFH\x87\xe6\x86T$*\xa7\xc0\xd1\xe6Fh\xe7)\xdb\xeb\x01\xb5\x02W\"m\xe9\xe9fB\nY\xf8\xa1\x00\x01\x9c\x82%BD½2\x80_\xad\x92\x0f̕sȈ\xb8L+\x9eɤ3\xca\x04\xce\xef{\xa3nG\xe7\xb0\xce\bY\x1cC\xf6\u007f\x06\xd5\xc1\xf3\xa0\xf83\x91<\x96\xe8e\x12\x9aFW\x8aq4\xb4y\xc9$\xaf\x10\xc8r\xc1\x19&\xed\n\xcd\x11\x14i\xd9\xe3Nw\x91|J\xfaZ3\x97\xb0s\t\x15A\xb6\xb3\xfdS{\xe8ܾ\x0f\x8a\xc7\x05\x10\x8d\x1a\xacc\xae\xb1`\x9b\xbc\x04f\xe1\x1e\xb7\xb3;\xf9`Ta\xd0\xda\x11\x18^<\xd3%\xb3]\x1c\v?\xf1\xba8V\xca\xd4\xcc\xcdAH\xf7\u05ff\x1c\xc7\x16\x17eN9V\xbd\xdf9\xb4\x1d\xa4\x8f\xfdဖ\x9c\xad\x88\xd7\xffM\xe0.\tҭ\x92]^\xdf\xf7F\xc7\xc0\xb6\x94\xa6@\x9c\r\x82hG뻢\xab\x8f3\x17\x06\xc2\xf4\xe6\xc7\x10\xca\xf2\x12k6\x8f\x92J\xa3|\xf7p\xf7\xf4\xe7Eg\x18@\x1b\xa5\xd18\x91\xa2k\xf8ZY\xa55\n]f\xafIa\x90\x02N\xe9\x04mp\x8a0\x86\xa3f5\xff\xce\xc4\xfck\xaf;X\aN\x17>\x9f\xebN\xdc\x00%;\x10\x16X\\\x1aNq :\x85\xec\x8f\xffX\xdc\xcf\xfe9\xc6\xfc\xfe\x14\xc0\xf2\x1c\xad\xf5\xf9\x1ak\x94\xee\xcd>gs\xb4\xc2 \xa7\xc2\x05\xb3\x9aI\xb1B벸\a\x1a\xfb\xf9\xed\x97q\xf6\x00~R\x06\xf0+\xabu\x85o@\x04\xc6\xf7\xe1/ٌ\xb0\x81\x8e\xbdF\xd8\nW\x8a~\xd2\xda3@\xd6\x15\x8f\xbd\xf5\xc7ul\x8d\xa0\xe2q\x1b\x84J\xacq\x0eW\xbe\x12<\xc0\xfc\x8d\x1c\xeb\xf7\xab#Z\xff\x14\x1c芄\xae\x02\xb8}\xbek{\xe4\x01\xa4+\x99\x03gDQ\xe0\xa1\x10\xed\u007f>xSH\xfc\x1e\x94!\x06\xa4j\xa9\xf0\x8a\xe9\xf6B\x00\xfd\xf9헣\x88\xbb|\x81\x90\x1c\xbf\xc2[\x102p\xa3\x15\xff>\x83Go\x1d;\xe9\xd8W\xda)/\x95\xc5c\xcc*Y\xedB\xb5\xbfA\xb0\xaaF\xd8bUMC\xbd\xc1a\xcbv\xc4B\xba8\xb27\x06\x9a\x19w\xd2ZS\x95\xf1\xf8\xe1\xf6\xc3< #\x83*|\xbc\xa3\xec\xb4\x12T5P\xb9\x10r\x9e\xb7\xc6A\xd2L\x9fm\x82\xf98\x05y\xc9d\x81\xe1\xbc\b\xab\x86\xb2Pv\xfd\x12?\x1e\xa6\xfe\xf4\x8d\x94\x00\xfd\xc0\xf1͒\xe83\x0f\xe7+\xd5g\x1c\xae\xfd\xd6:y\xb8u\xb3D#ѡ?\x1fW\xb9\xa5\xa3娝\x9d\xa9\r\x9a\x8d\xc0\xedl\xab\xccZ\xc8bJ\xa69\r6`g\xfe\xc9<\xfb\xce\xff\xf3\xe2\xb3\xf8\xd7\xf5s\x0f\xd4y\xf4\xbf\xe6\xa9h\x1f;{ѡR\xad\xf8\xfc2q5\xacTN0\x11\f\xe0\f\a\xb1\x994\xf22\x8a\xf6\x13*E?B\xaf\x11oE\xe3!\xf6R\xbb\xa2\x874\x95\xbd]\x84\xd3\xf1\xf7^OF+>\xe9\x93\xd6v\xc9\xde\xe4\xc1\xa1\xfa\x13][\xed\xcdv\x9a\x9c\xed\xd3\f\x9fʾ\x83v\xc9c9t\xed\"\xef!f\xbb\xd4ˣ\aˋ\x9f˹\xa2\xc7@\xf7W\x8b\xd36p3\\\xe1{S\x86G\x9f\x105\xfa7hh8n\x99M\x9b\x8c\xdd7\xb4\xf4\x85\xa5>O\x92:\xe4\xbeT\xa7\x97Ċ\x89\n9\xec\u007f7\xf1\xcdq\xeb\x9b4\xd7c\x95iR\xd4X\xe4>n\x8c\x80\x1e\xaeK}O\xce\x1cNI\xc5@B6UŖ\x15\xce\xc1\x99f8}½j\xb4\x96\x15\xe7\xfc\xeb\x97 \x15^\xf1q\t\xb0\xa5j\xdc\xfe\x19\x1f\x1d-Rqm\xa3\x15\\\xd6J(\x99=\a\xe5\x81d\xc6,n\xef\xf2\xa7M\x0eN\x84\xb2{\u070e\x8c\x0e\xfa\xd0\xedɛdB#s?y븈\x80\xb8\xd19\x0e\xa2\x18\x94\xaaJ֭\x1c%\xa5\xa6^\xa2!\"|\xf3;1\x92\x02\xc7X_Ŀ\xa7\x0eL\x1e4\xa4X\x18T\xc5\x17bΤo\x13\x92\xfd:\x05\\X]\xb1݈\xdet\x12_2\x91\xf9\x92\x1f\x1d,&y!\xb9\xbf\x9f\xbb\xb4\x9f\xb3o\xee\x8f\x17tc?\x15\x8c\xddB\xbb\xefߛ\xdf\xff\xaa\xf1:;\x9c(\xe2\xacc\xc6=7\xec-:\xc2\xe7\"\x9eW=\x1e\xefڡk\x18\xa8\xba\xdb\xfc\x911j\x94\xa8\xc1\xa0G\xce[\xbac/\xb4=\xd2,\xf7\x9d\xfe9\xfc\xf6\xfb\xe4\x90\xeeXNU;\xf2\xfb\xfeOڱVI\xbfP\xfb?s%\xc3O\xcav\x0e\x9f\xbfL 6M\x9f\xd2\xcf\xce4\xf8\xbf\x00\x00\x00\xff\xffe\xe5\xd5&\b \x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xdc<\xcbr۸\x96{\u007f\xc5)\xcf\"3U\x96ܩY̔w\x19wR\xed\xea\xee\xc4e\xa7ҋ\xa9\xbb\x80\xc8#\tm\x10`\xe3!G\xf7\xd6\xfd\xf7[x\xf1!\x82$$[\xe9\xf4\xe5N\x14p\x80\xf3\xc0y\x13\x17\x8b\xc5\xe2\x82\xd4\xf4\vJE\x05\xbf\x01RS\xfc\xaa\x91\xdb_j\xf9\xf4\xbfjI\xc5\xf5\xee\xed\xc5\x13\xe5\xe5\r\xdc\x1a\xa5E\xf5\x80J\x18Y\xe0\x8f\xb8\xa6\x9cj*\xf8E\x85\x9a\x94D\x93\x9b\v\x00¹\xd0ľV\xf6'@!\xb8\x96\x821\x94\x8b\r\xf2\xe5\x93Y\xe1\xcaPV\xa2t\xc0\xe3һ\x1f\x96\xff\xb3\xfc\xe1\x02\xa0\x90\xe8\xa6\u007f\xa6\x15*M\xaa\xfa\x06\xb8a\xec\x02\x80\x93\no@\xa2\xd2B\xa2Z\ue421\x14K*.T\x8d\x85]l#\x85\xa9o\xa0\xfd\xc3\xcf\t\x1b\xf1H<\xf8\xe9\xee\r\xa3J\xff\xdc}\xfb\vU\xda\xfdS3#\tk\x17s/\x15\xe5\x1bÈl^_\x00\xa8B\xd4x\x03\x1f\xed25)\xb0\xbc\x00\b8\xb9e\x17a\u05fb\xb7\x1eD\xb1Ŋ\xf8\xfd\x00\x88\x1a\xf9\xbb\xfb\xbb/\xff\xfd\xd8{\rP\xa2*$\xad\xb5\xa3L\xd8\x1bP\x05\x04\xbe8\xdc\xec\x06\x1c\x13@o\x89\x06\x89\xb5D\x85\\+\xd0[\x04R\u05cc\x16\x8e\x88\rD\x00\xb1nf)XKQ\xb5\xd0V\xa4x25h\x01\x044\x91\x1b\xd4\xf0\xb3Y\xa1\xe4\xa8QA\xc1\x8c\xd2(\x97\r\xacZ\x8a\x1a\xa5\xa6\x91\xb0\xfe\xe9\xc8Q\xe7\xed\x01.o,\xba~\x14\x94V\x80\xd0o9\x90\f\xcb@!\xbb[\xbd\xa5\xaaE\xed\x10\x9d\x80\x12\xe1 V\xbfc\xa1\x97\xf0\x88҂\x01\xb5\x15\x86\x95V\xeev(-q\n\xb1\xe1\xf4\xef\rle\x11\xb5\x8b2\xa21\xf0\xbb}(\xd7(9a\xb0#\xcc\xe0\x15\x10^BE\xf6 Ѯ\x02\x86w\xe0\xb9!j\t\xbf:\xf6\U00035e01\xadֵ\xba\xb9\xbe\xdeP\x1d\xcfO!\xaa\xcap\xaa\xf7\xd7\xee(Е\xd1B\xaa\xeb\x12wȮ\x15\xdd,\x88,\xb6Tc\xa1\x8d\xc4kRӅ\xdb:wghY\x95\xffѰ\xedMo\xafzo%OiI\xf9\xa6\xf3\x87\x13\xf3\t\x0eX\x81\xf7\xb2\xe4\xa7z,ZB\xdbW\x96:\x0f\xef\x1f?w匪C\xea;\xbaw\x84\xafe\x81%\x18\xe5k\x94\x9e\x89N\xda,L\xe4e-(\xd7\xeeG\xc1(\xf2C\xf2+\xb3\xaa\xa8\xb6|\xffà\xb2\x02-\x96p\xeb\x94\n\xac\x10L]\x12\x8d\xe5\x12\xee8ܒ\n\xd9-Qxv\x06XJ\xab\x85%l\x1e\v\xba\xfa\xf0p\xb0\xa7Z珨\xbcF\xf8\x15N\xffc\x8dE\xef\xc4\xd8it\x1d\x8e9\xac\x85\xec)\a;e\xd9\x03\x9a>\xb4\xf6\xf1\xa7\xdfj\xb0\xc3\u007f\x0e\xb6\xf2\u007f\xcd@+?v\x13\x86\xd3?\f:\x15\xe7O,\x0eT\xca\x00$\xc4\xfd9\xb1X\x0e\xfe\x1f\xa1\xa9}\xf0k\xc1L\x89e\xa3m\a\xb8\x1c\xec\xf8\xfd`\x823G\x84r+\xffV\xfd\xdbm\xf3\xf6_\xabN\x13;&\x12\xc1J \xe5\x1e\x1eP\xee\x90MR\xda>Tc\x95\xd8\xdc$v\xe0\xec\x1cY1\xbc\x01-\r\x8eP\x86HI\xf6#\x84\x89\xb69\x97.\xcd\xf8\xa0\x10\x18-\xb0k(q\f\x12\xfa\x86y\x06.\xb8\xf8\x95J\xac|\\\xfc\xd9Q\xb3}\xe3\x02\x8aw\x1f\u007f\xc4r\x8a<\x90'y\x03D\xde\x1dl\xb6\xbbtp\xf4s\xd1\b\xaeO\x134\xf9\x8c\xc7\x15\x10x½\xf7X\b\a\xcb\x1c\xa2\x9d\xbf\x9b\f\x9f\x86\xc4q\xa9\x17\xef\x1e\xe3ށ\t\xb9\x94\xd9ٹ\xa2\xe0\x9f'L\xf8\xfb\xa9\xa7G@\xbb\xa7\x10\xe1zJ\xda\x17\x8e\x10.\xf2\xce'\x1e\xb8\xbcX\xd4E\xf3\xc8A\xbe\"\x89O\xa4\xfd\th6l\xeb\xe4\x0f\x1dc\xdf(\xcf\"{\n\xb6\xb4\xceDԥ\x0f\x15\xba\xd3\x123c_\b\xa3e\xb3\x90\x97\xfb;>\xee\r\xf7\x9f\x8fB\xdf\xf1+\x1f\x92)'%?\nT\x1f\x85vo\xceBN\xbf\xf1\x13\x88\xe9'\xba\xe3Žڶt\xe8\xa6\xd82\x84\xdb?w>\x93Ұ\x87*\xb8\xe36p\t\xf4p\tS\xbfܴ}\xe8?\x95Q.\x87\xc6\x05_8S\xb9L\xad䉝\tR\xc8\x1eG\x86[k\x16\xf5\vf\x82\xfdl-\x89\x9f\xefS\xc0\x8c\x14X\xc6h\xd3%.\x89\xc6\r-\xa0B\xb9\x992\x1cݧ\xb6\xfa=o\v\x99Z\xd7?GJX\x9ei\x8fOP\xdd\xe5\xfcf\x16\xf6\xe4f\x8c\x8a̞\x1d:\x92\xaf\x1c\x1f:\x8f\x913\xb1\xce\xff\x98\xa5.)KW\\\"\xec\xfe\b\x8d\u007f\x04/\x86\xb6\xdfo\xcc[Ȋ\xd4\xf6\xfc\xfeÚ9'\xd0\xff\x84\x9aP\x99q\x86߹:\x11\xc3\xdeܐ\x19\xeb.cW\xa0\n,\u007fw\x84\r3\xe1\t\xe4\x84\xd5-ȼ!\x17\xeb\x81\xc7r\x05\xcf[\xa1\xbcM]Sd\xa9\x94M\xff\xa1\n.\x9fp\u007fy5\xd0\x03\x97w\xfc\xd2\x1b\xf8\xa3\xd5M\xe3-\b\xce\xf6p\xe9\xe6^\xbe\xc4\tʔĬa<\x99\xe7n\x9f\x9eXts\xddm\x92;\xb8\xb9S\xbbΒ\xc3Z(\xfdS:a7\xb2\x9f\xfb8\xa3\xef\x9b&\xf2^\xb3>{\xc8a5J\xd5zrk\x8d2$\U0007c88d\x11\xc0\vc\xa3\xb9$]\x93\xa0#Mf\xd5\x12xF*|\xcd#g\x8b\xc7x\x8d\x96.G\xfa\xdb\xef\xbfvr\x8c\xf6\x84\xda\xdf]D^۫-DU\x91\xc3*_\xd6Vo\xfd\xcc(\xd3\x01\x90\xe7\xbe\xdc\x18w.\xf3ݽ(C\xae\xbe\xf7L\xf5\x96r \xf1\xf8\xa3\f\x02E\xa0\x16\xf3\x9a\xc8?[\xa2`\x85ț\xdc\xf8\xf7`\xaf+\xca\xef\xdc\x02\xf0\xf6\xd5\xed;\xb4\xe4:\x89\x9d\x91\xd4\rC\x9b\x17\xce\xe2\xe4\xbaF\xa2\x84\xe7-J\xecI\xc50\xe1m=\xc6L\x90\\\xe8n^\xc1\u00adE\xf9F\xc1\x9aJ\xa5\xbb\x1b\xcd\x158\xa3r\xc5\xe1H\x0e[\xec>\xd3\n\x85\xd1'\xf0\xe0};\xbbW\xa0\xad\xc8WZ\x99\nH%L\x86q\xf7\x8f\xb5/\xb4j\xaa\xa8\x81\x03τꦞ\xe42,ZX.\xd5\fu.\x8bW\xb8\xb6\xea\xa8\x10\\\xd1\x12e\xac\xf2{\xceRa\x0f\xee\x9aPfR\xe5\x9b\xd4sl\x98\xca\xdfKyR\x94\xfa\xc9\xcf\xecd\r\xb7\xe2\xb9O\xa0l\x12l\xc9\x0e\x81\xae\x81j@^X\xbe\xa0\xf4*\xdb-\x11\x88\xe1H\x93-\x96y\n\xde>\xc8M\x95G\x80\x85;ٔO&ź\xc3?\x10\xca\xce\xc16+y\xa7\x1f\x8d\xdf\xda\xd9\xdf\xe4h4J%߄\xad\x10\x1e\x90\x94\xfbx>\x88\xd66Tu2 @\x1a\xdeՈg8\x19\xc7\xc4wa\x17\xaf\x19\xb8QN3\x18{\x90ϧ\xba\xeb\xedX\x10g\xf5v\xec\x02\x8d\xa1;%5s\xd7\x03`Met\x9c\xdd\xde\x1b\xa99\xc2\xf3Y\xa1\rP\xb1\xf4I/k>\x83\x1f\xed{\x97F\xca\xe0I\xec\x8ew]\xb28\u06dd\x90\x9f%\xfa\xbah\xdb\x15\x16.)(w\xb80\xfc\x89\x8bg\xbep1\xa5\x9a\xcd\xd67\x8b\x9f\xac8\xbe\xa5\xd2\xe8\x8bW\xbe\bD\xfb{\x06\xa5\x90\xcd\xe6\xa3\x02\xe3))\x98SC\xbe\x8du\xe4\xcf\xd9]L\xad?19\xd4\x1co}\xffino\xd3]zV\xc7\u007fxޢޢ\x8c\x8d\xad\v\xd7ÛR\xabmi\xb2u\x85\x9bf'+?ћ\xf2Mx\a\xedOi_\x99\x1bƮ\xac`\x13ôoE\x95&!DY=@+!\x18\x92ö\u061c\"\xfa\\\xe9\xbc\xdf\x0f֔\xaecC\x98\x88\x8b$0\xf4\xbc\xf4]\x9fݺl\xbf\x06\xee\xb2?q\xa7\u007fz\xabXFy{\xa6\xa8=\xdd@7E\xaf\xa1\xd8t)\xd6\xca`\x18\x17:+\xbf+\xf2\xcd\x14\xa2\xc7\xcb\xcf!ي\x9a\xec\xde.\xfb\xffh\x11\x8a\xd1.\xb3\x90@\xe5y\xdb\xe4\t\x9c\xe5\xe5%\xdd\xd1\xd2\x10֓\xc0\x0e\xcdZ҂\x90\xc0)Kա,\xcd\xe3\xfc\x1e\x8d\xe1S\xed\xf3\xd1G\x9f\xd5iw'\xaff}r\xa5\xba_\x89\x1e\xd1\xe0Ǧf\xf3[\xf2\xf2k\xd1\xd3\xc5\xe3c*Ї\xf5\xe5Q\xa0\xf3u\xe7\x1cOu\xa6\xc6|Be9\xb3\xab\xe8\xc5\t\xe8\x9c\xda\xf1I\x15\xe3\xd9ƛ\xcc:q\xbf\x02<\r\xf2\x88\xeap\x16q\xe6+\xc1G\xd7\u007fC\xbdu\x12\x8f\xec\xaao\xa2\x9e;\tx\xb4\xd6;Uŝ&y\xa2\u009b_\xbb\x9d\x04\xed\xea\xba\xf3\x15\xdb\xd7\xeb\xcbz\r\x17y\\\xd5\xccV]_\xe4Bg\xd4U\x8f\xa9\xa6\xceR\xec\xc4\xcaiS\x19\x1dY\xf7\xd8zi\xbf\x1e:\x024\xa7J:R\x05\x1d\x818Y\x1bͭ}\x8e\xc0\x9e1\xbb\x93R2\xf1g\xe3u\xffJ\xea\x9a\xf2͐\xf3\xb9\xf21)\x1b\x83\xd2iw͞pt\x9d\xe3^X\x91Z\xd2\u007f\x90\x98\bAb҉r-\x96\xf0\x8e\xef\ap]3t\xd2\xe5\xee\u007f\xb1b\xb7\xf5L\x19\xeb~\x95\xe1\xc0vA\x85\x0f\x9cT:\x10\xb6\x03\xc7>fJ2EȞ\xbf;\x17q|:\x18\xdeMcM\xfb\xcf)י\xea\xed\x89\xfese\x98\xa6u\xf2\x10\xd7R\xec\xa8K\x8amq\xdf\xd0\xf3wᾇX\xed\x1d\xa4O\x0f\xcd\xf9Z\x1e\x84\x02$u*\x9e\x911 j\x88~\xe1\xbf\t,\xc4\xc2}\xe6c9\x19\xe5!|;x\xe5\xce`*@\xe5\xf1k\xb5ʂq\xdf\x15\xaaD\x02`ԺL{\xb8\xde\x19w\xef\xfe0(\xf7 v\xae\n\x1a\\\x9e\x99\x86c\xaf)\x94am\x87GP\x80\xfeK\xd4\x03Ͽ\xd5\x18\xf0\x8e{\x1b\x9c\x04{\xb0G\a\xc7*\xad6ڱ\xfa\xd9\x062#C\x93P\xb9hf\xa7\xe5a\xd2\xd4\xe4v\xeb\x9e7\xf69>\xfa\x99\xf5;\xce\x12\x01\x9d\x1e\x03M\x80\xcc\xed\xbe\xcd\xcb\xd8\xcfv۞+\x16\x9a\x8b\x86\xb2\xdd\xc0\xbcn\xdast\xd1\x1e\xd1={DTt\\\\\x94M\xa6\x9c.ٳDGg\x8c\x8f\xce\x11!\x9d\x16#̀<\xe8~\xcd\xe9k\xcd*2e\x97(r\x8aJ\xf3u\xcd\xe9~Ռ>Ռ\xe2\xc7\xdcN3\xfaQ\x8f\xebC͠ᙢ\xa73\xc5O爠\xce\x1bC\xcdFQ\xb3\x923\xf9\xf7\xc99\xf2XM\xfd(J\xbc\x17R\xcf9\xfc\xf7\x87\xe3\x13\x15\xacN\x10$X\t<\x0eM \xe5|\xf9\xe0ǟ\x86T\xba\xd8\x14ֿ\xff2\x87\xcfC3p\x1a\x11\xeb\x92\xc6\xf8,\x81\x87\x9d\xefpQ\x9c\xd4j\x9b\b\xef^\x8ẹ&\xdad\xe2\xe3\xc7\xf6P\xa2ŶS\xb6y\xc6X=\x94\x9d\xab\x87\x0e\xf6d\x1d\x1e\x0fȝ'g\v9eW\x1d\x0f\xfb\xdbT#2\xbfl>\xf9\x9bfO\x9e\x91s\xe9BF\x1b\xd5EYh\xe9rB\x1dbV\x17g|~8m@2?\x8a=\xf9s\xd8yb%\b5\xf6%l\xce\u05ee\u007f*='Ԯ*\xb6X\x1a\x86\x19\x97\xd4\xcdB\xa6o`\x92r>U\xe9\xfau\x83?\xeb\x9al\x1d{\x19\v]\xb6\x15*E6\xf1Z\xa1g\x94\b\x1b\xe4\x96\xc4Sw̴\x8d\xca\xe1\x047\xfd\x12\x96Z\xa4І\x84\x05\xbc\xa5l\x92\xb8\xa9\x04\xa0\xbf\xb9\xcc\x0e!\x9b\xd1sC\xb9\xc6\xcd }\x1a\x9a\xa4\x1f\x90\xa8Û\xee\x06\x84\xf8\xd0\x1d\x1b\x82_O\x03\xffU5q<\xf5\x17\xa3i*q\xcaC\x10n壮˪\xb7Dͩ\xcb{;\xa6\xf9z\xa0s(\x1bM\xf90\xb2\xa7t7\xf3\x02>\xe2s\xe2\xed\a'\xf4.\xa1\x91>J\v\xb8\xe3\xf7Rll\x8c\x91\xf8\xf37B5\xe5\x9b\x0fB\xde3\xb3\xa1\xfcS<\x93\xc7\r\xbe'RS\xc2\xd8\xde\xef'1\xf76\x1e\xe6\xc4\u007f\xf3\xb3G\xfe\x98bR\xc0y6\x1a\xf0\xc3\xda\xe0\x88r\u007f\xd0]\xef\xfeJ\x18\xdd=\x15oT{`ҙY\am\t\x1f\x85Ƙt\xa3}\xa0T\xc1\n\x95^\xe0z-\xa4\xf6\xc1\xd8b\x01t\x1d\x14u*\xc8 \x949_\xc3\xdf\xd3g\x1d\x90\xa6\xf0\xdbX>!\x81\xf0=Hw*\x9c\x93R\x91\xbdo\xad#Ea\xac\x1e\xb8V\x9a\xa4\fڋ\\[\xe7\xdc\x04i\x1e\xc9K\xf4=\xb5\xee\xf8\xe6+@S\xadP\xba\xdea\xfb\xb7'\x9d\xfb\xac\xc0\xab\xa0d\xc1\x01\xdc\xc7\a\x9d\xaf\x9a@\xd9㜎\x8f\xa7\x94\x8f\xfb_h\xc2\xee\xc6\x1d\xb5~#m38\"\xe0\xa6\x0f\xd1\xe8]H6\xde&DU\x9cjyVl\t\xdfX\xf1\x91\xc2l\xb6Q\x04\xc74\xf5X>ĸ\xfb\xd0jwRUL^k#y'\xf7\x12\xd2\xd9e\xbb\xdd)\xa0\xd3$\x9c\xf23{\xa6u\xce\xd3\xec\r~\x99\v\xe1\x16\xb6\x0e\xc4\xf7k\xfaw\x8d\xee~\x9f\xe3\x04|9\x18~\xd0 i݁\x16b0\xdc\t\xe2\xfc']\xc7\xfblW\f\xffk0\xe2\x1b7:>\x13\xc9)\xdf\xcc!\xff[\x18\x96\xf0\x81\x02\x84\x84\x17\x94@\xa2\xf1\x8b\x8e\xf2\x82\xe2&G\xaell<\xa3\x17\xf8A\xc934x\xe9\x04\xb9\xec\x109\xac\x14\u07b4\xf1\x03)\n\xacuh@\xee\xde\xd6|y\xe9~\xc4\xeb\x98\xdd\xcfBp\xaf\x16\xd4\r\xfc\xff\xdf.\"B_\xe2\xad\xcb\xf6\xe5\xbf\x02\x00\x00\xff\xff鐱=\xdaZ\x00\x00"), diff --git a/pkg/apis/velero/v1/download_request_types.go b/pkg/apis/velero/v1/download_request_types.go index c55f957ddc..acb48c4d34 100644 --- a/pkg/apis/velero/v1/download_request_types.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -25,7 +25,7 @@ type DownloadRequestSpec struct { } // DownloadTargetKind represents what type of file to download. -// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupItemOperations;BackupResourceList;RestoreLog;RestoreResults;RestoreItemOperations;CSIBackupVolumeSnapshots;CSIBackupVolumeSnapshotContents +// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupItemOperations;BackupResourceList;RestoreLog;RestoreResults;RestoreResourceList;RestoreItemOperations;CSIBackupVolumeSnapshots;CSIBackupVolumeSnapshotContents type DownloadTargetKind string const ( @@ -36,6 +36,7 @@ const ( DownloadTargetKindBackupResourceList DownloadTargetKind = "BackupResourceList" DownloadTargetKindRestoreLog DownloadTargetKind = "RestoreLog" DownloadTargetKindRestoreResults DownloadTargetKind = "RestoreResults" + DownloadTargetKindRestoreResourceList DownloadTargetKind = "RestoreResourceList" DownloadTargetKindRestoreItemOperations DownloadTargetKind = "RestoreItemOperations" DownloadTargetKindCSIBackupVolumeSnapshots DownloadTargetKind = "CSIBackupVolumeSnapshots" DownloadTargetKindCSIBackupVolumeSnapshotContents DownloadTargetKind = "CSIBackupVolumeSnapshotContents" diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index de249c969e..ed3f4d8c41 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -158,6 +158,11 @@ func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *vel d.Println() d.Printf("Preserve Service NodePorts:\t%s\n", BoolPointerString(restore.Spec.PreserveNodePorts, "false", "true", "auto")) + d.Println() + if details { + describeRestoreResourceList(ctx, kbClient, d, restore, insecureSkipTLSVerify, caCertFile) + d.Println() + } }) } @@ -275,3 +280,34 @@ func groupRestoresByPhase(restores []velerov1api.PodVolumeRestore) map[string][] return restoresByPhase } + +func describeRestoreResourceList(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, insecureSkipTLSVerify bool, caCertPath string) { + buf := new(bytes.Buffer) + if err := downloadrequest.Stream(ctx, kbClient, restore.Namespace, restore.Name, velerov1api.DownloadTargetKindRestoreResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { + if err == downloadrequest.ErrNotFound { + d.Println("Resource List:\t") + } else { + d.Printf("Resource List:\t\n", err) + } + return + } + + var resourceList map[string][]string + if err := json.NewDecoder(buf).Decode(&resourceList); err != nil { + d.Printf("Resource List:\t\n", err) + return + } + + d.Println("Resource List:") + + // Sort GVKs in output + gvks := make([]string, 0, len(resourceList)) + for gvk := range resourceList { + gvks = append(gvks, gvk) + } + sort.Strings(gvks) + + for _, gvk := range gvks { + d.Printf("\t%s:\n\t\t- %s\n", gvk, strings.Join(resourceList[gvk], "\n\t\t- ")) + } +} diff --git a/pkg/controller/download_request_controller.go b/pkg/controller/download_request_controller.go index 391b21f5d3..b35dd9aadb 100644 --- a/pkg/controller/download_request_controller.go +++ b/pkg/controller/download_request_controller.go @@ -121,7 +121,8 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ downloadRequest.Status.Expiration = &metav1.Time{Time: r.clock.Now().Add(persistence.DownloadURLTTL)} if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreLog || - downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults { + downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults || + downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResourceList { restore := &velerov1api.Restore{} if err := r.client.Get(ctx, kbclient.ObjectKey{ Namespace: downloadRequest.Namespace, diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 18a87bd618..aa32f381e8 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -466,7 +466,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu for i := range podVolumeBackupList.Items { podVolumeBackups = append(podVolumeBackups, &podVolumeBackupList.Items[i]) } - restoreReq := pkgrestore.Request{ + restoreReq := &pkgrestore.Request{ Log: restoreLog, Restore: restore, Backup: info.backup, @@ -539,6 +539,10 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu r.logger.WithError(err).Error("Error uploading restore results to backup storage") } + if err := putRestoredResourceList(restore, restoreReq.RestoredResourceList(), info.backupStore); err != nil { + r.logger.WithError(err).Error("Error uploading restored resource list to backup storage") + } + return nil } @@ -585,6 +589,26 @@ func putResults(restore *api.Restore, results map[string]results.Result, backupS return nil } +func putRestoredResourceList(restore *api.Restore, list map[string][]string, backupStore persistence.BackupStore) error { + buf := new(bytes.Buffer) + gzw := gzip.NewWriter(buf) + defer gzw.Close() + + if err := json.NewEncoder(gzw).Encode(list); err != nil { + return errors.Wrap(err, "error encoding restored resource list to JSON") + } + + if err := gzw.Close(); err != nil { + return errors.Wrap(err, "error closing gzip writer") + } + + if err := backupStore.PutRestoredResourceList(restore.Name, buf); err != nil { + return err + } + + return nil +} + func downloadToTempFile(backupName string, backupStore persistence.BackupStore, logger logrus.FieldLogger) (*os.File, error) { readCloser, err := backupStore.GetBackupContents(backupName) if err != nil { diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 334f912493..1b59bfd521 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -452,6 +452,7 @@ func TestRestoreReconcile(t *testing.T) { backupStore.On("PutRestoreLog", test.backup.Name, test.restore.Name, mock.Anything).Return(test.putRestoreLogErr) backupStore.On("PutRestoreResults", test.backup.Name, test.restore.Name, mock.Anything).Return(nil) + backupStore.On("PutRestoredResourceList", test.restore.Name, mock.Anything).Return(nil) volumeSnapshots := []*volume.Snapshot{ { @@ -767,7 +768,7 @@ type fakeRestorer struct { } func (r *fakeRestorer) Restore( - info pkgrestore.Request, + info *pkgrestore.Request, actions []riav2.RestoreItemAction, volumeSnapshotterGetter pkgrestore.VolumeSnapshotterGetter, ) (results.Result, results.Result) { @@ -778,7 +779,7 @@ func (r *fakeRestorer) Restore( return res.Get(0).(results.Result), res.Get(1).(results.Result) } -func (r *fakeRestorer) RestoreWithResolvers(req pkgrestore.Request, +func (r *fakeRestorer) RestoreWithResolvers(req *pkgrestore.Request, resolver framework.RestoreItemActionResolverV2, itemSnapshotterResolver framework.ItemSnapshotterResolver, volumeSnapshotterGetter pkgrestore.VolumeSnapshotterGetter, diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index 2caae0c83b..033ba2ecea 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -327,3 +327,16 @@ func (_m *BackupStore) GetRestoreItemOperations(name string) ([]*itemoperation.R panic("implement me") return nil, nil } + +func (_m *BackupStore) PutRestoredResourceList(restore string, results io.Reader) error { + ret := _m.Called(restore, results) + + var r0 error + if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok { + r0 = rf(restore, results) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 4af7d1d3a4..82d0f5ddb2 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -78,6 +78,7 @@ type BackupStore interface { PutRestoreLog(backup, restore string, log io.Reader) error PutRestoreResults(backup, restore string, results io.Reader) error + PutRestoredResourceList(restore string, results io.Reader) error PutRestoreItemOperations(backup, restore string, restoreItemOperations io.Reader) error GetRestoreItemOperations(name string) ([]*itemoperation.RestoreOperation, error) DeleteRestore(name string) error @@ -537,6 +538,10 @@ func (s *objectBackupStore) PutRestoreResults(backup string, restore string, res return s.objectStore.PutObject(s.bucket, s.layout.getRestoreResultsKey(restore), results) } +func (s *objectBackupStore) PutRestoredResourceList(restore string, list io.Reader) error { + return s.objectStore.PutObject(s.bucket, s.layout.getRestoreResourceListKey(restore), list) +} + func (s *objectBackupStore) PutRestoreItemOperations(backup string, restore string, restoreItemOperations io.Reader) error { return seekAndPutObject(s.objectStore, s.bucket, s.layout.getRestoreItemOperationsKey(restore), restoreItemOperations) } @@ -563,6 +568,8 @@ func (s *objectBackupStore) GetDownloadURL(target velerov1api.DownloadTarget) (s return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreLogKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindRestoreResults: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreResultsKey(target.Name), DownloadURLTTL) + case velerov1api.DownloadTargetKindRestoreResourceList: + return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreResourceListKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindCSIBackupVolumeSnapshots: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getCSIVolumeSnapshotKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindCSIBackupVolumeSnapshotContents: diff --git a/pkg/persistence/object_store_layout.go b/pkg/persistence/object_store_layout.go index 360a3ba5c0..134562337e 100644 --- a/pkg/persistence/object_store_layout.go +++ b/pkg/persistence/object_store_layout.go @@ -105,6 +105,10 @@ func (l *ObjectStoreLayout) getRestoreResultsKey(restore string) string { return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-results.gz", restore)) } +func (l *ObjectStoreLayout) getRestoreResourceListKey(restore string) string { + return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-resource-list.json.gz", restore)) +} + func (l *ObjectStoreLayout) getRestoreItemOperationsKey(restore string) string { return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("restore-%s-itemoperations.json.gz", restore)) } diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index 4636bc8460..04301b6f13 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -622,6 +622,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindRestoreLog: "restores/my-backup/restore-my-backup-logs.gz", velerov1api.DownloadTargetKindRestoreResults: "restores/my-backup/restore-my-backup-results.gz", velerov1api.DownloadTargetKindRestoreItemOperations: "restores/my-backup/restore-my-backup-itemoperations.json.gz", + velerov1api.DownloadTargetKindRestoreResourceList: "restores/my-backup/restore-my-backup-resource-list.json.gz", }, }, { @@ -632,6 +633,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindRestoreLog: "velero-backups/restores/my-backup/restore-my-backup-logs.gz", velerov1api.DownloadTargetKindRestoreResults: "velero-backups/restores/my-backup/restore-my-backup-results.gz", velerov1api.DownloadTargetKindRestoreItemOperations: "velero-backups/restores/my-backup/restore-my-backup-itemoperations.json.gz", + velerov1api.DownloadTargetKindRestoreResourceList: "velero-backups/restores/my-backup/restore-my-backup-resource-list.json.gz", }, }, { @@ -641,6 +643,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindRestoreLog: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-logs.gz", velerov1api.DownloadTargetKindRestoreResults: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-results.gz", velerov1api.DownloadTargetKindRestoreItemOperations: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-itemoperations.json.gz", + velerov1api.DownloadTargetKindRestoreResourceList: "restores/b-cool-20170913154901-20170913154902/restore-b-cool-20170913154901-20170913154902-resource-list.json.gz", }, }, } diff --git a/pkg/restore/request.go b/pkg/restore/request.go new file mode 100644 index 0000000000..ec5745e6fc --- /dev/null +++ b/pkg/restore/request.go @@ -0,0 +1,79 @@ +/* +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 restore + +import ( + "fmt" + "io" + "sort" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/volume" +) + +const ( + itemRestoreResultCreated = "created" + itemRestoreResultUpdated = "updated" + itemRestoreResultFailed = "failed" + itemRestoreResultSkipped = "skipped" +) + +type itemKey struct { + resource string + namespace string + name string +} + +func resourceKey(obj runtime.Object) string { + gvk := obj.GetObjectKind().GroupVersionKind() + return fmt.Sprintf("%s/%s", gvk.GroupVersion().String(), gvk.Kind) +} + +type Request struct { + *velerov1api.Restore + + Log logrus.FieldLogger + Backup *velerov1api.Backup + PodVolumeBackups []*velerov1api.PodVolumeBackup + VolumeSnapshots []*volume.Snapshot + BackupReader io.Reader + RestoredItems map[itemKey]string +} + +// RestoredResourceList returns the list of restored resources grouped by the API +// Version and Kind +func (r *Request) RestoredResourceList() map[string][]string { + resources := map[string][]string{} + for i, action := range r.RestoredItems { + entry := i.name + if i.namespace != "" { + entry = fmt.Sprintf("%s/%s", i.namespace, i.name) + } + entry = fmt.Sprintf("%s(%s)", entry, action) + resources[i.resource] = append(resources[i.resource], entry) + } + + // sort namespace/name entries for each GVK + for _, v := range resources { + sort.Strings(v) + } + + return resources +} diff --git a/pkg/restore/request_test.go b/pkg/restore/request_test.go new file mode 100644 index 0000000000..5ed0af8cf5 --- /dev/null +++ b/pkg/restore/request_test.go @@ -0,0 +1,67 @@ +/* +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 restore + +import ( + "testing" + + "github.com/stretchr/testify/assert" + coreV1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestResourceKey(t *testing.T) { + namespace := &coreV1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + } + assert.Equal(t, "v1/Namespace", resourceKey(namespace)) + + cr := &coreV1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "customized/v1", + Kind: "Cron", + }, + } + assert.Equal(t, "customized/v1/Cron", resourceKey(cr)) +} + +func TestRestoredResourceList(t *testing.T) { + request := &Request{ + RestoredItems: map[itemKey]string{ + { + resource: "v1/Namespace", + namespace: "", + name: "default", + }: "created", + { + resource: "v1/ConfigMap", + namespace: "default", + name: "cm", + }: "skipped", + }, + } + + expected := map[string][]string{ + "v1/ConfigMap": {"default/cm(skipped)"}, + "v1/Namespace": {"default(created)"}, + } + + assert.EqualValues(t, expected, request.RestoredResourceList()) +} diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 53dd56eef6..22029b358f 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -73,25 +73,15 @@ type VolumeSnapshotterGetter interface { GetVolumeSnapshotter(name string) (vsv1.VolumeSnapshotter, error) } -type Request struct { - *velerov1api.Restore - - Log logrus.FieldLogger - Backup *velerov1api.Backup - PodVolumeBackups []*velerov1api.PodVolumeBackup - VolumeSnapshots []*volume.Snapshot - BackupReader io.Reader -} - // Restorer knows how to restore a backup. type Restorer interface { // Restore restores the backup data from backupReader, returning warnings and errors. - Restore(req Request, + Restore(req *Request, actions []riav2.RestoreItemAction, volumeSnapshotterGetter VolumeSnapshotterGetter, ) (Result, Result) RestoreWithResolvers( - req Request, + req *Request, restoreItemActionResolver framework.RestoreItemActionResolverV2, itemSnapshotterResolver framework.ItemSnapshotterResolver, volumeSnapshotterGetter VolumeSnapshotterGetter, @@ -160,7 +150,7 @@ func NewKubernetesRestorer( // and using data from the provided backup/backup reader. Returns a warnings and errors RestoreResult, // respectively, summarizing info about the restore. func (kr *kubernetesRestorer) Restore( - req Request, + req *Request, actions []riav2.RestoreItemAction, volumeSnapshotterGetter VolumeSnapshotterGetter, ) (Result, Result) { @@ -170,7 +160,7 @@ func (kr *kubernetesRestorer) Restore( } func (kr *kubernetesRestorer) RestoreWithResolvers( - req Request, + req *Request, restoreItemActionResolver framework.RestoreItemActionResolverV2, itemSnapshotterResolver framework.ItemSnapshotterResolver, volumeSnapshotterGetter VolumeSnapshotterGetter, @@ -280,6 +270,8 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( credentialFileStore: kr.credentialFileStore, } + req.RestoredItems = make(map[itemKey]string) + restoreCtx := &restoreContext{ backup: req.Backup, backupReader: req.BackupReader, @@ -305,7 +297,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( podVolumeBackups: req.PodVolumeBackups, resourceTerminatingTimeout: kr.resourceTerminatingTimeout, resourceClients: make(map[resourceClientKey]client.Dynamic), - restoredItems: make(map[velero.ResourceIdentifier]struct{}), + restoredItems: req.RestoredItems, renamedPVs: make(map[string]string), pvRenamer: kr.pvRenamer, discoveryHelper: kr.discoveryHelper, @@ -348,7 +340,7 @@ type restoreContext struct { podVolumeBackups []*velerov1api.PodVolumeBackup resourceTerminatingTimeout time.Duration resourceClients map[resourceClientKey]client.Dynamic - restoredItems map[velero.ResourceIdentifier]struct{} + restoredItems map[itemKey]string renamedPVs map[string]string pvRenamer func(string) (string, error) discoveryHelper discovery.Helper @@ -630,12 +622,12 @@ func (ctx *restoreContext) processSelectedResource( // Add the newly created namespace to the list of restored items. if nsCreated { - itemKey := velero.ResourceIdentifier{ - GroupResource: kuberesource.Namespaces, - Namespace: ns.Namespace, - Name: ns.Name, + itemKey := itemKey{ + resource: resourceKey(ns), + namespace: ns.Namespace, + name: ns.Name, } - ctx.restoredItems[itemKey] = struct{}{} + ctx.restoredItems[itemKey] = itemRestoreResultCreated } // Keep track of namespaces that we know exist so we don't @@ -702,6 +694,10 @@ func getNamespace(logger logrus.FieldLogger, path, remappedName string) *v1.Name if nsBytes, err = ioutil.ReadFile(path); err != nil { return &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, ObjectMeta: metav1.ObjectMeta{ Name: remappedName, }, @@ -712,6 +708,10 @@ func getNamespace(logger logrus.FieldLogger, path, remappedName string) *v1.Name if err := json.Unmarshal(nsBytes, &backupNS); err != nil { logger.Warnf("Error unmarshalling namespace from backup, creating new one.") return &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "v1", + }, ObjectMeta: metav1.ObjectMeta{ Name: remappedName, }, @@ -719,6 +719,10 @@ func getNamespace(logger logrus.FieldLogger, path, remappedName string) *v1.Name } return &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: backupNS.Kind, + APIVersion: backupNS.APIVersion, + }, ObjectMeta: metav1.ObjectMeta{ Name: remappedName, Labels: backupNS.Labels, @@ -944,12 +948,12 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } else { // Add the newly created namespace to the list of restored items. if nsCreated { - itemKey := velero.ResourceIdentifier{ - GroupResource: kuberesource.Namespaces, - Namespace: nsToEnsure.Namespace, - Name: nsToEnsure.Name, + itemKey := itemKey{ + resource: resourceKey(nsToEnsure), + namespace: nsToEnsure.Namespace, + name: nsToEnsure.Name, } - ctx.restoredItems[itemKey] = struct{}{} + ctx.restoredItems[itemKey] = itemRestoreResultCreated } } } else { @@ -980,16 +984,29 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso name := obj.GetName() // Check if we've already restored this itemKey. - itemKey := velero.ResourceIdentifier{ - GroupResource: groupResource, - Namespace: namespace, - Name: name, + itemKey := itemKey{ + resource: resourceKey(obj), + namespace: namespace, + name: name, } if _, exists := ctx.restoredItems[itemKey]; exists { ctx.log.Infof("Skipping %s because it's already been restored.", resourceID) return warnings, errs } - ctx.restoredItems[itemKey] = struct{}{} + ctx.restoredItems[itemKey] = "" + defer func() { + // the action field is set explicitly + if action := ctx.restoredItems[itemKey]; len(action) > 0 { + return + } + // no action specified, and no warnings and errors + if errs.IsEmpty() && warnings.IsEmpty() { + ctx.restoredItems[itemKey] = itemRestoreResultSkipped + return + } + // others are all failed + ctx.restoredItems[itemKey] = itemRestoreResultFailed + }() // TODO: move to restore item action if/when we add a ShouldRestore() method // to the interface. @@ -1243,6 +1260,9 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso ctx.log.Infof("Attempting to restore %s: %v", obj.GroupVersionKind().Kind, name) createdObj, restoreErr := resourceClient.Create(obj) + if restoreErr == nil { + ctx.restoredItems[itemKey] = itemRestoreResultCreated + } isAlreadyExistsError, err := isAlreadyExistsError(ctx, obj, restoreErr, resourceClient) if err != nil { errs.Add(namespace, err) @@ -1317,6 +1337,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso errs.Merge(&errsFromUpdate) } } else { + ctx.restoredItems[itemKey] = itemRestoreResultUpdated ctx.log.Infof("ServiceAccount %s successfully updated", kube.NamespaceAndName(obj)) } default: @@ -1334,6 +1355,9 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } else if resourcePolicy == velerov1api.PolicyTypeUpdate { // processing update as existingResourcePolicy warningsFromUpdateRP, errsFromUpdateRP := ctx.processUpdateResourcePolicy(fromCluster, fromClusterWithLabels, obj, namespace, resourceClient) + if warningsFromUpdateRP.IsEmpty() && errsFromUpdateRP.IsEmpty() { + ctx.restoredItems[itemKey] = itemRestoreResultUpdated + } warnings.Merge(&warningsFromUpdateRP) errs.Merge(&errsFromUpdateRP) } diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 6f860bb201..b3c7d3863f 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -569,7 +569,7 @@ func TestRestoreResourceFiltering(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -649,7 +649,7 @@ func TestRestoreNamespaceMapping(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -733,7 +733,7 @@ func TestRestoreResourcePriorities(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -810,7 +810,7 @@ func TestInvalidTarballContents(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -856,12 +856,13 @@ func assertWantErrsOrWarnings(t *testing.T, wantRes Result, res Result) { // with the expected metadata/spec/status in the API. func TestRestoreItems(t *testing.T) { tests := []struct { - name string - restore *velerov1api.Restore - backup *velerov1api.Backup - apiResources []*test.APIResource - tarball io.Reader - want []*test.APIResource + name string + restore *velerov1api.Restore + backup *velerov1api.Backup + apiResources []*test.APIResource + tarball io.Reader + want []*test.APIResource + expectedRestoreItems map[itemKey]string }{ { name: "metadata uid/resourceVersion/etc. gets removed", @@ -893,6 +894,10 @@ func TestRestoreItems(t *testing.T) { Result(), ), }, + expectedRestoreItems: map[itemKey]string{ + {resource: "v1/Namespace", namespace: "", name: "ns-1"}: "created", + {resource: "v1/Pod", namespace: "ns-1", name: "pod-1"}: "created", + }, }, { name: "status gets removed", @@ -999,6 +1004,10 @@ func TestRestoreItems(t *testing.T) { want: []*test.APIResource{ test.ServiceAccounts(builder.ForServiceAccount("ns-1", "sa-1").Result()), }, + expectedRestoreItems: map[itemKey]string{ + {resource: "v1/Namespace", namespace: "", name: "ns-1"}: "created", + {resource: "v1/ServiceAccount", namespace: "ns-1", name: "sa-1"}: "skipped", + }, }, { name: "update secret data and labels when secret exists in cluster and is not identical to the backed up one, existing resource policy is update", @@ -1013,6 +1022,10 @@ func TestRestoreItems(t *testing.T) { want: []*test.APIResource{ test.Secrets(builder.ForSecret("ns-1", "sa-1").ObjectMeta(builder.WithLabels("velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1")).Data(map[string][]byte{"key-1": []byte("value-1")}).Result()), }, + expectedRestoreItems: map[itemKey]string{ + {resource: "v1/Namespace", namespace: "", name: "ns-1"}: "created", + {resource: "v1/Secret", namespace: "ns-1", name: "sa-1"}: "updated", + }, }, { name: "update service account labels when service account exists in cluster and is identical to the backed up one, existing resource policy is update", @@ -1163,13 +1176,14 @@ func TestRestoreItems(t *testing.T) { h.AddItems(t, r) } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, PodVolumeBackups: nil, VolumeSnapshots: nil, BackupReader: tc.tarball, + RestoredItems: map[itemKey]string{}, } warnings, errs := h.restorer.Restore( data, @@ -1179,6 +1193,9 @@ func TestRestoreItems(t *testing.T) { assertEmptyResults(t, warnings, errs) assertRestoredItems(t, h, tc.want) + if len(tc.expectedRestoreItems) > 0 { + assert.EqualValues(t, tc.expectedRestoreItems, data.RestoredItems) + } }) } } @@ -1393,7 +1410,7 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { actions = append(actions, action) } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -1568,7 +1585,7 @@ func TestRestoreActionModifications(t *testing.T) { } } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -1734,7 +1751,7 @@ func TestRestoreActionAdditionalItems(t *testing.T) { h.AddItems(t, r) } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -2728,7 +2745,7 @@ func TestRestorePersistentVolumes(t *testing.T) { } } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, @@ -2862,7 +2879,7 @@ func TestRestoreWithPodVolume(t *testing.T) { Return(nil) } - data := Request{ + data := &Request{ Log: h.log, Restore: tc.restore, Backup: tc.backup, diff --git a/pkg/util/results/result.go b/pkg/util/results/result.go index f211b8b178..7d69c90dab 100644 --- a/pkg/util/results/result.go +++ b/pkg/util/results/result.go @@ -65,3 +65,8 @@ func (r *Result) Add(ns string, e error) { r.Namespaces[ns] = append(r.Namespaces[ns], e.Error()) } } + +// IsEmpty returns true if all collections of messages are empty +func (r *Result) IsEmpty() bool { + return len(r.Velero) == 0 && len(r.Cluster) == 0 && len(r.Namespaces) == 0 +} diff --git a/pkg/util/results/result_test.go b/pkg/util/results/result_test.go index f447c9d1e7..26017c35fa 100644 --- a/pkg/util/results/result_test.go +++ b/pkg/util/results/result_test.go @@ -194,3 +194,19 @@ func TestAdd(t *testing.T) { }) } } + +func TestIsEmpty(t *testing.T) { + result := &Result{ + Velero: nil, + Cluster: nil, + Namespaces: nil, + } + assert.True(t, result.IsEmpty()) + + result = &Result{ + Velero: []string{"error"}, + Cluster: nil, + Namespaces: nil, + } + assert.False(t, result.IsEmpty()) +}