From 4581955aa6391de987c8caf5ce0ebf060a70d603 Mon Sep 17 00:00:00 2001 From: Dave Smith-Uchida Date: Sun, 5 Dec 2021 21:29:10 -0800 Subject: [PATCH 1/2] Adds -itemsnapshots.gz file to backup (when provided). Also adds DownloadTargetKindBackupItemSnapshots type to allow downloading. Updated object store unit test Fixes #3758 Signed-off-by: Dave Smith-Uchida --- changelogs/unreleased/4429-dsmithuchida | 8 ++ .../v1/bases/velero.io_downloadrequests.yaml | 1 + config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/download_request_types.go | 3 +- pkg/persistence/mocks/backup_store.go | 4 + pkg/persistence/object_store.go | 82 +++++++++++------- pkg/persistence/object_store_layout.go | 4 + pkg/persistence/object_store_test.go | 83 ++++++++++++++++--- pkg/volume/item_snapshot.go | 57 +++++++++++++ 9 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 changelogs/unreleased/4429-dsmithuchida create mode 100644 pkg/volume/item_snapshot.go diff --git a/changelogs/unreleased/4429-dsmithuchida b/changelogs/unreleased/4429-dsmithuchida new file mode 100644 index 0000000000..4e4c6b5a4e --- /dev/null +++ b/changelogs/unreleased/4429-dsmithuchida @@ -0,0 +1,8 @@ +Added ``-itemsnapshots.json.gz to the backup format. This file exists +when item snapshots are taken and contains an array of volume.Itemsnapshots +containing the information about the snapshots. This will not be used unless +upload progress monitoring and item snapshots are enabled and an ItemSnapshot +plugin is used to take snapshots. + +Also added DownloadTargetKindBackupItemSnapshots for retrieving the signed URL to download only the ``-itemsnapshots.json.gz part of a backup for use by +`velero backup describe`. diff --git a/config/crd/v1/bases/velero.io_downloadrequests.yaml b/config/crd/v1/bases/velero.io_downloadrequests.yaml index 17517342bc..45f229c44b 100644 --- a/config/crd/v1/bases/velero.io_downloadrequests.yaml +++ b/config/crd/v1/bases/velero.io_downloadrequests.yaml @@ -46,6 +46,7 @@ spec: - BackupLog - BackupContents - BackupVolumeSnapshots + - BackupItemSnapshots - BackupResourceList - RestoreLog - RestoreResults diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index fde97d2ca5..b23a26318c 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -32,7 +32,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec<]o#9r\xef\xfe\x15\x05\xe7a\xee\x00K\xbeE\x1e\x12\xf8m\xd6\xe3E\x84\xdd\xcc\x19;\x8e\xf3\x10\xe4\x81\xea.Ib\x81F\xaf\xa5\xbe\xb2\x15f4\xd7\xde躺\x83\xf6\a?$\xe0\xe1\xd7\xf0#\x8f\xe6\x0f\x85\xb4\xee\xe7\xce\xc7_\xa4u\xfcCU\xd4F\x14\xcdL\xfc\xcdJ\xb5\xaf\va\xe2\xd7+\x00\x9b\xe9\n\xef\xe03MQ\x89\f\xf3+\x80\xb0\x1c\x9er\x15\x10>\xfe\xe0!d\a,\x85\xc7\x05@W\xa8>>n\x9e\xff\xf1K\xef3@\x8e63\xb2rL\x14\x8f\x18H\v\x02\x9eyY`\x02\xf9\xc1\x1d\x84\x03\x83\x95A\x8b\xcaYp\a\x84LT\xae6\bz\a?\xd7[4\n\x1d\xda\x064@V\xd4֡\x01\xeb\x84C\x10\x0e\x04TZ*\aR\x81\x93%\xc2\x1f>>n@o\xff\x82\x99\xb3 T\x0e\xc2Z\x9dI\xe10\x87\xa3.\xea\x12\xfd\xd8?\xae\x1b\xa8\x95\xd1\x15\x1a'#\x9d}\xebHU\xe7\xeb`y\x1f\x88\x02\xbe\x17\xe4$N\xe8\x97\x11\xa8\x88y \x1a\xad\xc7\x1d\xa4m\x97\xcb\x12\xd2\x03\f\xd4I\xa8\x80\xfc\x1a\xbe\xa0!0`\x0f\xba.r\x92\xc2#\x1a\"X\xa6\xf7J\xfew\x03ۂ\xd3\xa3\xc1`u\x0fg\x03ؤ\t\xa9hא\x11!\xf4T\xfb+)\xe6\x91\xc5\t\x83@r+\x95\x87\xc7:\xf7\x80\xa3\f\xa2&\x1d\x96#\xb8M\x8a\x99od*Ŷ\xc0;p\xa6\xc6\t\xca\bc\xc4i\x82.Ѽ\xa7\x92\xa5\xe9\x1f\xb4H!3\xb6?\x8d\xae`\xcaxk%\xcc9F\xf0{&\xcaA\xeb\x97%B\xfc\v\xf5i\xf5\x1ed\xec%\xc1\x16\x0f\xe2(\xb5\tK\x0ffh\x8b\x80_1\xab\x1d\x8eɿp\x90\xcb\xdd\x0e\r\xc1\xa9\x0e¢\xf5\xa6o\x9a \xd3[\x99\x9a\x99f\xe6\xd9:ZF\x92\xa4\xf2ʧP\xa7\r=\xdcW\xb1\x11\xa2d4\xc8mQ\xb9<ʼ\x16\x05He\x9dP\x99_\x8fh\xf0:_\x0f\xcc1\xf9\fg\xaf\x0e#\xe6ĉ\x9ej\xd4\nA\x1b(\xc9\x1e\x9cw\x1dZ\xb0\xb6M-{+H;i/\xa2\xa6.І\xa9rֹ\xad\x0e\xb8\x99\x04\xddp\xc4\xfb\x12\x85\xd8b\x01\x16\v̜6\xe3\xe4Xb\xb2o)zm\x82\x8a#\x1a\xae\xd5ݴ\xd4va3 \x81\xd4\xf6\xebAf\ao\xe6I\x82\x18\x0e\xe4\x1a-\xefrQU\xc5ij\x91\xb0\xc4\xf90\xc9\xdcFo\xdb\u0096\x1f\xc2\x1b\xdb\xfcmKЍm[В}\xca6\xe2\x00N\xcf.\xfb\xff'a\xa3\xda\u007f\x83\xd0nΆ\xbe\xaf\xd0\x12I%\xb9\xf3\x9b\x1d`Y\xb9\xd3\rH\x17\xbf.A$g\xa5\x9d\xff\xef\x981\x97K\xfcf8\xf2]%~\x96+K\x10\x89+\xcd\xf4\u007f\x87Lac\xf1%؊d\x86\xfc\xd2\x1du\x03r\xd70$\xbf\x81\x9d,\x1c\x9a\x01g~\xd3~y\x0fb\xa4\xd8;j\xa5p\xd9\xe1\xe1+y^\xb6M8%\xd2e8\xd8\xfb\xafџ\xef\x1b\xe6\x05\xb8\xc0\x01\xaa4X\xfa\xc0\xf7\x89\xa9\xd9~a\x8f\xea\xe3\xe7O\x98ϑ\a\xd2$\xefl!\x1f\a\xc8v\xa7\x0eNy\xea2\x82\xeb\xd3\xc47>\xa5q\x03\x02^\xf0\xe4=\x16\xa1\x80\x98#h\xa2\x89H\xe7\x9c8\x9c[a!{\xc1\x13\x83\tɒ\xc5ѩ\xa2\xe0\xdb\v\x9eR\xba\r\bH8I\x1b\x92@DI\xfa\xc0\x84\xe0\xd8:\x9dx\xc0\x89\xaf\xa8\x8b\x96\x17\a\xe9\x8a$\xb6H\xfb7,\xb3a['iȌ\xfd`=\x8bh\x17\x1cd\x95\xb8P2s`\x91wKL}=\x8bB\xe6\xcdD^\xee7j\xda\x1b\xee\xb7\xcf\xdam\xd4\r<|\x956d\x1f?i\xb4\x9f\xb5\xe3/߄\x9c\x1e\xf17\x10\xd3\x0f\xe4\xed\xa5\xbc\xda&:tsh\t\xc2\xed\xdb\xc6Gx\r{\xa4\x85\x8d\xa2\xb8%Ѓ3\xa2~\xbay\xfb\xd0oem9I\xa6\xb4Z\xb1\xa9\\\x8f\xcd䉝\bR\x9b\x1eG\xceQk&\xf5\x13&\x82}\"K\xe2\xc7\xfb\x1co!2\xcc!\xaf\x99\x98\x9c\x99\x14\x0e\xf72\x83\x12\xcd~\xcept[E\xfa=\r\x85D\xad\xebۅ\x12\x96f\xdac\v\xaa;_FfE;7\xa1Wd\xf6b\u05c9\x84\xe4t\xd7\xe5\x15\xb1\x89e\xffc\x91\xba\"\xcf\xf9,I\x14\x8f\x17h\xfc\vxqn\xfb=b\xdeB\x96\x82\x93\x8c\xffCf\x8e\x05\xfa\u007f\xa1\x12\xd2$\xec\xe1\x8f|4T`ol\xc8bu\xa7\xa1\x19\xa4\x05\xe2\xefQ\x14\xe7\xa9\xee\x91\xc5i\xd2-XxC\xaewg\x1e\xcb\r\xbc\x1e\xb4\xf56u'q4\xa5\xdao\xd2\xc2\xf5\v\x9e\xaeo\xce\xf4\xc0\xf5F]{\x03\u007f\xb1\xbai\xbc\x05\xad\x8a\x13\\\xf3\xd8\xeb\xdf\xe2\x04%JbR7>\x82Ku\x95)\x96\x8c\x9e\x00\rlΝ\xc8͝\xc3:I\x0e+m]2*\x8f\xda:\x9fY칥\x97d\xb1\xc0\xcbP\xc8^\x81\xd8\xf9\x93?m\xe2\x99\x0e\xa9\xbdA\u0095\xb8f\xe75,\xb1\xb1Ɉy\xa0\x14X]\xb7;\xd8\xeb\xd3k\u007f\xd0Ó\x88\x8c\x9d\x8bE\xb8\x95\xd1\x19Z;/\"\t\xdaz!I\xd8$\b\x85\x0f`\xfc\x81\xc9|R2\xb6t\x87\x94\x88t\xa1+\xff\U000354fd\xa4\xcdO\u007f/\tߥx\x01\xefٲ\x14Ó\xc1$\x14\xef\xfdȸM\x02 \x1f\x1a\x98}\xcd[=݃\f\x82\xf4{0ӥT\x1b\x9e\x00~xw\xb3\xde(I|\x8b\xe3~\x1fǶDo>\xf0\xeeM\xf5\x884g\xee\r\xf68w\x9e\xe7&G1\x11\xa4Ү\x9bN \xb8\x95\xce?X\xd8Ic]\x17\xd1T\xa1\xa8\x17v\u007f\xdb.\x8d\x9cԃ1o\n\x9c\xfe\xecGv\x12Y\a\xfd\x1a\xcfW'\x0f3\xc7\x1a\x1f\n!\xc8\x1dH\a\xa82]+N\xbf\xd0V\xe7)<\v\xbc\x82N&Y\x9a\x82\xa0\x86\xaa.\xd3\b\xb0b\xa9\x93j6O\xd3\xed\xfe\x93\x90ŷ`\x9b\x93%\xeaz\xd6p\xb6\xadǶ'?\xb2wP^\x8a\xaf\xb2\xacK\x10%\x91>5\xec\xd9\xf9\xe2\x98\x1e\xc7\xe1UHǖ\x83\xe0\xb2\x19q\x9a6UU\xa0Kݑ[\xdci\xc3\xfb\xd9\xca\x1c\x1b\xc3\x1c\xa4@+\x10\xb0\x13\xb2\xa8M\xa2\x86\xbc\x88\xb6\x97\xc4\x1aAY\xbc_\x10\x916\xf9\x8aI\x91\x90\x88Mt\x16\xe7\xb5ue\xd2]\xc5G\x83i\xee\xd9RR:\xbag\x95\x91$K\xfa\xbd=\xb4 bB\x9d\xbe\xbbhg\xed\xbb\x8b\xb6о\xbbh\x93\xed\xbb\x8b\xb6ܾ\xbbh\xa1}w\xd1b\xfb\xee\xa2}w\xd1\xe6\xba\xcdi\xeb%\x8c|\xc5\xfdď\x8bX$\x1cOϡ8\x03?TS\xdc\xfb\xea\xfb\xd4\n\xcb\xcd\xf8\xa8\x91\xba\xdaPֿ\xe2\x1b\tc\x12\xd0\x16]\xb4\xa6\xa4)\xb9\xa4\r\x12\xc5\xdb\x17\x10/\x14a&\x95S\x8eWߦ\x14\xfc,\x95\xf9\xf4\xebL\x9b2\x9bXh\xaa\xe3$#t\x887\x1b\xc8\xed\xed\u0590\xf4\xebu\xd8ύ\x98\xfe\xcdkP\x13Jq\x16\np\xe6\vs\xe7\xe85\b=\xfa\x043\xbd\x82\xd1\xdf\r\xbd\x16\xaad\xa6kc\xc2I\x10:q\xfca\xdd\xff\xc5\xe9P)\x03\xaf\xd2\x1dF\x96\xf2z@\xc5gXj\xdf-{\x8d\xf2\x16\xae\x98\f\xe9\bڀ\x92\x05\x93sFZ{\xe4\x85?W>\x84\xbbx_·\x1fi\xb54o\xae\xa0\xe9W\xc8L\xa8\xe8K\x8f\x8c\xd2\v\x85\xd3kd\xe6\x8bZ.\xa9\x8c\x19ֽL\x02]\xae\x87I\x89\x1c\x17j_\xdeP\xf1\x92X\xed\xf8\x9b\x0f\xc6RjZ\xdeTɲX\x10\x98X\xbfүL\x99\ayA\xd5J\x12q\x96+T.\xaeK\tu \xb3\xebH\xaeF\x19\xa93\x99\x05\xf0\xb4\xaaY\xac\x06Y\xf4\x91\xe7\xf1[\xac\xf7\xb8\xa4\xcac\x91bo\xac\xe8h*6&潴\x8e\xa3_\xa71\x014\xa5zc\xa2:c\x02\xe2l\xcdFjM\xc6\x04\xec\x05\xb3;+%3?\x8e_\x84\x84E\xfbV\xfc\xb5$\xea\xad\v\xd3&G3롧\xa29\x8bb?\xe35\x98sPf\x1f/NR\xaf\xae\xd7?\xc6rݔ\x84g\xf0\xb3T\xb9\x97\x13\x12\xf4\x8e\x9f\xc0\x17\x85\xb9(\xa6qWZ\u007fo\x1c\xe8 ҰX\t\xc37ɷ'\x9f\xad\xb0kx\x10١\xdf\x11\x0e\xc2RLZ\x8e\xbaa\xd7M\x98v\x1bGї\xeb5\xc0O\xba\x89\x84\xbb\u05ec\xac,\xab\xe2\x04\xb5E\xb8\xee\x0fy[\xcc1*\x01V\x89\xca\x1et\xbc\x03\xbb\x10v|\xe9\xf7\x1e\x89\xe8\xe3\rج\xd0u\xde@\x9f`\x9eP'x|f߇\xef\x0ef\xed=\xca\xe0\xdf\xc4Hbx\xcd\xf2\xc7\xf7\x8f\xf0\xad\xd3F\xec\xf1\x17\xed/#/Q\xa2\u07fbw\x13=谘q\x8b\x05Yb\x84\b\xe1Z\xf4\x00X\x9bH\x0f\xbb\xa1M~\x10\x96c\xeamf\xff9W,,\xe6\xe9\xe9\x17\xbf\x00'K\\\u007f\xaa}6eU\tc\x91\xa8\x19\x17\xe6\am\xe9\xbf\a\xfd:\xa6\xf0tX\xf3\x8fC\xbc\rr\xb2\x9e\x936\x17a\xef\xafMG\xc1\x8b$Z\x12\xd4\xe7\xf1Q\x9d@\xaf\xc3$\xbf\xcb\xf5ع\xc4\x14\x9c\xce\xeb\x12\x14X\xfbb\xbb\xf7\xbd\xfb;\xe5\xb1Lݿw\xc2\xd5v\xf9\x06>w\x8b\xefm\x84#\x9f\xda\xf0\xc5]\x0f\xc2_t}\xd3%\xfc\x90\xa1\uef412ϧ\xfb\xf3\x11\xfc҅\xc9=j\x9c\x1bonӿ\n\xdbd\xc1GM|\vΏd\x0f\x9a\xa0a\x0exD\x05Zqқ\xaf\xc4\xfa\xd7X\x86cƒI\x1d(!\xab^W\x85\x16y\xdc\xe1\xd1f\x85\x17<\x9eX\u007f\x99#\x9a\x0fv\x06&\xbf\x18\xb0\xd3f\x8c\b\xe7\n\xd3\x1b\x96;ȅ\xc3\xd5(\xd0$\xdd7*l|>\xb4xo\x9e;\xf9\x1dćK\xf1q\x03\u007f\xbaT\xa2\xb5b\x1f/̿\x92\x02ۣB\xb6\x9b#\xeb\x0f\xf1L{\x1aѿ.\xeeS*\"s\xb5\b\x13\xc4lR\xa7ׇ1\xb3R\xe8=\xecd\xc1]\xc3\xd3\x1eA\xb3O\xa9\x1d\xa9\x1c\xeeq\xe8\xae\xe2\xd7J\x9a\x14K\xf0\xd0t$\xdap>\x8d\xb5A\xfb\x04\x0e\x16r/I\x8d\x12\xb3\xf7\xc2l\xc5\x1eW\x99.\n\xe4҅s\xbc\xbe%\xaf=\xec\xd1'nΖ\xf6S\xb7o\xf4\xa7\x82\xb0{8\xf1ś\x9b`\xa1\xc7\x1d\xd4R\xfcE\x9b\x1b(\xa5\xa2\u007f\xc8\r\xe3\xc84\x0e\xbe\xc8\x1e\xf0\xeb\x02\vx?R\x9f\xe6@\xbb\xa3\xdd0\x8aٔ\xff0~\x88\xb9\x82\xcfxn\xee\xfc\xb9$\xe6\x9c{\x19{ׇ\xbalԣ\xd1{\n\x9dF~\xbc\x8f\xbal\xe4\xb7Ga\x9c\x14Eq\xf2\x93L\xce>\xf2\xc3'$m2iR\xc6\xc9\x1a\xb0\\\xa2l\xe8\xd6\xc6iRyI\xe0\x13ŭ\xae]o\x83\xb6\x1b|D,\xe2\x9ck\xf8\xac\x1d\xc64\x9e\xec\xc3$\xbb\x8a֭p\xb7\xd3\xc6\xf9\xb0p\xb5\x02\xb9\v&j\x04.ixN]\xfbgv@\xba6}\xd2J/{\x9f\x06\x85e\xe9u\xfc\xd8\x0f\x9f.\x89,#\x0f\bo\xad\x13ň\xd6\xf8M\xd9j\xf6\x05H\xfa0\xff\xb7\x11\xe3xF\xf0M\xb7\u007fS\xe2_\x97[\x1f\x1d18O9>\xfd\xf7\x1a\xb3\x98ʣl\x11\x15\xbc\x1a\xe9\x1ci\xa9nn\x1f\x1c饢\x00\xaba'&\x1et\x98ӗ\xfc\xbbv\xa2\xd8L\xe7\x94\xfaNg\xd39.\x8b\x87\x9f/N\x13[\xb6L\x82\x89e\xf9\xea2i\xe3Xbev\x10jOBet\xbd?D\xb9\x9c\xb07S\x99\xf8\x9a\x90\x82\xaa\xa8\xf7$\xea!7\xeej\xa3:qqȖ\xe7\x1dtE\xf62\x89i\xc8\x0eƧ\xden\xc3\x13\r\xab\x9d\xd1\xe5*\xf0\x82\xf3\x067!^5R\x93SF\xd1\xd5\x04\xd0\xf6.4\x8bAU\xa1\x02a\x03>\t\xa5o\xf3l\x9d\v\x1e\x9d0.\xd5\xd5\xfb\xd2\xeb\xbc\xe0\xe51\xe4q|\xbf\x84hܗ\x00\xde\x0f\x1fݣ\xb8Y\xc5W\xe6|\xb4\xefE\xc1\x92\xf3g\x90\x03\xa8\xd1\xf3\x8a3\xb7\xad\xe7\xa4\xf5\xd1\xff\xeb\xfag\xc7\xc6\xc2<\xa4xjσ\xee\x83sT\xda\xe5-\xc4\xe0]\x8d\xd0\xe3\x0fr\xe7\x0fR2\xc2\xfa\x8f\u007f\xf3\xf3\xd1c\x92\xcf\xf2a\xd6]aO\xa4\xf1;\xe0\x13V\x063ڽc\xcbx,\x90\xfc\b\x8b\xd8\xf7\x84>\\\xe4H\xf6\x03X\xfb\xd19,\xab\xd1\x19g\"\xd8vؔ\xb2\x14\xb1\xc3\xc8B\xe2\x03\x88\x11X(-\x9a\tY/XP\xe3\xc4\\\xb6\xa0f\xd8Ԃl\x9d\x91\xd2\xda\xd5\xe3欉\x03\xdfyu\xaf\xc2(\xa9\xf6K{\xec\xdfC\xb7\x91x(@\x18\x89\x88F\x96\xd1\xc4H\x8b\x11Q' \x8a8N\xbcK6\b\x92\xde)$\x1a\xb5\x03g\x1fY\x81杽\x1df\n_\xda,\x85\xc82$q\xfd<|\xe8\xf4\xfa\x9a\xff\x88o\x99\xf2\x9f\x99V\xde\xdc\xda;\xf8\x8f\xff\xbc\x82\x90\x06{\x8e\x8f\x96\xd2\xc7\xff\v\x00\x00\xff\xffy\x8fd\x10\x14V\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YK\x8f\xe3\xb8\x11\xbe\xfbW\x14f\x0f}\x19˳\xc9!\x81/\x81Ǔ\x00\x83\xf4l7Ɲ\xce!\t\xb04Y\xb2\xb9M\x91\nI\xd9\xeb\x04\xf9\xefA\xf1!ɒ\xdcv\x0f\x92]^\xba\xcdG\xb1\xea\xab75\x9b\xcf\xe73V\xcbg\xb4N\x1a\xbd\x04VK\xfc٣\xa6_\xaex\xf9\xbd+\xa4Y\x1c\xbe\x9f\xbdH-\x96\xb0n\x9c7\xd5Wt\xa6\xb1\x1c?a)\xb5\xf4\xd2\xe8Y\x85\x9e\t\xe6\xd9r\x06\xc0\xb46\x9eѴ\xa3\x9f\x00\xdcho\x8dRh\xe7;\xd4\xc5K\xb3\xc5m#\x95@\x1b\x88\xe7\xab\x0f\x1f\x8a\xdf\x15\x1ff\x00\xdcb8\xfe$+t\x9eU\xf5\x12t\xa3\xd4\f@\xb3\n\x97\xb0e\xfc\xa5\xa9\x9d7\x96\xedP\x19\x1e\xef*\x0e\xa8КB\x9a\x99\xab\x91\xd3\xd5;k\x9az\t\xddB\xa4\x90؊\"}\f\xc46\x91\xd8}\"\x16֕t\xfeϗ\xf7\xdcK\xe7þZ5\x96\xa9Kl\x85-no\xac\xff\xa1\xbbz\x0e[\xa7\xe2\x8aԻF1{\xe1\xf8\f\xc0qS\xe3\x12\xc2\xe9\x9aq\x143\x80\x84Y\xa06\a&D\xd0\x02S\x8fVj\x8fvmTS\xe9\xf6.\x81\x8e[Y\xfb\x80r\x94\x05\x920\x90\xa5\x01\xe7\x99o\x1c\xb8\x86\xef\x819X\x1d\x98Tl\xabp\xf1\x17\xcd\xf2\xff\x81\x1e\xc0O\xce\xe8G\xe6\xf7K(⩢\xde3\x97W\xa3\x8e\x1e{3\xfeD\x028o\xa5\xdeM\xb1tϜ\u007ffJ\x8aV\xeb \x1d\xf8=\x82b\u0383\xa7\t\xfa\x15\x11\x02\x82\b!#\x04G\xe6\xd2=\x00\x87H%`4ͩ\x1a\xddu\xc66\xb1\x02\xcf\x03*\x91\u007f\x9aI\xdc\xf7\xc8f\xc3/FF{Fw\xb5\xc3K\xc4Π\xf8\x84%k\x94\xef\x8bJZR}\xbb<\x17\xabF^\x88x\xea\xec\xc6Ogs\xf1֭1\nY\xa4\x12w\x1d\xbe\x8fV\xc8\xf7X\xb1e\xdaljԫ\xc7\xcfϿݜMÔ!\r\x9c\x82\x14\xc7z\xba٣Ex\x0e\xfe\x17\xf5\xe6\x92h-M\x00\xb3\xfd\t\xb9\xef\x94X[S\xa3\xf52;K\x1c\xbd ՛\x1d\xf0tGl\xc7] (:a\xb4\xa3\xe4/(\x92\xa4`J\xf0{\xe9\xc0bmѡ\xf6}x[\xc6J`:\xb1W\xc0\x06-\x91!_n\x94\xa0\xa0v@\xeb\xc1\"7;-\xff\xd5\xd2v\xe0M2^\x8f\xce\x0fh\x06\xff\xd4L\x91\xa96\xf8\x1e\x98\x16P\xb1\x13X\xa4[\xa0\xd1=za\x8b+\xe0\vٻԥY\xc2\xde\xfb\xda-\x17\x8b\x9d\xf498sSU\x8d\x96\xfe\xb4\bqVn\x1bo\xac[\b<\xa0Z8\xb9\x9b3\xcb\xf7\xd2#\xf7\x8d\xc5\x05\xab\xe5<\xb0\xaecЬ\xc4w6\x85sww\xc6\xeb\xc8k\xe3\bQ\xf3\x15\rPČV\x10\x8fF):\xa0i\x8a\xd0\xf9\xfa\xc7\xcd\x13䫃2\x86\xe8\aܻ\x83\xaeS\x01\x01&u\x896*\xb1\xb4\xa6\n4Q\x8b\xdaH\xed\xc3\x0f\xae$\xea!\xfc\xae\xd9Vғ\xde\xff٠\xf3\xa4\xab\x02\xd6!c\xc1\x16\xa1\xa9\x83\xdf\x17\xf0YÚU\xa8\xd6\xcc\xe1\xff]\x01\x84\xb4\x9b\x13\xb0\xb7\xa9\xa0\x9fl\x87\x9b#j\xbd\x85\x9c\v/\xe8kҋ75\xf23\xff\x11\xe8\xa4%\v\xf7\xccc\xf0\x8b\x01\xae\xc9\xc5/'\xd3<\xa6\x9d\x9b\x06\xe3\x1c\x9d\xfbb\x04\x0eW\x06,\xafڍg<\xd6h+\xe9BZ\x84\xd2\xd8a\xc6`m\x04\xee\x8f\x1c\xa9\x8a\xd1\x1a\xea\xa6\x1a32\x87\xaf\xc8ăV\xa7\vK\u007f\xb5ҏ/\xba\xa0H\x1a\x91\xc5\xcdI\xf3G\xb4҈+\xc2\u007f\x1clo!؛#\x94\xc1\xac\xb5W'\x8aA\xee\xa4\xf98\xda\xe6\xb1z\xfc\x9c#ot\xa0\xe4o\t\xab\x02V\xc9sM\t\x1f@HG\x05\x80\vD\xc7`QyF\xebK\xf0\xb6y\x93\xf8\xdc\xe8R\xee\xc6B\xf7k\x9aK\x16s\x85\xf4\x00\xb9u\xb8\x89B\x13YGm\xcdA\n\xb4s\xf2\x0fYJ\x9e8il\xcc\\\xa5D%\xdcX\xd2\v^\x16D\xb1(ȫ\x99\xba\xa2\xc3u\xbb1\x94\xc6L\xeah\xc1\x1d\x81\x10ll\x95R\xaa\xf6\xa8E[\x8d\x9cqcB\xd4r(\xe0(\xfd>\x86C5\xe5w\xf0\xaa\xef\xd1x\xc1\xd3\xd4\xf4\x80\xf7\xa7=\xd2Θ@\x11\x1cr\x8b>X\x1b*2\x1f2\xa5\x02\xe0K\xe3B@\x1dƉ\x86\x8d\x19\xf0\x9a\xf9=H\xed\xa4@`\x13\xf0\xc7b킠m\xfe\u007fHQ\xe4\x1b\xd4\xf3\x9a\xb7Gv\xde\xe2\xf0\x19\xe3+\xfe\U000d8db5(\xe4\xdf)\xf2\x9fׂ\x97\xfcxR\xa2C\xfb`\xf0\xa7Xa\xf1\x89Ty\xc6\xcc\xf3\xf8\xc4+\x95Z~\xb6\x98rf\xaa\v\x8c\xb5\xe8j\xa3\x055O\xb7\xd5i\x1d\xcb\xff\xbbjmZ\xad\xf3\xf3(7X\xcbZ\xb8\xa9U\tO4onV\xe2\xc3U\xbf\x150[G\x9dbׯ\fd\xfcEڔw\xbd>\x85\xfaa\r\x8d\x0e\x95Z\xc8\xf8\x05\xfc]\xc3'\xeam);\x89%\xf1m\xa7\f@:\xd0\xe6H\xc7{\xf4\x02\t0:\xe6k\xea֘\x16\xa9\x19\x0eKG\xa9\x14el\x8b\x959Lfl*4-\xaa\x130G\xa6s\xf8M\xf1\xa1x\xf7\xabuA\x8a9OM\r\x8a\xafx\x90\xe3W\x9e1\xba\xf7\xa3\x13\xd9\xf1[w\xa0\x1f?\xe6fyaӶ\x1f'\xc0(\xa5\xa2Zp\"Nt\x15\xc3\xf8=\xf2\xe3\xe6\xfe΅\x12\x1e\xb5\x9f*\xfb\x8eh1tL(\xa8\x8a7\xe9]\xa2q\x1e\xed\x84\x01\xb4\xda\v:\ae\xf4n\xe08q\xa4W\n\xaaТA\x19\v\x02=\xa5&\xbd\x03\xbegz\x87\xdd+T\xe2\xffuN\xc9|\x066\xd3Y\x88ԗ\xcc\xe3&\x8d>ɩ2}\xf4\x02\xdcm\x9e~\xfd\xcd\xdcg\xcd^ls\xae\xe0>ڟ\xb34\x81:\xf7\u074bp7\xbe\xbd\xbd\x1d?7߀\xc4[\xdf\xc2_y׀#sݫ\xf8\xaf\x87C\xf8@p-\x81Ӟ,-o,\xb5i\u074bSp\xb8\xa9\xb8}\xfb\xd3\xcdj\xf0\x1d\xa3\xbf6\xfe\xcaq\x83\\\x93yl4\x19sQ\x0f\xb3\x14Z\xfa3Ͷ}\x85͜\xa7l\b\xff\xfeϬK\x8c\x94}j\x8f\xe2\x87\xe1g\xacw\xd19\xf3\xb7\xa8\xf0\x93S\xc5\x10\xbf\xc3\xc1\xdf\xfe1\x8b\x17\xa3x\xce\x1f\x8fh\xf2\xbf\x01\x00\x00\xff\xffHX\xbeK\x01\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4V=s\xe36\x10\xed\xf9+v.\xc55\x11u7)\x92Q\x97\xf8\xae\xf0$\xf1x\xec\x1b7\x99\x14\x10\xb0\x127&\x01dw!\xc7\xf9\xf5\x19\x00\xa4%Q\xf4\xc5)\u008e\xfb\x85\x87\xf7v\x97lV\xabUc\"= \v\x05\xbf\x01\x13\t\xffR\xf4\xf9M\xda\xc7\x1f\xa4\xa5\xb0>|l\x1eɻ\r\\%\xd10ܡ\x84\xc4\x16?\xe1\x8e<)\x05\xdf\f\xa8\xc6\x195\x9b\x06\xc0x\x1f\xd4d\xb3\xe4W\x00\x1b\xbcr\xe8{\xe4\xd5\x1e}\xfb\x98\xb6\xb8M\xd4;\xe4R|:\xfa\xf0\xa1\xfd\xbe\xfd\xd0\x00Xƒ\xfe\x85\x06\x145C܀O}\xdf\x00x3\xe0\x06\x1c\xf6\xa8\xb85\xf61E\xc6?\x13\x8aJ{\xc0\x1e9\xb4\x14\x1a\x89h\xf3\xc1{\x0e)n\xe0\xe8\xa8\xf9#\xa8z\xa1O\xa5\xd4O\xa5\xd4]-U\xbc=\x89\xfe\xfcZ\xc4/4F\xc5>\xb1\xe9\x97\x01\x95\x00!\xbfO\xbd\xe1Ő\x06@l\x88\xb8\x81\x9b\f+\x1a\x8b\xae\x01\x18\xf9(0W\xe3\x8d\x0f\x1fk9\xdb\xe1`*~\x80\x10\xd1\xffx{\xfd\xf0\xdd\xfd\x99\x19\xc0\xa1X\xa6\xa8\x85\xd5\x05\xfc@\x02\x06F\x14\xa0a\x04\a\xc1#\x04\x86!0BE*\xedK\xd1\xc8!\"+M\xfc\xd5\xe7\xa4uN\xac3\b\xef3\xca\x1a\x05.\xf7\f\nh\x87\xd3Mэ\x17\x83\xb0\x03\xedH\x8012\n\xfa\xdaEg\x85!\a\x19\x0fa\xfb\aZm\xe1\x1e9\x97\x01\xe9B\xea]n\xb5\x03\xb2\x02\xa3\r{O\u007f\xbfԖ|\xcf|hot\x12\xf9\xf8\x90Wdoz8\x98>\xe1\xb7`\xbc\x83\xc1<\x03c>\x05\x92?\xa9WB\xa4\x85_3M\xe4wa\x03\x9dj\x94\xcdz\xbd'\x9dFƆaH\x9e\xf4y]\xba\x9f\xb6I\x03\xcb\xda\xe1\x01\xfb\xb5\xd0~e\xd8v\xa4h51\xaeM\xa4U\x81\xee\xcbش\x83\xfb\x86\xc7!\x93\xf7gX\xf597\x8c(\x93ߟ8J7\u007fE\x81\xdc\xcbU\xf6\x9aZoq$:\x9b2;w\x9f\xef\xbf\xc0tt\x11c\xce~\xe1\xfd\x98(G\t2a\xe4w\xc8U\xc4\x1d\x87\xa1\xd4D\xefb \xaf\xe5\xc5\xf6\x84~N\xbf\xa4\xed@*SKf\xadZ\xb8*{\x04\xb6\b):\xa3\xe8Z\xb8\xf6pe\x06쯌\xe0\xff.@fZV\x99طIp\xba\x02\xe7\xc1\x95\xb5\x13Ǵ\xa3^\xd1kah\xef#ڬ`&1gӎl\x19\x0f\xd8\x05\x86\xa7\x8el7\r\xed\x8cݗ\x01o\xcf\x1c\xcb\x03\x9d\x9fZ&/\xa5\xb9\xe7\xd5\xcbCю\x18g]\xb8:)\xf6&^\xd4h\x92\xff\xc8Lə\xb8\xb1\x89\x19\xbd\x8e\x95ʶXJz+\x17\xc8\x1c\xf8\xc2:\x03\xf5\xb9\x04\x95\xef\x9c!/`\xfc\xf3\x98\b\xda\x19\x85'\xe4<\x066\xa4\xbcgЁK\x17\xfc\x8d\xb4tX\xc5\xca\xc2F\x0e\x16Eڋ8R\x1c\x160}E\x9d\xfc\xe4o\xa8\xd9\xf6\xb8\x01儯(k\x98\xcd\xf3\xcc\x17;#\v\xadpF\xc1m\x8eY\xd2\x00\xebV\xc7\u007f\x17\xa1\xd0\xed\xd3py\xd2\nn\xf0i\xc1z\xedo9\xec\x19e\xde\xf2\xd9y[\xd9+\xdf\xd47\xb2\xb4ؔ\x17F\xc9\xfbΝ\xb0(\x1a\xd8\xec'^\x8f-l\xacŨ\xe8n\xe6\u007f\x1d\xefޝ\xfd>\x94W\x1b\xbc\xa3\xfa\xd3\x04\xbf\xfd\xdeԪ\xe8\x1e\xa6\xbf\x81l\xfc'\x00\x00\xff\xff\x8c\xdb\x1fܮ\t\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WMo\xdc6\x10\xbd\xebW\f\xd2CZ \xd2&\xe8\xa1\xc5\xdeZ'\x87\xa0i\x10ة/E\x0f\\jVbM\x91,g\xb8\x8e[\xf4\xbf\x17CJ\xfb\xa1\xd5\xdaΡ\xba-9\x1c>>\xbe7\x9c\xad꺮T0\xb7\x18\xc9x\xb7\x06\x15\f~at\U0008b6bb\x1f\xa91~\xb5{S\xdd\x19\u05ee\xe1*\x11\xfb\xe1\x1aɧ\xa8\xf1-n\x8d3l\xbc\xab\x06d\xd5*V\xeb\n@9\xe7Y\xc90\xc9O\x00\xed\x1dGo-ƺC\xd7ܥ\rn\x92\xb1-Ɯ|\xdaz\xf7\xba\xf9\xa1y]\x01\xe8\x88y\xf9g3 \xb1\x1a\xc2\x1a\\\xb2\xb6\x02pj\xc05\xb4\xfe\xdeY\xafڈ\u007f%$\xa6f\x87\x16\xa3o\x8c\xaf(\xa0\x96M\xbb\xe8SX\xc3a\xa2\xac\x1d\x01\x95ü\x1d\xd3\\\x974y\xc6\x1a\xe2_\x96f?\x981\"\xd8\x14\x95=\a\x91'ɸ.Y\x15Ϧ+\x00\xd2>\xe0\x1a>\n\x8c\xa04\xb6\x15\xc0x\xf6\f\xab\x1eO\xb7{SR\xe9\x1e\aU\xf0\x02\xf8\x80\xee\xa7O\xefo\xbf\xbf9\x19\x06h\x91t4\x813\x833\xcc`\b\x14\x8c\b\x80\xfd\x1e\x14(\a*\xb2\xd9*Ͱ\x8d~\x80\x8d\xd2w)\xec\xb3\x02\xf8͟\xa8\x19\x88}T\x1d\xbe\x02J\xba\a%\xf9J(X\xdf\xc1\xd6Xl\xf6\x8bB\xf4\x01#\x9b\x89\xe5\xf2\x1d\x89\xebht\x06\xfc\xa5\x9c\xadDA+\xaaB\x02\xeeq\xe2\aۑ\x0e\xf0[\xe0\xde\x10D\f\x11\t]\xd1\xd9Ib\x90 \xe5\xc6\x134p\x83Q\xd2\x00\xf5>\xd9Vĸ\xc3\xc8\x10Q\xfbΙ\xbf\xf7\xb9I\x18\x92M\xad\xe2I\x0e\x87\xcf8\xc6蔅\x9d\xb2\t_\x81r-\f\xea\x01\"f\x9e\x92;ʗC\xa8\x81_}D0n\xeb\xd7\xd03\aZ\xafV\x9d\xe1\xc9T\xda\x0fCr\x86\x1fV\xd9\x1ff\x93\xd8GZ\xb5\xb8C\xbb\"\xd3\xd5*\xea\xde0jN\x11W*\x98:Cw\xd9X\xcd\xd0~\x13G\x1b\xd2\xcb\x13\xac\xfc 2#\x8e\xc6uG\x13Y\xf3\x8f܀\xa8\xbe\b\xa6,-\xa78\x10-C\xc2\xce\xf5\xbb\x9b\xcf0m\x9d/c\xce~Q\xce~!\x1d\xae@\b3n\x8b\xb1\\bV\x9e\xe4D\xd7\x06o\x1c\xe7\x1f\xda\x1ats\xfa)m\x06\xc34\x89Y\uea81\xab\\i`\x83\x90B\xab\x18\xdb\x06\xde;\xb8R\x03\xda+E\xf8\xbf_\x800M\xb5\x10\xfb\xbc+8.\x92\xf3\xe0\xc2\xda\xd1\xc4T\xc9.\xdc\xd7\xcc\xea7\x01\xb5ܞ\x10(+\xcd\xd6\xe8l\r\xd8\xfa\b\xea\xe0\xfc\x91\xc0\xe6$\xf3\xb2s38\x15;\xe4\xf9\xe8\f\xcb\xe7\x1c$\xdb\xdf\xf7\xea\xb4\xd0|\x8bM\xd7H\xad\xa0\x11H\xa9\x1e\xdf5g\x19/c\x80E\xf5.\"\x99D,4\b\xafR\n\xa4H\x1dc:\xdfZ>tiXޠ\x86\x9f3\xe6\x0f\xbe{t\xfe\xca;\x16\xb9?\x1at\xebm\x1a\xf0Ʃ@\xbd\u007f\"vzf\xf7O\xcfy\xe05J\x81\xc6\xcb\xd0ƀk\xa4d/lwA\xacӗ\x1f\xa5\xa7\x99\x97gmb^\x96\x94J\x8d \x8f}t\xc8H\x87\xa2qo\xb8_\xcc\bp\xdf\x1b\xdd\xe7\x85\xf9ڤ\x1e\x11ym\xb2\xbb\xbf\x1e\xbe\xa8\xddD\\\x90N\x9d%\xb50,\xe0φ/x\xf4\xd2\x06\xf5\xe8\x9bg\xf9\x9c\x15'\xfa\n\xa7\xe7\xf8\x89j\x9dbD\xc7c\x96\xfc\xf2\xcd\x17<\xd7\xea\x93?~\xbb\xfe\xf0\x84\xdf\xdf\x1e\"so\xa7\x8c+hBĚL'\xef\xb5̉\xe3\xb3\x13\xcf\xc9(\xdfi\xffpJ\xd4\xe2\x8d\xe2\x97`b\xaekO@|\xb7\x0f,e\t]yr\xe6\x1dRN\x88\x94\x9fs\xad捄|\x1b\x84\x16-2\xb6\xb0y(\xf5\xf5\x81\x18\x87s\xdc[\x1f\a\xc5k\x90\xa7\xa8f\xb3 #\xe9b\xd5\xc6\xe2\x1a8\xa6K*[\xaa\f\xb8X\x05k\xf8\x88\xf7\v\xa3\x9f\xa2\xd7H\x84\xe76\xbax\x92E\x13\x9c\r\x92\xf4\v\xed\x11Kc\x1bz<\x926\xfb\xfegB>\xd1U]ו\n\xe6\x16#\x19\xef֠\x82\xc1/\x8cN\xbe\xa8\xb9\xfb\x91\x1a\xe3W\xbb7՝q\xed\x1a\xae\x12\xb1\x1f\xae\x91|\x8a\x1a\xdf\xe2\xd68\xc3ƻj@V\xadb\xb5\xae\x00\x94s\x9e\x95L\x93|\x02h\xef8zk1\xd6\x1d\xba\xe6.mp\x93\x8cm1f\xe7\xd3ѻ\xd7\xcd\x0f\xcd\xeb\n@G\xcc\xdb?\x9b\x01\x89\xd5\x10\xd6\xe0\x92\xb5\x15\x80S\x03\xae\xa1\xf5\xf7\xcez\xd5F\xfc+!15;\xb4\x18}c|E\x01\xb5\x1c\xdaE\x9f\xc2\x1a\x0e\ve\xef\b\xa8\\\xe6\xed\xe8溸\xc9+\xd6\x10\xff\xb2\xb4\xfa\xc1\x8c\x16\xc1\xa6\xa8\xec9\x88\xbcH\xc6uɪx\xb6\\\x01\x90\xf6\x01\xd7\xf0Q`\x04\xa5\xb1\xad\x00ƻgX\xf5x\xbbݛ\xe2J\xf78\xa8\x82\x17\xc0\at?}z\u007f\xfb\xfd\xcd\xc94@\x8b\xa4\xa3\t\x9c#8\xc3\f\x86@\xc1\x88\x00\xd8\xefA\x81r\xa0\"\x9b\xad\xd2\f\xdb\xe8\a\xd8(}\x97\xc2\xde+\x80\xdf\xfc\x89\x9a\x81\xd8G\xd5\xe1+\xa0\xa4{P⯘\x82\xf5\x1dl\x8d\xc5f\xbf)D\x1f0\xb2\x99\xa2\\\xc6\x11\xb9\x8efg\xc0_\xca݊\x15\xb4\xc2*$\xe0\x1e\xa7\xf8`;\x86\x03\xfc\x16\xb87\x04\x11CDBWxv\xe2\x18\xc4H\xb9\xf1\x06\r\xdc`\x147@\xbdO\xb6\x152\xee02DԾs\xe6\xef\xbdo\x92\bɡV\xf1D\x87\xc30\x8e1:ea\xa7l\xc2W\xa0\\\v\x83z\x80\x889N\xc9\x1d\xf9\xcb&\xd4\xc0\xaf>\"\x18\xb7\xf5k\xe8\x99\x03\xadW\xab\xce\xf0TT\xda\x0fCr\x86\x1fV\xb9>\xcc&\xb1\x8f\xb4jq\x87vE\xa6\xabUԽaԜ\"\xaeT0u\x86\xeera5C\xfbM\x1cː^\x9e`\xe5\a\xa1\x19q4\xae;ZȜ\u007f$\x03\xc2\xfaB\x98\xb2\xb5\xdc\xe2\x10h\x99\x92\xe8\\\xbf\xbb\xf9\f\xd3\xd19\x19\xf3\xe8\x17\xe6\xec7\xd2!\x05\x120\xe3\xb6\x18K\x123\xf3\xc4'\xba6x\xe38\u007fhk\xd0\xcd\xc3Oi3\x18\xa6\x89̒\xab\x06\xae\xb2\xd2\xc0\x06!\x85V1\xb6\r\xbcwp\xa5\x06\xb4W\x8a\xf0\u007fO\x80D\x9aj\t\xec\xf3Rp,\x92s\xe3\x12\xb5\xa3\x85I\xc9.\xe4kV\xea7\x01\xb5dO\x02(;\xcd\xd6\xe8\\\x1a\xb0\xf5\x11ԡ\xf2\xc7\x006'\x9e\x97+7\x83S\xb1C\x9e\xcfΰ|\xceFr\xfc}\xafN\x85\xe6[l\xbaF\xb4\x82F E=\xbek\xce<^\xc6\x00\x8b\xec]D2\x91X\xc2 q\x15)\x10\x91:\xc6t~\xb4\ftiX>\xa0\x86\x9f3\xe6\x0f\xbe{t\xfd\xca;\x16\xba?jt\xebm\x1a\xf0Ʃ@\xbd\u007f\xc2\xf6=\xe3\xf0<\xcb\xe9A\xde?R\xe7\x86\xd7(R\x8e\x97/1\x1a\\#%{\xe1\xb8\v\xb4\x9eF~\xbe\x9eΑ<\x80S\x8edK\xd1t\x04i\v\xa2CF:\xc8˽\xe1~\xd1#\xc0}ot\x9f7\xe6\x04\x8br\x11ym\xb2\x0e|=|\xa9\v\x13q\x81du&\xdf´\x80?\x9b\xbeP͗\x0e\xa8\xc7\n{\x96\"\xb0\xe2D_\xa1\t\xd9~\n\xb5N1\xa2\xe3\xd1K~#\xe7\x1b\x9e+\nS%\xfdv\xfd\xe1\tex{\xb0\xcc]\xa02\xae\xa0\t\x11k2\x9d\xbc\xec\xb2&ڐk\xf6<\x18e\x9cv\x1a\xa7\x81Z\xcc(~\t&f\x05|\x02\u2efda\x110t\xe5q\x9a\xf7R\xd9!R~\xf8\xb5\x9a\xb7\x1c26\b-Zdla\xf3P\x94\xf8\x81\x18\x87s\xdc[\x1f\a\xc5k\x90G\xabf\xb3@#\xe9w\xd5\xc6\xe2\x1a8\xa6K,[\xbcx\xe8\x15-\x94\xe1ɝ?\x89\xcd\x121\xf6\xc5\xf8(3\xe0\xa2^\xd6\xf0\x11\xef\x17f?E\xaf\x91\b\xcf\xcb\xe8\xe2M\x16\x8b\xe0l\x92\xa4\xb3h\x8f\xa246\xac\xc73i\xb3\xef\x94&\xc4c)\xc1?\xffV\x87\xaaRZc`l?\xce\u007f\x14^\xbc8\xe9\xfc\xf3\xa7\xf6\xae5\xe5\x1f\a~\xff\xa3*\ac{;5\xf42\xf9_\x00\x00\x00\xff\xff\xcbT\xc3P]\r\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Y_o\x1b\xb9\x11\u007fק\x18\xf8\x1e\xdc\x03\xbc\xd2ݵh\v\xbd]\xec^\xe1\xf6\xce1\"7/A\x1eF\xcb\xd9]\xd6\\\x92%\xb9RԢ߽\x18\x92\xab?\xab\x95d\x1bH\xba/\x89\xc9\xe1p\xfe\xcfo\xa8IQ\x14\x13\xb4\xf2#9/\x8d\x9e\x03ZI_\x02i\xfe\xcbO\x9f\xff\xec\xa7\xd2\xccV?N\x9e\xa5\x16s\xb8\xed|0\xed\a\xf2\xa6s%\xddQ%\xb5\f\xd2\xe8IK\x01\x05\x06\x9cO\x00Pk\x13\x90\x97=\xff\tP\x1a\x1d\x9cQ\x8a\\Q\x93\x9e>wKZvR\tr\x91y\u007f\xf5\xea\x87韦?L\x00JG\xf1\xf8\x93l\xc9\al\xed\x1ct\xa7\xd4\x04@cKs\xb0F\xac\x8c\xeaZZb\xf9\xdcY?]\x91\"g\xa6\xd2L\xbc\xa5\x92/\xad\x9d\xe9\xec\x1cv\x1b\xe9l\x16()\xf3h\xc4\xc7\xc8\xe6]d\x13w\x94\xf4\xe1\xefc\xbb\xbfJ\x1f\"\x85U\x9dCu,D\xdc\xf4RםBw\xb4=\x01\xf0\xa5\xb14\x87\a\x16\xc3bIb\x02\x90u\x8fb\x15Y\xbbՏ\x89U\xd9P\x8bI^\x00cI\xff\xfcx\xff\xf1\xf7\x8b\x83e\x00\xeb\x8c%\x17d\xafZ\xfa\xf6<\xba\xb7\n ȗN\xda\x10\xed}\xcd\f\x13\x15\bv%y\b\r\xf5B\x91\xc82\x80\xa9 4҃#\xebȓN\xce=`\fL\x84\x1a\xcc\xf2\x9fT\x86),\xc81\x1b\xf0\x8d\xe9\x94\xe0\bX\x91\v\xe0\xa84\xb5\x96\xff\xde\xf2\xf6\x10L\xbcTa\xa0l\xe1\xdd'u \xa7Q\xc1\nUG7\x80Z@\x8b\x1bpķ@\xa7\xf7\xf8E\x12?\x85ߌ#\x90\xba2shB\xb0~>\x9b\xd52\xf4\x91\\\x9a\xb6\xed\xb4\f\x9bY\fJ\xb9\xec\x82q~&hEj\xe6e]\xa0+\x1b\x19\xa8\f\x9d\xa3\x19ZYD\xd1u\x8c\xe6i+\xbes9\xf6\xfd\xf5\x81\xacaþ\xf5\xc1I]\xefm\xc4@;\xe3\x01\x0e5\x90\x1e0\x1fMZ\xec\f\xcdKl\x9d\x0f\u007fY[S\xe1\x87`F$[&\x17\x90\x80a@\xc0٠\x803U}T\xe2\x9f\x1f\xef\xfbJ\xde\x1b1\xcb\x1e\x8e\xef\xbd`\x1f\xfe*IJ\xbc\x9ft;!S\xe69Y\xd7\xe4\"p\x19\xfbbS\xe1R\xfd=\x18\xc7\x16\xd0f\x8fEd\xcc\xdeK\x85\x92đП~\xfa|R\xe2C{\x81Ԃ\xbe\xc0O u\xb2\x8d5\xe2\xfb)<\xc5\xe8\xd8\xe8\x80_\xf8\xa6\xb21\x9eNY\xd6h\xb5a\x9d\x1b\\\x11x\xd3\x12\xacI\xa9\"\xe1 \x01kܰ\x15z\xc7q\xbc!Xt\xe1l\xb4\xf6\xe8\xe7\xe9\xfd\xdd\xfby\x92\x8c\x03\xaa\x8e\x95\x98\xbbf%\x19\xcd0\x8cI\xbd8F\xe3Q3\xef?ߥ\xf0\t\x06\xca\x06uMI_\x82\xaa\xe3\xee8\xbd~K\x1e\x1fC\x92\xfe\x1b\x81&\xc3\xc2\xf1\u007fk\xee/T.\"\xe8\x17(\xf7\xb0\x17\xe5g\x95\xe3Y\xc5i\n\x14\xf5\x13\xa6\xf4\xacZI6\xf8\x99Y\x91[IZ\xcf\xd6\xc6=K]\x17\x1c\x9aE\x8a\x01?\x8b\xe3\xc6\xec\xbb\xf8ϛu\x89\x83\xc2K\x15\x8a\xc4\xdfB+\xbe\xc7\xcfޤT\x8fa_\xdeǮ\x17\x19Y\r\xcfrZ\xac\x1bY6\xfdp\x92k\xec\x89d\x92\x8c\x84E*ͨ7_=\x94٠\x9dc\x896E\x1e\x80\vԂ\xff\xef\xa5\x0f\xbc\xfe&\vv\xf2E\xe9\xfb\x8f\xfb\xbbo\x13\xe0\x9d|S\xae\x9e\x00\xe0)F\xac\xb9\x17l\xcaJ\x92\xbb\x00\xcc>\x1c\x10\xf7\xd0q\x04\xb1ni^\x85\f\x03\xd6#P\f\x85\x88\xcf\x1e\xa8\x1e\xcf\x02\xb6\xb3\x168P\xe3\tk\x0f\xe8\b\x10Z\xb4\xec\xb9g\xda\x14\xa9\xc5[\x94ܟ\xb9\x05g̳$@k\x95\x1cmŹ\x91g\x10\x9a\xf1>\x0f\xdaX\xfbS\xba\x8f\xfa!q\xb8`\xff4\xe0\x8cA\xf6,@\xc27[\xd8\x1e\f,\xc7R\xf4\f(>iE\x9eK\x19\xad\x1d\x8aX\x8c\x0fP\x03\x1a\x1e(\x06Kֈ\xc1\xcaa$\x0e6\x93~/\x9a*\x03\x86οb\xae\x8c\xf4\xbdMS\x15\t\x99K\x84\xd0o\x9d,K\xc3\xe8\xf4\xf0i\xed\xbc{o\x8fO\xc4G\x1c'\x92pA\xb6\x1c\xb39\xca\xd6\xe8\xfb;\xc6FC\xd8c\x97Nƺ\xcd\xdcHD\xe8\xc8ȶB\xa9H@\xff\xb67<3\xc2u\x9f˒*.r\x9dU\x06E?\x90e\xf1\xb6\xf0\x8c\xe7\xf5\xf8:r\xed\xcf\xf0\xec<\x898ɏ\x18\xe1\x18\xb2UƵ\x18\xe6 0P1\xcaTwJ\xe1R\xd1\x1c\x82뎷\xcf\x14\x8b\x96\xbc\xc7\xfaR*\xfe\x96\xa8Ҝ\x9a\x8f\x00.M\x17\xb6\x83\xeaAQ\xb8\xf69\xa6^7+\x8f\x8e\x80\x87\xe1\x8c\f\xd1}\x86\xaaJ\xc53\xfb\x85`\xf7 \x1c\xa5Z\xd2x\xab{KM\x00\xb0\r\xfaK\xa6zd\x9a\xb1\x04\xdbV\xaf\xb3\x19\xc6\x1f\xe9\xae=\xbe\xa5\x80\aZ\x8f\xac\xde\xebGgjG\xfe8p\x8a>\xbeF\xaay\x01\xbf\xc4lx\x95\xfe\xf9\xa2K&\xc8d\xd0\x18\xd5'\xb3\t\xa8@w\xed\x92\x1c\xdba\xb9\t\xe4\x0f\xcb\xf9ثD\x9cfvf\xdc;\xdf\xfb/q\xca\x03Z\x89:\xbe\x1erv\x05\x03Bz\xabp3¸W$\"\x16N..\x01\xbbx\xee\x93ڒ\x8b[\xaf}M\x892\xdd\x19}\x02_\xf7\xf9,u\xf8\xe3\x1f\xce\xe0\x1b\xa9\x03Ճ\xe6\x90\xf7ٜ\xef\xf8\x96\xafsÙ\xd6\xed5Zߘp\u007fw!\n\x16[\xc2>\x1bv@)־\xf8\xb6\x99\x89r(\x8c\xb9j[[^\x95\xaa>\xa0\v/mE\x8b\x03\xe2\v](r\x1e\xefA\v\xb2\xe88\xd3\xe3K\xf8\xed\xf0\xb7\xa6\x1b\xf02>\xef1\xdeJ\x00,\rߞ\x9b\x13\x03K\xe3h\xa4d\xc2q[9h\"\x87\xe2\u007f\xcb\xfe1\x1a'G\x8bQr\xb1\xc7;?\x11\xe7\x95\x1d\x86\xc1\x92\xa7\x03\x12\x0f\xc3\xdfӮ\xd2\xebM\xff\x03Y\xfc\xb34:Ae?\x87O\x9f'\x90\x9f\x8d?\xf6\xbf{\xf1\xe2\xff\x02\x00\x00\xff\xffTTw\xa4\x84\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\xe3\xb8\x11\xbe\xebWty\x0f\xceV\r\xa9\xddI*I\xe9\xb6kgSJv=\xae\x913\x97\xa99@DS\xec\x98\x04\x18\xa0)YI忧\x1a \xf4\xa4\x1ev\xd5Lx\xb1\x85G\xe3\xeb\xaf\x1f\xe8&GY\x96\x8dTK\x9f\xd0y\xb2f\x02\xaa%|a4\xf2\xcb\xe7\xcf\u007f\xf69\xd9\xf1\xf2\xc7\xd13\x19=\x81\xbbγm>\xa2\xb7\x9d+\xf0\x1eK2\xc4dͨAVZ\xb1\x9a\x8c\x00\x941\x96\x95\f{\xf9\tPX\xc3\xce\xd65\xbal\x81&\u007f\xee\xe68\xef\xa8\xd6\xe8\x82\xf0t\xf4\xf2\x87\xfcO\xf9\x0f#\x80\xc2a\xd8\xfeD\rzVM;\x01\xd3\xd5\xf5\b\xc0\xa8\x06'\xd0Z\xbd\xb4uנC\xcf֡ϗX\xa3\xb39ّo\xb1\x90S\x17\xcev\xed\x04\xb6\x13qs\x8f(j\xf3h\xf5\xa7 \xe7c\x94\x13\xa6j\xf2\xfc\xf7\xc1\xe9_\xc9sX\xd2֝S\xf5\x00\x8e0\xeb\xc9,\xbaZ\xb9\xe3\xf9\x11\x80/l\x8b\x13x\x10(\xad*P\x8f\x00z\x02\x02\xb4\xacWq\xf9c\x94UTب\x88\x19\xc0\xb6h~z\x9c~\xfa\xfdlo\x18\xa0u\xb6EǔԋώYwF\x014\xfa\xc2Qˁ\xf4[\x11\x18W\x81\x16{\xa2\a\xae0\x81B\xddc\x00[\x02W\xe4\xc1a\xebУ\x89\x16\xde\x13\f\xb2H\x19\xb0\xf3\u007fb\xc19\xccЉ\x18\xf0\x95\xedj-n\xb0D\xc7ర\vC\xff\xde\xc8\xf6\xc06\x1cZ+ƞ\xe3\xedC\x86\xd1\x19U\xc3R\xd5\x1d\xbe\x03e44j\r\x0e\xe5\x14\xe8̎\xbc\xb0\xc4\xe7\xf0\x9bu\bdJ;\x81\x8a\xb9\xf5\x93\xf1xA\x9cܹ\xb0M\xd3\x19\xe2\xf58x&\xcd;\xb6Ώ5.\xb1\x1e{Zd\xca\x15\x151\x16\xdc9\x1c\xab\x96\xb2\x00\xdd\x04\x97\xce\x1b\xfd\x9d\xeb\x03\xc0\xdf\xeea\xe5\xb5\xd8ֳ#\xb3ؙ\b\xcev\xc6\x02\xe2m@\x1eT\xbf5j\xb1%Z\x86\x84\x9d\x8f\u007f\x99=A::\x18\xe3\x90\xfd\xc0\xfbv\xa3ߚ@\b#S\xa2\x8bF,\x9dm\x82L4\xba\xb5d8\xfc(jBsH\xbf\xef\xe6\r\xb1\xd8\xfd_\x1dz\x16[\xe5p\x17b\x1c\xe6\b]\xab\x15\xa3\xceaj\xe0N5X\xdf)\x8f_\xdd\x00´τ\xd8\xebL\xb0\x9b\x9e\x0e\x17G\xd6v&R\n9a\xafô0k\xb1\x10\xf3\t\x83\xb2\x95J*Bl@i\x1d\xa8\xa3\xf5\xf9\x9e\xe8\xe1Еg\xae\x8a箝\xb1uj\x81\xbf\xda(\xf3p\xd1\x01\xb6\x9f\x87\xf6$p\x92Yb\x18c/\x1c|\\y$\x14\xa0N\x9bW\x15:\f{$\x8bQ!\xeee=\xb1uk\x11\x1cT\xd2\xf9\x91\x84\x13\x86\b*[}A\x8dG\xdb\a\x84\xc3\x12\x1d\x1aq\xf7\x98!Z\x1b\xf2\b+2),b\x8a\x05\xb6\x03Z\xcc#\xeaa\x88\xa7\xa9\x873\xd9s\x10\xf0O\x8fӔ1\x13\xc3=t>>\xf7\x02=\U00094135~T\\]q\xf6\xed\xb4\x8c\x87\x85\xdc\xc1\x16\x14\xb4\x84\x05\xee%c \xe3\x19\x95\x06[\x0eJ\x94[\x1b$\xc0\x1c\xf6;\xde\xc5Lѧ\xa4m\n\x17\xeaAI\x8e\"\r\u007f\x9b}x\x18\xffu\x88\xf9\x8d\x16\xa0\x8a\x02\xbd\bR\x8c\r\x1a~\a\xbe+*P^\xd4 \x87z&3y\xa3\f\x95\xe89\xef\xcf@\xe7?\xbf\xff2\xcc\x1e\xc0/\xd6\x01\xbe\xa8\xa6\xad\xf1\x1dPd|\x93\xfe\x92ϐ\x8ftl$\u008a\xb8\xa2\xc3KkÀxW\xaf\xf6*\xa8\xcb\xea\x19\xc1\xf6\xeav\b5=\xe3\x04n$\xcaw`\xfeG\x02\xeb\xbf7'\xa4\xfe.\x06Ѝ,\xba\x89\xe06\xf7\xddnDnAr\xa5\x18\xd8\xd1b\x81.\x14\bCOHޒ\x12\xbf\a\xeb\x84\x01cwD\x04\xc1b\xbd\x98\x8fP\x1f\x81\xfe\xfc\xfe\xcbI\xc4\xfb|\x01\x19\x8d/\xf0\x1e\xc8DnZ\xab\xbf\xcf\xe1)x\xc7ڰz\x91\x93\x8a\xcaz<Ŭ5\xf5Zt\xae\xd4\x12\xc1\xdb\x06a\x85u\x9d\xc5zC\xc3J\xad\x85\x85d8\xf17\x05\xadr|\xd6[S\x95\xf1\xf4\xe1\xfe\xc3$\"\x13\x87Z\x84|'\xb7SIR5H\xb9\x10\xef\xbc\xe0\x8dG\x97fz|\x17݇-\x14\x952\v\x8c\xfa\"\x94\x9d\xdcB\xf9\xed[\xe2\xf8\xf8\xeaO\xcf@\tp\x988\xfeo\x97\xe8\x95ʅJ\xf5\n\xe5\x1ev\xbc\xfc\xacr\xd2\x188\x83\x8cA?m\v/\xaa\x15ز\x1f\xdb%\xba%\xe1j\xbc\xb2\xee\x99\xcc\"\x13\xd7̢\x0f\xf8q(\xed\xc7߅?o\xd6%\x14\xe4\xd7*\x14\x16\u007f\v\xad\xe4\x1c?~\x93R\xa9V\xbc\xfe\x1e\xbb\x9d\xf5\x05\xcc\xe1^\t\x8bUEE\x95\x9a\x80>Ǟ\b&\x92\x8aS\xc7Ԭ\xcc\xfa\xab\xbb\xb2\x10\xda9A\xb4\xce\xfan3SF\xcb\xff\x9e<\xcb\xf8\x9b\x18\xec\xe8\xaa\xf0\xfd\xc7\xf4\xfe\xdb8xGo\x8a\xd5\x13\x85n\xf4\x91\xd6N\xb5PY\x12\xba\vu\xd9ǽũ\xae\x1c\xa8\v7k^U\x18z\xa3Z_Y\x9e\xde_\xc01\xdb,L\x18\xb6\x06\xe8\xcb\xc1$K\x1c\xf7l\x15x\x06O\x14u\x01K\xac\xed\x87j\xec\x1eI\xac9\u0088Ե\x01\xcfp\xb0\xbe\x16\xa1\xb4dR@\xed#̆;\x87\x835\xad\xd5\a#\xfb\x9ep0\xb95\xcd\xc1DT\U000aad8a\x15w\xfe5\x8dUؐ\x98\x8d\xf1ͽ\x98Pܾ\xb9\xb5*\xac\x14\x8e\xfb\xaf\x98\xce[\xf9\xeexGx\x8f\xe1tD\xc7\xd4`\xe8W\x02\x0eX)\x9f\x0e\x19\xb2(\xecȋ[CN\x15q\xa8CY'Ug\xa9\xa8F\r\x9b\x97\\\xf0$\x1dfh\xe8o\x87\xaa\x98$\xa8\xf3\xa8C\xef9\x00\xfax_i]\xa3x\x02\xd2\xc6g\"\xe2h\x85\xe9\xeaZ\xcdk\x9c\x00\xbb\xeex\xfaL\x005\xe8\xbdZ\\\x8a\xa0\xdf\xe2\xaa\xd8\xf1\xf5[@\xcdmǛ\x96\xaf\x0f\xa5\x9e\x8a[\xdf{\xc1\xeb\xda\xceJ\xf9KP\x1ee͐\xc7m\x82\xfa\xbc\xcbɃ\xa6k\x8e\x8f\xc9\xe0\x01W\x03\xa3S\xf3\xe8\xec¡?\xb6L\x96\f8\xd0\x04d\xf0K\xf0\x8eW\x11\xd0\x1ft\x89\x83~\x19T\xb6N\xdemY\xd5`\xbaf\x8eN\x88\x98\xaf\x19}b$\xa5\x86\xa1\x1e:\xd4\xde[&\xb7\x12R\xb6\x8b\xa2\xfan\xa2P&\xbcR\x12\xffe\v\x9a|[\xab\xf5\x80ܤI\xb8^\xc5}%\x8e\xb6\x1e\x93\xa2P\xc2?̽\xb6\xf7\x0f\xa0\xee\xad9Q\r\xa6\x90!\xc3\u007f\xfcÙۘ\f\xe3\xe2 \x95\xf6\xf3B\xe8\xcfr\xca\xd79\xe1̅\xefY9\xbe6\xed\xcd\xf6\x16_\xcaxA\xf4p\xbe\xdbM]ljj\xff\x98o\x99\xa3\x06\x89:\x1a\f\xc8\xf5\x8e\xec\xfe\xbdY?\xb2\xbd\xd9T!\xc5\x1c\xea\x87\xc3O\r77{_\x0e\xc2\xcf\xc2\x1aM\xf13\t|\xfe2\x82\xfe]ڧ\xf49@\x06\xff\x17\x00\x00\xff\xffñ\x1b\xae\xa0\x19\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4W\xc1n\xe36\x10\xbd\xfb+\x06\xdb\xc3^*y\x17=\xb4ЭM[ h\x12,\x9cE.E\x0f\x145\xb2\xa7\xa1H\x96\x1c:u\xbf\xbe\x18J\x8aeY\x897\v\xacn&g\x1e\xdf̛\x19ҫ\xa2(V\xca\xd3\x03\x86H\xceV\xa0<ῌV~\xc5\xf2\xf1\xa7X\x92[\xef?\xae\x1e\xc96\x15\\\xa5Ȯ\xdb`t)h\xfc\x15[\xb2\xc4\xe4\xec\xaaCV\x8dbU\xad\x00\x94\xb5\x8e\x95,G\xf9\t\xa0\x9d\xe5\xe0\x8c\xc1Plі\x8f\xa9\xc6:\x91i0d\xf0\xf1\xe8\xfd\x87\xf2\xc7\xf2\xc3\n@\a\xcc\ue7e9\xc3Ȫ\xf3\x15\xd8d\xcc\n\xc0\xaa\x0e+\b\x18\x99t@\xef\"\xb1\v\x84\xb1ܣ\xc1\xe0Jr\xab\xe8Q˱\xdb\xe0\x92\xaf\xe0\xb8\xd1{\x0f\x94\xfap6\x19h3\x02\x1d\xf2\x96\xa1\xc8\u007f,n\xdfP\xe4l\xe2M\n\xca,\x11\xc9ۑ\xec6\x19\x15\xce\f䀨\x9d\xc7\n\ue10bW\x1a\x9b\x15\xc0\x90\x82̭\x18\x82\xdc\u007f\xec\xb1\xf4\x0e;Փ\x06p\x1e\xedϟ\xae\x1f~\xb8?Y\x06\xf0\xc1y\fLc|\xfd7\x11v\xb2\n\xd0`ԁ<紿\x17\xc0\xde\n\x1aQ\x14#\xf0\x0eGR\xd8\f\x1c\xc0\xb5\xc0;\x8a\x10\xd0\a\x8ch{\x8dO\x80A\x8c\x94\x05W\xff\x8d\x9aK\xb8\xc7 0\x10w.\x99F\na\x8f\x81!\xa0v[K\xff=cG`\x97\x0f5\x8aqH\xf2\xf1#\xcb\x18\xac2\xb0W&\xe1\xf7\xa0l\x03\x9d:@@9\x05\x92\x9d\xe0e\x93X\u00ad\v\bd[W\xc1\x8e\xd9\xc7j\xbd\xde\x12\x8f\x05\xad]\xd7%K|X\xe7ڤ:\xb1\vq\xdd\xe0\x1e\xcd:ҶPA\xef\x88Qs\n\xb8V\x9e\x8aL\xdd\xe6\xa2.\xbb\xe6\xbb0\xb4@|\u007f\u0095\x0f\xa2m\xe4@v;\xd9\xc8\xd5\xf6\x8a\x02Rn@\x11\xd4\xe0\xdaGqL\xb4,Iv6\xbf\xdd\u007f\x86\xf1\xe8,\xc6<\xfb9\xefG\xc7x\x94@\x12F\xb6\xc5Ћ\xd8\x06\xd7eL\xb4\x8dwd9\xffІ\xd0\xce\xd3\x1fS\xdd\x11\x8b\xee\xff$\x8c,Z\x95p\x95\xbb\x1cj\x84\xe4\x1b\xc5ؔpm\xe1Juh\xaeT\xc4o.\x80d:\x16\x92\xd8/\x93`:\xa0\xe6\xc6}\xd6&\x1b\xe3\fyA\xaf\xf9\\\xb8\xf7\xa8E>ɠ\xb8RK:\xf7\x06\xb4.\x80:\xb3/O\xa0\x97[W\xbeZ\xe9\xc7\xe4\xef\xd9\x05\xb5\xc5\x1b\xd7c\u038df\xdc~Y\xf2\x19\xc9\xc9d\xe9\xdb\x18\x97\rϰ\x01x\xa7xҿ\xac\xc8>\x8f\x81\xc5x^\x11!\v\xa1\xa4\x9d\xad\xb2\x1a\u007f\xcf\x15e\xf5\xe1BL\xb7\v.\x12\xd2\xce=\x81k\x19\xed\x14t\xe0\xba\x10I\x8d\x10\x92}\x13\xd9~~_7Rx-a\xb8@t33\x1f\xf3\xde&c\x06\xacB\xbb\xce+\xa6\xda\xe0\xf2\x91\xf2I\xd9P\x8fr\xe8{\xff\xeb\xf3\xbdw&u\xf8|\xdd\\\x88\xe0\xe1\xd4zZ8\xfd\xc2@EB\x81pzq\x9e~C\xadD\xf0\xae\x19H\f\x05\x1d%\xbe7\xc4 \x92S\xc0\xd9\x04-\x96\xdbcf\xb3Tm3\x93\xb9Ƴ\xedY\xfe\xbeh|\xb0\xe2\x14\xdf2@\xb2Øl\x9dB@\xcb\x03L\xbeQ\xbfz\x84\x18\x15y\xd2>\xf2\xa2\xbaP\x017\xe7\x1e#1\x01\x03\x96\x85i\xbf=\xa9\xf9-\x94E[\xea\xb4օNq\x05ra\x14\x02tf!\xef-\xacnP5\xe7}\\\xc0\x9d\xe3\xe5\xad\x17#\\슳\xc5(\xef\x92f\xa2s\xec\x1byX9\xf6\x90\xd2\x1a=cs7\u007f\xbd\xbf{w\xf2\x18\xcf?\xb5\xb3\r\xf5\u007f=\xe0ϿV=*6\x0f\xe3\x03[\x16\xff\x0f\x00\x00\xff\xff\x83\xf9\xd9\xe0\xf4\f\x00\x00"), diff --git a/pkg/apis/velero/v1/download_request_types.go b/pkg/apis/velero/v1/download_request_types.go index 945cd8257a..e773f64df2 100644 --- a/pkg/apis/velero/v1/download_request_types.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -25,13 +25,14 @@ type DownloadRequestSpec struct { } // DownloadTargetKind represents what type of file to download. -// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupResourceList;RestoreLog;RestoreResults +// +kubebuilder:validation:Enum=BackupLog;BackupContents;BackupVolumeSnapshots;BackupItemSnapshots;BackupResourceList;RestoreLog;RestoreResults type DownloadTargetKind string const ( DownloadTargetKindBackupLog DownloadTargetKind = "BackupLog" DownloadTargetKindBackupContents DownloadTargetKind = "BackupContents" DownloadTargetKindBackupVolumeSnapshots DownloadTargetKind = "BackupVolumeSnapshots" + DownloadTargetKindBackupItemSnapshots DownloadTargetKind = "BackupItemSnapshots" DownloadTargetKindBackupResourceList DownloadTargetKind = "BackupResourceList" DownloadTargetKindRestoreLog DownloadTargetKind = "RestoreLog" DownloadTargetKindRestoreResults DownloadTargetKind = "RestoreResults" diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index d3a7d7c5ae..b41125e56a 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -283,3 +283,7 @@ func (_m *BackupStore) GetCSIVolumeSnapshotContents(backup string) ([]*snapshotv panic("Not implemented") return nil, nil } + +func (_m *BackupStore) GetItemSnapshots(name string) ([]*volume.ItemSnapshot, error) { + panic("implement me") +} diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index d9f2d8a39a..bf5744509b 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -44,6 +44,7 @@ type BackupInfo struct { Log, PodVolumeBackups, VolumeSnapshots, + ItemSnapshots, BackupResourceList, CSIVolumeSnapshots, CSIVolumeSnapshotContents io.Reader @@ -58,6 +59,7 @@ type BackupStore interface { PutBackup(info BackupInfo) error GetBackupMetadata(name string) (*velerov1api.Backup, error) + GetItemSnapshots(name string) ([]*volume.ItemSnapshot, error) GetBackupVolumeSnapshots(name string) ([]*volume.Snapshot, error) GetPodVolumeBackups(name string) ([]*velerov1api.PodVolumeBackup, error) GetBackupContents(name string) (io.ReadCloser, error) @@ -225,50 +227,51 @@ func (s *objectBackupStore) ListBackups() ([]string, error) { } func (s *objectBackupStore) PutBackup(info BackupInfo) error { - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupLogKey(info.Name), info.Log); err != nil { - // Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the - // backup's status. - s.logger.WithError(err).WithField("backup", info.Name).Error("Error uploading log file") - } - - if info.Metadata == nil { - // If we don't have metadata, something failed, and there's no point in continuing. An object - // storage bucket that is missing the metadata file can't be restored, nor can its logs be - // viewed. - return nil + if info.Log != nil { + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupLogKey(info.Name), info.Log); err != nil { + // Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the + // backup's status. + s.logger.WithError(err).WithField("backup", info.Name).Error("Error uploading log file") + } } - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupMetadataKey(info.Name), info.Metadata); err != nil { - // failure to upload metadata file is a hard-stop - return err + if info.Metadata != nil { + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupMetadataKey(info.Name), info.Metadata); err != nil { + // failure to upload metadata file is a hard-stop + return err + } } - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupContentsKey(info.Name), info.Contents); err != nil { - deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) - return kerrors.NewAggregate([]error{err, deleteErr}) + if info.Contents != nil { + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupContentsKey(info.Name), info.Contents); err != nil { + deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) + return kerrors.NewAggregate([]error{err, deleteErr}) + } } - // Since the logic for all of these files is the exact same except for the name and the contents, // use a map literal to iterate through them and write them to the bucket. var backupObjs = map[string]io.Reader{ s.layout.getPodVolumeBackupsKey(info.Name): info.PodVolumeBackups, s.layout.getBackupVolumeSnapshotsKey(info.Name): info.VolumeSnapshots, + s.layout.getItemSnapshotsKey(info.Name): info.ItemSnapshots, s.layout.getBackupResourceListKey(info.Name): info.BackupResourceList, s.layout.getCSIVolumeSnapshotKey(info.Name): info.CSIVolumeSnapshots, s.layout.getCSIVolumeSnapshotContentsKey(info.Name): info.CSIVolumeSnapshotContents, } for key, reader := range backupObjs { - if err := seekAndPutObject(s.objectStore, s.bucket, key, reader); err != nil { - errs := []error{err} - - // attempt to clean up the backup contents and metadata if we fail to upload and of the extra files. - deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupContentsKey(info.Name)) - errs = append(errs, deleteErr) - - deleteErr = s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) - errs = append(errs, deleteErr) - return kerrors.NewAggregate(errs) + if reader != nil { + if err := seekAndPutObject(s.objectStore, s.bucket, key, reader); err != nil { + errs := []error{err} + + // attempt to clean up the backup contents and metadata if we fail to upload and of the extra files. + deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupContentsKey(info.Name)) + errs = append(errs, deleteErr) + + deleteErr = s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) + errs = append(errs, deleteErr) + return kerrors.NewAggregate(errs) + } } } @@ -324,6 +327,27 @@ func (s *objectBackupStore) GetBackupVolumeSnapshots(name string) ([]*volume.Sna return volumeSnapshots, nil } +func (s *objectBackupStore) GetItemSnapshots(name string) ([]*volume.ItemSnapshot, error) { + // if the itemsnapshots file doesn't exist, we don't want to return an error, since + // a legacy backup or a backup with no snapshots would not have this file, so check for + // its existence before attempting to get its contents. + res, err := tryGet(s.objectStore, s.bucket, s.layout.getItemSnapshotsKey(name)) + if err != nil { + return nil, err + } + if res == nil { + return nil, nil + } + defer res.Close() + + var itemSnapshots []*volume.ItemSnapshot + if err := decode(res, &itemSnapshots); err != nil { + return nil, err + } + + return itemSnapshots, nil +} + // tryGet returns the object with the given key if it exists, nil if it does not exist, // or an error if it was unable to check existence or get the object. func tryGet(objectStore velero.ObjectStore, bucket, key string) (io.ReadCloser, error) { @@ -473,6 +497,8 @@ func (s *objectBackupStore) GetDownloadURL(target velerov1api.DownloadTarget) (s return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupLogKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindBackupVolumeSnapshots: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupVolumeSnapshotsKey(target.Name), DownloadURLTTL) + case velerov1api.DownloadTargetKindBackupItemSnapshots: + return s.objectStore.CreateSignedURL(s.bucket, s.layout.getItemSnapshotsKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindBackupResourceList: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupResourceListKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindRestoreLog: diff --git a/pkg/persistence/object_store_layout.go b/pkg/persistence/object_store_layout.go index 025da966bf..046b99a68b 100644 --- a/pkg/persistence/object_store_layout.go +++ b/pkg/persistence/object_store_layout.go @@ -88,6 +88,10 @@ func (l *ObjectStoreLayout) getBackupVolumeSnapshotsKey(backup string) string { return path.Join(l.subdirs["backups"], backup, fmt.Sprintf("%s-volumesnapshots.json.gz", backup)) } +func (l *ObjectStoreLayout) getItemSnapshotsKey(backup string) string { + return path.Join(l.subdirs["backups"], backup, fmt.Sprintf("%s-itemsnapshots.json.gz", backup)) +} + func (l *ObjectStoreLayout) getBackupResourceListKey(backup string) string { return path.Join(l.subdirs["backups"], backup, fmt.Sprintf("%s-resource-list.json.gz", backup)) } diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index 76d37bd408..f3cb554866 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -223,6 +223,7 @@ func TestPutBackup(t *testing.T) { log io.Reader podVolumeBackup io.Reader snapshots io.Reader + itemSnapshots io.Reader resourceList io.Reader expectedErr string expectedKeys []string @@ -234,6 +235,7 @@ func TestPutBackup(t *testing.T) { log: newStringReadSeeker("log"), podVolumeBackup: newStringReadSeeker("podVolumeBackup"), snapshots: newStringReadSeeker("snapshots"), + itemSnapshots: newStringReadSeeker("itemSnapshots"), resourceList: newStringReadSeeker("resourceList"), expectedErr: "", expectedKeys: []string{ @@ -242,6 +244,7 @@ func TestPutBackup(t *testing.T) { "backups/backup-1/backup-1-logs.gz", "backups/backup-1/backup-1-podvolumebackups.json.gz", "backups/backup-1/backup-1-volumesnapshots.json.gz", + "backups/backup-1/backup-1-itemsnapshots.json.gz", "backups/backup-1/backup-1-resource-list.json.gz", }, }, @@ -253,6 +256,7 @@ func TestPutBackup(t *testing.T) { log: newStringReadSeeker("log"), podVolumeBackup: newStringReadSeeker("podVolumeBackup"), snapshots: newStringReadSeeker("snapshots"), + itemSnapshots: newStringReadSeeker("itemSnapshots"), resourceList: newStringReadSeeker("resourceList"), expectedErr: "", expectedKeys: []string{ @@ -261,6 +265,7 @@ func TestPutBackup(t *testing.T) { "prefix-1/backups/backup-1/backup-1-logs.gz", "prefix-1/backups/backup-1/backup-1-podvolumebackups.json.gz", "prefix-1/backups/backup-1/backup-1-volumesnapshots.json.gz", + "prefix-1/backups/backup-1/backup-1-itemsnapshots.json.gz", "prefix-1/backups/backup-1/backup-1-resource-list.json.gz", }, }, @@ -271,19 +276,21 @@ func TestPutBackup(t *testing.T) { log: newStringReadSeeker("log"), podVolumeBackup: newStringReadSeeker("podVolumeBackup"), snapshots: newStringReadSeeker("snapshots"), + itemSnapshots: newStringReadSeeker("itemSnapshots"), resourceList: newStringReadSeeker("resourceList"), expectedErr: "error readers return errors", expectedKeys: []string{"backups/backup-1/backup-1-logs.gz"}, }, { - name: "error on data upload deletes metadata", - metadata: newStringReadSeeker("metadata"), - contents: new(errorReader), - log: newStringReadSeeker("log"), - snapshots: newStringReadSeeker("snapshots"), - resourceList: newStringReadSeeker("resourceList"), - expectedErr: "error readers return errors", - expectedKeys: []string{"backups/backup-1/backup-1-logs.gz"}, + name: "error on data upload deletes metadata", + metadata: newStringReadSeeker("metadata"), + contents: new(errorReader), + log: newStringReadSeeker("log"), + snapshots: newStringReadSeeker("snapshots"), + itemSnapshots: newStringReadSeeker("itemSnapshots"), + resourceList: newStringReadSeeker("resourceList"), + expectedErr: "error readers return errors", + expectedKeys: []string{"backups/backup-1/backup-1-logs.gz"}, }, { name: "error on log upload is ok", @@ -292,6 +299,7 @@ func TestPutBackup(t *testing.T) { log: new(errorReader), podVolumeBackup: newStringReadSeeker("podVolumeBackup"), snapshots: newStringReadSeeker("snapshots"), + itemSnapshots: newStringReadSeeker("itemSnapshots"), resourceList: newStringReadSeeker("resourceList"), expectedErr: "", expectedKeys: []string{ @@ -299,11 +307,12 @@ func TestPutBackup(t *testing.T) { "backups/backup-1/backup-1.tar.gz", "backups/backup-1/backup-1-podvolumebackups.json.gz", "backups/backup-1/backup-1-volumesnapshots.json.gz", + "backups/backup-1/backup-1-itemsnapshots.json.gz", "backups/backup-1/backup-1-resource-list.json.gz", }, }, { - name: "don't upload data when metadata is nil", + name: "data should be uploaded even when metadata is nil", metadata: nil, contents: newStringReadSeeker("contents"), log: newStringReadSeeker("log"), @@ -311,7 +320,13 @@ func TestPutBackup(t *testing.T) { snapshots: newStringReadSeeker("snapshots"), resourceList: newStringReadSeeker("resourceList"), expectedErr: "", - expectedKeys: []string{"backups/backup-1/backup-1-logs.gz"}, + expectedKeys: []string{ + "backups/backup-1/backup-1.tar.gz", + "backups/backup-1/backup-1-logs.gz", + "backups/backup-1/backup-1-podvolumebackups.json.gz", + "backups/backup-1/backup-1-volumesnapshots.json.gz", + "backups/backup-1/backup-1-resource-list.json.gz", + }, }, } @@ -326,6 +341,7 @@ func TestPutBackup(t *testing.T) { Log: tc.log, PodVolumeBackups: tc.podVolumeBackup, VolumeSnapshots: tc.snapshots, + ItemSnapshots: tc.itemSnapshots, BackupResourceList: tc.resourceList, } err := harness.PutBackup(backupInfo) @@ -426,6 +442,48 @@ func TestGetBackupVolumeSnapshots(t *testing.T) { assert.EqualValues(t, snapshots, res) } +func TestGetItemSnapshots(t *testing.T) { + harness := newObjectBackupStoreTestHarness("test-bucket", "") + + // volumesnapshots file not found should not error + harness.objectStore.PutObject(harness.bucket, "backups/test-backup/velero-backup.json", newStringReadSeeker("foo")) + res, err := harness.GetItemSnapshots("test-backup") + assert.NoError(t, err) + assert.Nil(t, res) + + // volumesnapshots file containing invalid data should error + harness.objectStore.PutObject(harness.bucket, "backups/test-backup/test-backup-itemsnapshots.json.gz", newStringReadSeeker("foo")) + res, err = harness.GetItemSnapshots("test-backup") + assert.NotNil(t, err) + + // volumesnapshots file containing gzipped json data should return correctly + snapshots := []*volume.ItemSnapshot{ + { + Spec: volume.ItemSnapshotSpec{ + BackupName: "test-backup", + ResourceIdentifier: "item-1", + }, + }, + { + Spec: volume.ItemSnapshotSpec{ + BackupName: "test-backup", + ResourceIdentifier: "item-2", + }, + }, + } + + obj := new(bytes.Buffer) + gzw := gzip.NewWriter(obj) + + require.NoError(t, json.NewEncoder(gzw).Encode(snapshots)) + require.NoError(t, gzw.Close()) + require.NoError(t, harness.objectStore.PutObject(harness.bucket, "backups/test-backup/test-backup-itemsnapshots.json.gz", obj)) + + res, err = harness.GetItemSnapshots("test-backup") + assert.NoError(t, err) + assert.EqualValues(t, snapshots, res) +} + func TestGetBackupContents(t *testing.T) { harness := newObjectBackupStoreTestHarness("test-bucket", "") @@ -506,6 +564,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindBackupContents: "backups/my-backup/my-backup.tar.gz", velerov1api.DownloadTargetKindBackupLog: "backups/my-backup/my-backup-logs.gz", velerov1api.DownloadTargetKindBackupVolumeSnapshots: "backups/my-backup/my-backup-volumesnapshots.json.gz", + velerov1api.DownloadTargetKindBackupItemSnapshots: "backups/my-backup/my-backup-itemsnapshots.json.gz", velerov1api.DownloadTargetKindBackupResourceList: "backups/my-backup/my-backup-resource-list.json.gz", }, }, @@ -517,6 +576,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindBackupContents: "velero-backups/backups/my-backup/my-backup.tar.gz", velerov1api.DownloadTargetKindBackupLog: "velero-backups/backups/my-backup/my-backup-logs.gz", velerov1api.DownloadTargetKindBackupVolumeSnapshots: "velero-backups/backups/my-backup/my-backup-volumesnapshots.json.gz", + velerov1api.DownloadTargetKindBackupItemSnapshots: "velero-backups/backups/my-backup/my-backup-itemsnapshots.json.gz", velerov1api.DownloadTargetKindBackupResourceList: "velero-backups/backups/my-backup/my-backup-resource-list.json.gz", }, }, @@ -527,6 +587,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindBackupContents: "backups/b-cool-20170913154901-20170913154902/b-cool-20170913154901-20170913154902.tar.gz", velerov1api.DownloadTargetKindBackupLog: "backups/b-cool-20170913154901-20170913154902/b-cool-20170913154901-20170913154902-logs.gz", velerov1api.DownloadTargetKindBackupVolumeSnapshots: "backups/b-cool-20170913154901-20170913154902/b-cool-20170913154901-20170913154902-volumesnapshots.json.gz", + velerov1api.DownloadTargetKindBackupItemSnapshots: "backups/b-cool-20170913154901-20170913154902/b-cool-20170913154901-20170913154902-itemsnapshots.json.gz", velerov1api.DownloadTargetKindBackupResourceList: "backups/b-cool-20170913154901-20170913154902/b-cool-20170913154901-20170913154902-resource-list.json.gz", }, }, @@ -537,6 +598,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindBackupContents: "backups/my-backup-20170913154901/my-backup-20170913154901.tar.gz", velerov1api.DownloadTargetKindBackupLog: "backups/my-backup-20170913154901/my-backup-20170913154901-logs.gz", velerov1api.DownloadTargetKindBackupVolumeSnapshots: "backups/my-backup-20170913154901/my-backup-20170913154901-volumesnapshots.json.gz", + velerov1api.DownloadTargetKindBackupItemSnapshots: "backups/my-backup-20170913154901/my-backup-20170913154901-itemsnapshots.json.gz", velerov1api.DownloadTargetKindBackupResourceList: "backups/my-backup-20170913154901/my-backup-20170913154901-resource-list.json.gz", }, }, @@ -548,6 +610,7 @@ func TestGetDownloadURL(t *testing.T) { velerov1api.DownloadTargetKindBackupContents: "velero-backups/backups/my-backup-20170913154901/my-backup-20170913154901.tar.gz", velerov1api.DownloadTargetKindBackupLog: "velero-backups/backups/my-backup-20170913154901/my-backup-20170913154901-logs.gz", velerov1api.DownloadTargetKindBackupVolumeSnapshots: "velero-backups/backups/my-backup-20170913154901/my-backup-20170913154901-volumesnapshots.json.gz", + velerov1api.DownloadTargetKindBackupItemSnapshots: "velero-backups/backups/my-backup-20170913154901/my-backup-20170913154901-itemsnapshots.json.gz", velerov1api.DownloadTargetKindBackupResourceList: "velero-backups/backups/my-backup-20170913154901/my-backup-20170913154901-resource-list.json.gz", }, }, diff --git a/pkg/volume/item_snapshot.go b/pkg/volume/item_snapshot.go new file mode 100644 index 0000000000..909c219944 --- /dev/null +++ b/pkg/volume/item_snapshot.go @@ -0,0 +1,57 @@ +/* +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 volume + +import isv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/item_snapshotter/v1" + +// ItemSnapshot stores information about an item snapshot (includes volumes and other Astrolabe objects) taken as +// part of a Velero backup. +type ItemSnapshot struct { + Spec ItemSnapshotSpec `json:"spec"` + + Status ItemSnapshotStatus `json:"status"` +} + +type ItemSnapshotSpec struct { + // ItemSnapshotter is the name of the ItemSnapshotter plugin that took the snapshot + ItemSnapshotter string `json:"itemSnapshotter"` + + // BackupName is the name of the Velero backup this snapshot + // is associated with. + BackupName string `json:"backupName"` + + // BackupUID is the UID of the Velero backup this snapshot + // is associated with. + BackupUID string `json:"backupUID"` + + // Location is the name of the location where this snapshot is stored. + Location string `json:"location"` + + // Kubernetes resource identifier for the item + ResourceIdentifier string "json:resourceIdentifier" +} + +type ItemSnapshotStatus struct { + // ProviderSnapshotID is the ID of the snapshot taken by the ItemSnapshotter + ProviderSnapshotID string `json:"providerSnapshotID,omitempty"` + + // Metadata is the metadata returned with the snapshot to be returned to the ItemSnapshotter at restore time + Metadata map[string]string `json:"metadata,omitempty"` + + // Phase is the current state of the ItemSnapshot. + Phase isv1.SnapshotPhase `json:"phase,omitempty"` +} From 635c0b2a15f10712650bd0c563ad0c6a5c850c47 Mon Sep 17 00:00:00 2001 From: Dave Smith-Uchida Date: Fri, 10 Dec 2021 17:05:07 -0800 Subject: [PATCH 2/2] Removed redundant checks Signed-off-by: Dave Smith-Uchida --- pkg/persistence/object_store.go | 49 ++++++++++++++------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index bf5744509b..744f2baaaf 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -227,27 +227,22 @@ func (s *objectBackupStore) ListBackups() ([]string, error) { } func (s *objectBackupStore) PutBackup(info BackupInfo) error { - if info.Log != nil { - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupLogKey(info.Name), info.Log); err != nil { - // Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the - // backup's status. - s.logger.WithError(err).WithField("backup", info.Name).Error("Error uploading log file") - } + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupLogKey(info.Name), info.Log); err != nil { + // Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the + // backup's status. + s.logger.WithError(err).WithField("backup", info.Name).Error("Error uploading log file") } - if info.Metadata != nil { - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupMetadataKey(info.Name), info.Metadata); err != nil { - // failure to upload metadata file is a hard-stop - return err - } + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupMetadataKey(info.Name), info.Metadata); err != nil { + // failure to upload metadata file is a hard-stop + return err } - if info.Contents != nil { - if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupContentsKey(info.Name), info.Contents); err != nil { - deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) - return kerrors.NewAggregate([]error{err, deleteErr}) - } + if err := seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupContentsKey(info.Name), info.Contents); err != nil { + deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) + return kerrors.NewAggregate([]error{err, deleteErr}) } + // Since the logic for all of these files is the exact same except for the name and the contents, // use a map literal to iterate through them and write them to the bucket. var backupObjs = map[string]io.Reader{ @@ -260,18 +255,16 @@ func (s *objectBackupStore) PutBackup(info BackupInfo) error { } for key, reader := range backupObjs { - if reader != nil { - if err := seekAndPutObject(s.objectStore, s.bucket, key, reader); err != nil { - errs := []error{err} - - // attempt to clean up the backup contents and metadata if we fail to upload and of the extra files. - deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupContentsKey(info.Name)) - errs = append(errs, deleteErr) - - deleteErr = s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) - errs = append(errs, deleteErr) - return kerrors.NewAggregate(errs) - } + if err := seekAndPutObject(s.objectStore, s.bucket, key, reader); err != nil { + errs := []error{err} + + // attempt to clean up the backup contents and metadata if we fail to upload and of the extra files. + deleteErr := s.objectStore.DeleteObject(s.bucket, s.layout.getBackupContentsKey(info.Name)) + errs = append(errs, deleteErr) + + deleteErr = s.objectStore.DeleteObject(s.bucket, s.layout.getBackupMetadataKey(info.Name)) + errs = append(errs, deleteErr) + return kerrors.NewAggregate(errs) } }