From 92c72b1a63458132e68ae2b33e53dbed567f07ec Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 30 May 2025 12:59:14 +0800 Subject: [PATCH] data path for vgdp ms pvb Signed-off-by: Lyndon-Li --- changelogs/unreleased/8998-Lyndon-Li | 1 + .../v1/bases/velero.io_podvolumebackups.yaml | 5 + config/crd/v1/crds/crds.go | 2 +- internal/credentials/local.go | 7 + pkg/apis/velero/v1/pod_volume_backup_types.go | 4 + pkg/cmd/cli/datamover/backup.go | 13 +- pkg/cmd/cli/datamover/data_mover.go | 30 -- pkg/cmd/cli/datamover/data_mover_test.go | 131 ----- pkg/cmd/cli/datamover/restore.go | 5 +- pkg/cmd/cli/nodeagent/server.go | 6 +- pkg/cmd/cli/podvolume/backup.go | 291 ++++++++++++ pkg/cmd/cli/podvolume/backup_test.go | 216 +++++++++ pkg/cmd/cli/podvolume/podvolume.go | 43 ++ pkg/cmd/cli/repomantenance/maintenance.go | 2 +- pkg/cmd/server/config/config.go | 7 +- pkg/cmd/velero/velero.go | 2 + pkg/podvolume/backup_micro_service.go | 313 ++++++++++++ pkg/podvolume/backup_micro_service_test.go | 447 ++++++++++++++++++ pkg/podvolume/util.go | 15 + pkg/util/kube/pod.go | 28 ++ pkg/util/kube/pod_test.go | 107 +++++ 21 files changed, 1491 insertions(+), 184 deletions(-) create mode 100644 changelogs/unreleased/8998-Lyndon-Li create mode 100644 internal/credentials/local.go delete mode 100644 pkg/cmd/cli/datamover/data_mover_test.go create mode 100644 pkg/cmd/cli/podvolume/backup.go create mode 100644 pkg/cmd/cli/podvolume/backup_test.go create mode 100644 pkg/cmd/cli/podvolume/podvolume.go create mode 100644 pkg/podvolume/backup_micro_service.go create mode 100644 pkg/podvolume/backup_micro_service_test.go diff --git a/changelogs/unreleased/8998-Lyndon-Li b/changelogs/unreleased/8998-Lyndon-Li new file mode 100644 index 000000000..a2385b5ad --- /dev/null +++ b/changelogs/unreleased/8998-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #8988, add data path for VGDP ms pvb \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_podvolumebackups.yaml b/config/crd/v1/bases/velero.io_podvolumebackups.yaml index 9ccff4124..0eadd8e59 100644 --- a/config/crd/v1/bases/velero.io_podvolumebackups.yaml +++ b/config/crd/v1/bases/velero.io_podvolumebackups.yaml @@ -76,6 +76,11 @@ spec: BackupStorageLocation is the name of the backup storage location where the backup repository is stored. type: string + cancel: + description: |- + Cancel indicates request to cancel the ongoing PodVolumeBackup. It can be set + when the PodVolumeBackup is in InProgress phase + type: boolean node: description: Node is the name of the node that the Pod is running on. diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 60c18e9eb..16288b1b4 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -34,7 +34,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccYK\x8f\x1b\xb9\x11\xbe\xebW\x14v\x0f{ٖ\xec\x04\t\x02\xdd\xc6r\x02\x18\x19\xc7\x03k2\xb9.EVK\\\xb1\xc9\x0e\x1f\x92\x95\xc7\x7f\x0f\x8a\x0f\xa9\xd5\x0fK\xe3\x04\x9b\xe5eF|\x14\xeb\xf9U\x15\xbb\xaa\xaa\x19k\xe5\vZ'\x8d^\x02k%~\xf1\xa8闛\xef\xff\xe0\xe6\xd2,\x0eog{\xa9\xc5\x12V\xc1y\xd3|Fg\x82\xe5\xf8\x1ek\xa9\xa5\x97F\xcf\x1a\xf4L0ϖ3\x00\xa6\xb5\xf1\x8c\xa6\x1d\xfd\x04\xe0F{k\x94B[mQ\xcf\xf7a\x83\x9b \x95@\x1b\x89\x97\xab\x0fo\xe6o\x7f?\xff\xdd\f@\xb3\x06\x97\xb0a|\x1fZ\xe7\x8de[T\x86'\x92\xf3\x03*\xb4f.\xcd̵\xc8醭5\xa1]\xc2e!Qȷ'\xce\xdfEb\xebD\xec1\x13\x8b\xebJ:\xff\xe7\xe9=\x8f\xd2\xf9\xb8\xafU\xc125\xc5V\xdc\xe2v\xc6\xfa\xbf\\\xae\xae`\xe3TZ\x91z\x1b\x14\xb3\x13\xc7g\x00\x8e\x9b\x16\x97\x10O\xb7\x8c\xa3\x98\x01d\xd5Dj\x150!\xa2\xb2\x99z\xb2R{\xb4+\xa3B\xa3\xcfw\tt\xdc\xca\xd6Ge&Y \v\x03E\x1ap\x9e\xf9\xe0\xc0\x05\xbe\x03\xe6\xe0\xe1\xc0\xa4b\x1b\x85\x8b\xbfjV\xfe\x8f\xf4\x00~vF?1\xbf[\xc2<\x9d\x9a\xb7;\xe6\xcaj\xb2\xd1SgƟH\x00\xe7\xad\xd4\xdb1\x96\x1e\x99\xf3/LI\x119y\x96\r\x82t\xe0w\b\x8a9\x0f\x9e&\xe8W\xd2\x10\x90\x8a\x10\x8a\x86\xe0\xc8\\\xbe\a\xe0\x90\xa8D\x1d\x8ds\xaa\x06w]\xb1M\xac\xc0K\x8fJ\xe2\x9ff2\xf7\x1d\xb2ſ\xe7\xdc♤\xf3\xaci\xaf\xe8>lq\x8aؕ*\xdec͂\xf2]Q\xc9J\xaa\xeb\x97\xd7b\xb5\xc8\xe7\"\x9d\xba\xba\xf1\xfd\xd5\\\xbauc\x8cB\x96\xa8\xa4]\x87\xb7\xc9\v\xf9\x0e\x1b\xb6̛M\x8b\xfa\xe1\xe9\xc3\xcbo\xd7W\xd30\xe6H\xbd\xa0 ñ\x8emvh\x11^b\xfc%\xbb\xb9,ڙ&\x80\xd9\xfc\x8c\xdc_\x8c\xd8ZӢ\xf5\xb2\x04K\x1a\x1d,\xea\xcc\xf6x\xfaWu\xb5\x06@b\xa4S \b\x940\xf9U\x8e\x1f\x14Yr05\xf8\x9dt`\xb1\xb5\xe8P'\x98\xa2i\xa63\x83\xf3\x1e\xe95Z\"C\xb1\x1d\x94 ,;\xa0\xf5`\x91\x9b\xad\x96\xff8\xd3v\xe0Mvf\x8f\xceC\x8cP\xcd\x149k\xc0\x1f\x81iѣܰ\x13X\xa4;!\xe8\x0e\xbdx\xc0\xf5\xf9\xf8H\xd1 um\x96\xb0\xf3\xbeu\xcb\xc5b+}Ahn\x9a&h\xe9O\x8b\b\xb6r\x13\xbc\xb1n!\xf0\x80j\xe1\xe4\xb6b\x96\xef\xa4G\xee\x83\xc5\x05ke\x15\x05\xd1\tR\x1b\xf1\xbd͘\uebae\x1d\x84t\x1a\x11R_a\x1e\x82\xd7\xe42\x89T\x12\xf1b\x05\x9a\"\xd5}\xfe\xe3\xfa\x19\n'\xc9R\xc9(\x97\xad\x03\xbd\x14\xfb\x906\xa5\xaeѦs\xb55M\xa4\x89Z\xb4Fj\x1f\x7fp%Q{pa\xd3HOn\xf0\xf7\x80Γ\xe9\xfadW1\x8b\xc1\x06!\xb4\x11$\xfa\x1b>hX\xb1\x06Պ9\xfc\x85mEVq\x15\x19\xe1.kuss\x7fsRog\xa1\xe4\xd4\tӎ\xa2\xc1\xbaE~\x15w\x02\x9d\xb4\x14\x19\x9ey\x8c\xd1\xd5SP\x86\x8a\xe9\xa4\\\xc68H\xd0`\x9c\xa3s\x1f\x8d\xc0\xfeJ\x8f\xe5\x87\xf3\xc6+\x1e[\xb4\x8dt1\xbdBml?\xf3\xb03\x92wGA\xbc\xbe\xc1\x01P\x87f\xc8H\x05\x9f\x91\x89OZ\x9d&\x96\xfef\xa5\x1f^4aH\x1a\x89\xc5\xf5I\xf3'\xb4҈\x1b¿\xebm?\xab`g\x8ePG\xff\xd7^\x9d\b\xbb\xdcI\xf3!j\x97\xf1\xf0\xf4\xa1 x\x8a\xad\x1c\x98YWsx\xc8Amjx\x03B:*$\\$:T\x96\x0e*\x16\x1aK\xf06\xbcJ|nt-\xb7C\xa1\xbb\xb5є\xc7\xdc \xdd\xd3\xdc*\xdeD\xa8E\xde\xd1Zs\x90\x02mE\xf1!k\xc93'\xc1\xa6\fRKTb\x80M\x93Q\x16E\xb1((\xa8\x99\xbaa\xc3\xd5yc\xac\xa4\x99\xd4Ƀ/\x04\"\xd6\xd8&\xa7f\xedQ\v\xecg\x9bȍ\x89\x80\xe6P\xc0Q\xfa]BJ5\x16w\xf0\xd5أ\xb1\xc7\xd3\xd8t\x8f\xf7\xe7\x1d\xd2Δx\x11\x1cr\x8b>z\x1b*r\x1fr\xa59\xc0\xc7\xe0\"\xd6\xf6q\xa2\x8cX\xf0\x95\xd3{<\r\x15\r\xb7\x8c\x9bK\xa1\t\x96c\x11\xb5\x84ᄏ-\xd2 \xbb\x95A\xa5{\x11\xd4b\x8d\x16\xf5\xa0\x9a(\xe39\xe6(r\x1a\xf20\xack\xe4^\x1eP\x9dbN\"\xf0\xfc\x116\xc1\x83\b\x18\xad\xc6\xf8\xfeȬp\xc0M\xd32/7RI\x7f\x02\xe9&\xe83\xa5\xcc\x11E\xb686\xad?\xcd\xe1\x83v\x9ei\x8e\xee\\\a\x91ƒ+0\x9dv\xe5(\x8e\x05\x1d\xb3c\x18\x98\xc87\xc6y\xe0h\xc9\x1d\xd5\t\x8e\xd6\xe8픰#\xe9\x90z@\xab\xd1c̈\xc2pGɐc\xeb\xdd\xc2\x1c\xd0\x1e$\x1e\x17Gc\xf7Ro+b\xb0\xcaೈ\x9d\xdd\xe2\xfb\xf8\xe7[\xbc\xc0\xb4\t'\xeep\xdeu\x8c\xf5\x13\x95\xb7~\x87)E\xac\x93\x0f\x1a\vT@\x90k7\xd9w\x13\xb2\x8e\x85\xddX]\xde\x1d\xc5\xe4c\xf9c\x8f\xc3\xd4\xf1\x15P\x01\xf8R]t[5\xac\xad\xd2n\xe6M#\xf9\xac/m\xf2\xfb\xaf\xe3OiV\xa4\x16\x92Sq{\x8d\x1b\xa5\x89\x13W=͈\x1a\xfa]\xce\x14Z\x8e\xab)\x89\x9bk\x85\x1b\x1c\x7f\xea\uef74\xbe\t\xbas\xfew\xe8\xa9\xeet\xa0\x91\xea\x03f\x87z\x8e\x80ɍքT\xde\x00;\xa7\x81\x1f\\?\xff\xbd\x12=7\x81\xefqD\xf1\x03Q\xdeōE\xc7\xe9\x18\xf1\x12\x1c\xc6\xc4t\x8b\r\xb8\x1d\x11\x9c\xad\xd0\xde\xc3\xcb\xea\x816\x9eK\b\x06\xab\a\xd8\x04-\x14\x16\x8e\x8e;\xd4\xd4u\xc9\xfa4~\x17\x8d\xe7\xc7u\xd1j\xac\xber\xdfTt;.C\xcaoK\u061cF\xea\xa5;\x84l-\xd6\xf2\xcb\x1dB>ōE\xe1-\xf3;\x90\xdaI\x81\xc0Fԟ\n\xd9\tAϵѧ\x8c9\xdf`\x9e\xafaCb\xe75\xf0Pt|#~\x9e\xf2\xb6\xb3\x16\xca\xef\x9cݮ\xeb\xe4\xa98\x1e\x95\xe8p~\x94\xf9S\xaa>\xf9H\x19q\xc5\xcc\xcb\xf0\xc4W\xaa\xd8\xf244\x16\xccT3\x19kѵF\v\xea9\xef\xaba/,\xff\xef*\xd9q\xb3V\xd7(\xd7[+V\xb8\xab\x8d\x8b\xcf`\xafn\xe4\xd2\xe3`\xb7M2\x1bG\r\xf6\xa5\x97\xeb\xc9\xf8\x8b\xb4p\xa3%W\xa7\xaf\x93\x8eꗠce\x1b\xab\xaa\xf9l\xe4\xc4{l-R\x06\x13K\x92\xcdƃ\xda\x1c\xe9p\x87Z*ˌN\xf9\x9ez[\xa6E~U\xa0\xa5\x11\xcaG\xa9\x14\xd5\x00\x16\x1bCʢ\xb2\xdcR5\xc7b\xadu\xf8\xcd\xfc\xcd\xff\xafeT\xccy\xea\x00Q|ƃ\x1c>\xadݧ\xee\xc7\x01\x95\x82\x0e瘡\x1f?\x95׆\x85\xcd\xdb~\x82Z*\xaa\xff:\xd0qGu0\xf20\xfcn\xfd\xf8\x83\x8b=\x10j\xef\xe0H\x16t\x91%jzL~\xe1\t\xceS\x12\xb9i\xffn\x01\xae\r(\xa3\xb7h\xcbk\x0f\x15xɛ\x8c\x05\x81\x9er\x95\xde\x02\xdf1\xbd\xa5\xc8\x18\x83\xfc\xc8p\xe6\xbe\xcb'yϤ\x83H=\xe1\x1dw\x19\xf4Y\x8e\xb54\xaf1\xe6\xf43\xfc\x99\xffl\xd9\xcbkoO\xefSP[,\xd1_,\xa9\x9c\x14]\xf9\xcb\xd3\xfce|\xfb\xfb\xc0\xf0\xdd\xff[\xd5\xf3_}\xa9\x18|\xa1\xf8U(\xa7\xa1:\xf7f\xf1\xfc1\xedJ\xef\xb5\xf9\b\xb0\x8d\t~$\xf7w\x1c~4\xa6\xe3ǘ\xd7\xf0\x18?1\xdd*OhO\xb1\b\x0f\xd6\xc67\xdd\xf2\xd6\x18\x91b,+ݏ\xc0\x0f\xbd/aݵ\xe1w\xb2;\xe4\x1a\xcd҃ɔi;v\xcdJ\xee΄\xcd\xf9\xa5~\t\xff\xfc\xf7\xec?\x01\x00\x00\xff\xff\x03f\x86Y\xc0\x1d\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMo\x1b7\x10\xbd\xebW\f\xd0kwU\xa3hQ\xec\xadqr0\xda\x06\x82\x1d\xe4N\x91#-c.\xc9\xce\f\xe5\xba\x1f\xff\xbd \xb9+K\xab\x95\x93\\\xb27\x91Ù\xc7\xf7f\x1e\xd54\xcdJE\xfb\x11\x89m\xf0\x1d\xa8h\xf1/A\x9f\x7fq\xfb\xf8\v\xb76\xac\x0f7\xabG\xebM\a\xb7\x89%\f\xf7\xc8!\x91Ʒ\xb8\xb3ފ\r~5\xa0(\xa3Du+\x00\xe5}\x10\x95\x979\xff\x04\xd0\xc1\v\x05琚=\xfa\xf61mq\x9b\xac3H%\xf9T\xfa\xf0C{\xf3s\xfb\xd3\n\xc0\xab\x01;0\xe8Pp\xab\xf4c\x8a\x84\x7f&d\xe1\xf6\x80\x0e)\xb46\xac8\xa2\xce\xf9\xf7\x14R\xec\xe0e\xa3\x9e\x1fkW\xdcoK\xaa7%\xd5}MUv\x9de\xf9\xedZ\xc4\xefv\x8c\x8a.\x91rˀJ\x00[\xbfON\xd1b\xc8\n\x80u\x88\xd8\xc1\xfb\f+*\x8df\x050^\xbb\xc0l@\x19S\x88TnC\xd6\v\xd2mpi\x98\bl\xc0 k\xb2Q\nQ\x1fz,W\x84\xb0\x03\xe9\x11j9\x90\x00[\x1c\x11\x98r\x0e\xe0\x13\a\xbfQ\xd2w\xd0f\xbe\xda\x1a\x9a\x81\x8c\x01\x95\xea7\xf3ey\u0380Y\xc8\xfa\xfd5\b,J\x12O J]\x1b<\xd0\t\xbf\xe7\x00J|\x1b{\xc5\xe7\xd5\x1f\xcaƵ\xca5\xe6pS\x99\xd6=\x0e\xaa\x1bcCD\xff\xeb\xe6\xee\xe3\x8f\x0fg\xcbp\x8euAZ\xb0\fjB\x9a\x89\xab\xacA\xf0\b\x81`\b4\xb1\xca\xed1i\xa4\x10\x91\xc4N\xadU\xbf\x93\xe19Y\x9dA\xf8\xb79\xdb\x03Ȩ\xeb)0y\x8a\x90\v\x89cS\xa0\x19/Zɵ\f\x84\x91\x90\xd1\u05f9\xca\xcb\xcaC\xd8~B-\xed,\xf5\x03RN\x03܇\xe4L\x1e\xbe\x03\x92\x00\xa1\x0e{o\xff>\xe6\xe6|\xef\\\xd4))\x94\xe4\xb6\xf3\xca\xc1A\xb9\x84߃\xf2f\x96yP\xcf@\x98kB\xf2'\xf9\xca\x01\x9e\xe3\xf8#\x93h\xfd.tЋD\xee\xd6뽕\xc9Rt\x18\x86\xe4\xad<\xaf\x8b;\xd8m\x92@\xbc6x@\xb7f\xbbo\x14\xe9\xde\njI\x84k\x15mS.⋭\xb4\x83\xf9\x8eF\x13Ⳳ\x17\xddS\xbf\xe2\x02_!O\xf6\x84\xda#5U\xbd\xe2\x8b\ny)Sw\xff\xee\xe1\x03LH\xaaRU\x94\x97\xd0\v^&}2\x9b\xd6\xef\x90\xea\xb9\x1d\x85\xa1\xe4Dob\xb0^\xca\x0f\xed,z\x01N\xdb\xc1\nO\x1d\x9b\xa5\x9b\xa7\xbd-\xb6\x9b\x1d E\xa3\x04\xcd<\xe0\xceí\x1a\xd0\xdd*\xc6o\xacUV\x85\x9b,\xc2\x17\xa9u\xfa\x98̃+\xbd'\x1b\xd33pEڅ\xe1\x7f\x88\xa8\xb3\xb8\x99\xdf|\xda\ueb2ec\xb5\v\x04O\xbd\xd5\xfd4\xfc3\x9a\x8eFq\xce߲1\xe4\xef\xc5n\xe7;W/\x0fEdK8k\xd8\x06.\xbc\xfbu^\x8a\xa9~%3\xd5\xd1Gnt\"*\xcdw\xf4y\xb5t\xe8K\xb9@\xa2@\x17\xab3P\xefJP\xf9Ǡ\xacgP\xfey<\b\xd2+\x81'\xa4\r\x97\x95\x1ax\x8fO\v\xabw~CaO\xc8\xf3\x96ϛ\x9b\xca\x1e\xce߃WXZlʋE\xceVhNXd\t\xa4\xf6\xa7\xbcr\xda\x1e\x9d\xbe\x83\x7f\xfe[\xfd\x1f\x00\x00\xff\xff\xbeM\x1a\xea\xb1\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcWMo\xe36\x10\xbd\xfbW\f\xd0K\v\xac\xe4\x06E\x8b·\xd6\xd9C\xb0\xe96\x88\xb7\xb9S\xd4HbC\x91,9t6E\x7f|1\xa4\xe4\x0fYv\x9c\xcb\xea\xe6\xe1p\xf8\xe6\xcd\xcc#]\x14\xc5B8\xf5\x84>(kV \x9c¯\x84\x86\x7f\x85\xf2\xf9\xd7P*\xbb\xdc\xde,\x9e\x95\xa9W\xb0\x8e\x81l\xff\x88\xc1F/\xf1\x16\x1be\x14)k\x16=\x92\xa8\x05\x89\xd5\x02@\x18cI\xb09\xf0O\x00i\ry\xab5\xfa\xa2ES>\xc7\n\xab\xa8t\x8d>\x05\x1f\x8f\xde\xfeX\xde\xfcR\xfe\xbc\x000\xa2\xc7\x15\xd4\xf6\xc5h+j\x8f\xffD\f\x14\xca-j\xf4\xb6Tv\x11\x1cJ\x8e\xddz\x1b\xdd\n\xf6\vy\xefpn\xc6|;\x84y\xccaҊV\x81>ͭޫ\xc1\xc3\xe9\xe8\x85>\x05\x91\x16\x832m\xd4\u009f,/\x00\x82\xb4\x0eW\xf0\x99a8!\xb1^\x00\f)&XŐ\xdd\xf6&\x87\x92\x1d\xf6\"\xe3\x05\xb0\x0e\xcdo\x0fwO?m\x8e\xcc\x005\x06镣D\xd4\x7f\xc5\xce\x0e\xd3\x04@\x05\x100\xc0\x01\xb2;\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10\x1c%;\x1c\x9c\xa5m\v\x8d\xd2X\xeel\xce[\x87\x9e\xd4Hy\xfe\x0e\x1a\xea\xc0z)\v\xfe8\xf1\xbc\vj\xee,\f@\x1d\x8e\xe4a=p\x05\xb6\x01\xeaT\x00\x8f\xcec@\x93{\x8d\xcd\xc2\fٔ\x93\xd0\x1b\xf4\x1c\x06Bg\xa3\xae\xb9!\xb7\xe8\t\x1aE\xaf\xcb41\xaa\x8ad}XָE\xbd\f\xaa-\x84\x97\x9d\"\x94\x14=.\x85SEJĤQ+\xfb\xfa;?\ff8:\x96^\xb9!\x03yeڃ\x854\x1d\xef(\x0f\xcfK\xee\xae\x1c*\xa7\xb8\xaf\x02\x9b\x98\xbaǏ\x9b/0\"ɕ\x1aZl\xe7z\xc2\xcbX\x1ffS\x99\x06}ޗڔc\xa2\xa9\x9dU\x86\xd2\x0f\xa9\x15\x1a\x82\x10\xab^Q\x18{\x9dK7\r\xbbNR\x04\x15Bt\xb5 \xac\xa7\x0ew\x06֢G\xbd\x16\x01\xbfq\xad\xb8*\xa1\xe0\"\\U\xadC\x81\x9d:gz\x0f\x16Fy&j^\x01\x128\xe1[\xa4\xa9u\x82\xe5Kr\xe2\xe3_:q,X\xdfcٖ\xac9a\x00\x92\xf5\xe8\x87i\xa1.a\x80\xd9F\x9fE2\xf67\xd3\xc0\xbc\xb2\xa0\xb0\xd8\x1db:=\x9a?4\xb1\x9f?\xa0\x80\xdf\x13\xe6{\xdb^\\_[C<\x17\x17\x9d\x9e\xac\x8e=n\x8cp\xa1\xb3o\xf8\xde\x11\xf6\x7f:\xf4\xf9\x1a\xbe\xe8:\xde滫\xef\x82c\xd4g\xcf}D\xbeA\xf0|\xa6\x83\xc3UQ\xae\xc04x^\x95\xe8zs\xf7\x1e\nϸ\xbf\xa3Hw\xa6\xb1o\xa4\xb8w\x9c\xf5;#\x03\xe3\x97\xde\x10o\xf74\xbfBƞ\xe6-\xf9\xeeD\xf8\x14+\xf4\x06\t\xc3^\xa9_\x14u\xb3\x11\x01^:%\xbb\xb41\r\x04_\x02!X\xa9\xe6$\xf5\n\xf8\xac#\xca\xe3\xccP\x16iXg\xcc\f\xfe\xc4|F\xfd\xce\x1dP\f\x8at\x95\x82\x92\xa0\x18ޡ\xa1\xc9\x7f\xa4ZF\xef\xd3\x15\x95\xad\xfc2\x99n\xb8VDG\xe5\xf9\xeb\xf1\xfe\r%\xbd\xdd{\xa6\x17\xb7P&\xa3q\x1e\x8b\xa0Z~A\xf1\x1akiҸS2\xf2w\xfc\xc2;&j\xb6\xa2\xf8թ<\x80o@\xfc\xb8ŝ\x8f&\xdf\xf3\xd37l\n\x88\x81\x9f[ \x85\x99\xc1X!Ԩ\x91\xb0\x86\xea5\xdf\\\xaf\x81\xb0?\xc5\xddX\xdf\vZ\x01\xdf\xff\x05\xa9\x9962QkQi\\\x01\xf9x\xae\xcbf\x13w\x9d\b3cx\x94\xf3\x03\xfb\xcc5\xc6n\x18/v\x06\x9c\xbd_\n\xf8\x8c/3\xd6\ao%\x86\x80\xa7ct6\x93\xd9!81\x06~\xa4\xd5\a,\r\x7f\x19\x06\xcb\xff\x01\x00\x00\xff\xffx\xae@\xbaJ\x0e\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4ZK\x93\x1b\xb7\x11\xbe\xef\xaf\xe8Z\x1flWi\xc8HI\\)ޤU\x9c\xda\xc4\xdel\x89+]\\>4\aM\x0e\xbc3\x00\f`\xb8b\x1c\xff\xf7T\xe3A\xce\f\x87\xe4\x92r\xa4\xb9Hģ\xfbC\xbf\xd1آ(\xae\xd0\xc8\x0fd\x9d\xd4j\x06h$}\xf4\xa4\xf8\x97\x9b<\xfe\xcdM\xa4\x9e\xae_^=J%fp\xd3:\xaf\x9bw\xe4tkKzKK\xa9\xa4\x97Z]5\xe4Q\xa0\xc7\xd9\x15\x00*\xa5=\xf2\xb0\xe3\x9f\x00\xa5V\xde\xea\xba&[\xacHM\x1e\xdb\x05-ZY\v\xb2\x81xf\xbd\xfe\xd3\xe4\xe5w\x93\xbf^\x01(lh\x06F\x8b\xb5\xaeۆ\x16X>\xb6\xc6M\xd6T\x93\xd5\x13\xa9\xaf\x9c\xa1\x92i\xaf\xacn\xcd\fv\x13qo\xe2\x1b1\xdfk\xf1!\x90y\x13Ȅ\x99Z:\xff\xaf\xb1\xd9\x1f\xa4\xf3a\x85\xa9[\x8b\xf5>\x880\xe9\xa4Z\xb55ڽ\xe9+\x00WjC3\xb8c\x18\x06K\x12W\x00\xe9\x88\x01V\x01(D\x10\x1a\xd6\xf7V*O\xf6\x86)da\x15 ȕV\x1a\x1f\x84r\xaf\x05D\x80\x10\x11\x82\xf3\xe8[\a\xae-+@\aw\xf44\xbdU\xf7V\xaf,\xb9\b\x0f\xe0\x17\xa7\xd5=\xfaj\x06\x93\xb8|b*t\x94f\xa3x\xe7a\"\r\xf9\r\x83v\xdeJ\xb5\x1a\x83\xf1 \x1b\x82\xa7\x8a\x14\xf8J:\x88\xa7\x85't\f\xc7\xfap\xcaq\xc6a\x9e\xb7;\x8f\x8d\xe9!\xb8\xb1\x84\xbb\xad\x11\x82@Oc\x00\xb6\xf2\x04\xbd\x04_\x11K>\x18\x16J%\xd5*\fEM\x80װ\xa0\x00\x91\x04\xb4f\x04\x99\xa1rb\xb4\x98\xa8L\xb4\a\xebn0zJ6\xbc\xfe\x8fF\xd5\x03t\xaf\xc5\x05P\xce\xe2\x1b\x17\xf7\xb8~\xe8\x0e\x9d\xb4\x8f\x8a\u009a̼5\xb5FA\x96\xd9W\xa8DM\xacY\x04oQ\xb9%\xd9\x030\U000b61cd\xe9\x83y\x9f\xe9uf\xce\x11F\xf2\x9d\xb9\xd7\x16W\x04?\xe82\x04(6iK=\x9bv\x95nk\x01\x8b\xcc\x05\xc0ymG\r\x9c\x11\xc7]\x89n&;\xf0\xb3>\xcf\xc3\xe8;\xb4s<\x9d\x94\xec#R\xabq\x0fz\xbd\xa2q\xef\x89\xd3\xeb\x971\\\x95\x1558K+\xb5!\xf5\xfa\xfe\xf6ß\xe7\xbda\x00c\xb5!\xebe\x0e\x9f\xf1\xeb$\x87\xce(\xf4E\xfdߢ7\a\xc0\f\xe2.\x10\x9c%\xc8E\x9b\x8cc$\x12\xa6\xa8\x1e\xe9\xc0\x92\xb1\xe4Hż\xc1è@/~\xa1\xd2O\x06\xa4\xe7d\x99LVT\xa9՚\xac\aK\xa5^)\xf9\x9f-mǶ\xc7Lk\xf4\xe4<\x84P\xab\xb0\x865\xd6-\xbd\x00Tb@\xb9\xc1\rXb\x9eЪ\x0e\xbd\xb0\xc1\rq\xfc\xa8-\x81TK=\x83\xca{\xe3f\xd3\xe9J\xfa\x9c2K\xdd4\xad\x92~3\r\xd9O.Z\xaf\xad\x9b\nZS=urU\xa0-+\xe9\xa9\xf4\xad\xa5)\x1aY\x84\x83\xa8\x906'\x8d\xf8ʦ$\xebzl\xf7\xac&~!ӝ\xa1\x1e\xce} \x1d`\"\x15\x8f\xb8\xd3B\x8e]\xef\xfe>\x7f\x80\x8c$j**e\xb7tO.Y?,M\xa9\x96\x1c\x03x\xdf\xd2\xea&\xd0$%\x8c\x96ʇ\x1fe-Iyp\xed\xa2\x91\x9e\xcd\xe0ז\x9cg\xd5\r\xc9ބ\xb2\x82cYk\xd8\xcc\xc5p\xc1\xad\x82\x1bl\xa8\xbeAG\x9fYW\xac\x15W\xb0\x12\x9e\xa5\xadn\xb14\\\x1c\xc5ۙȥ\xce\x01\xd5\x0eꗹ\xa1\x92\x15˲\xe5\x9dr)S\xa4[j\v8\\ޗ\xd3x\x00\xe0o4\xca\r\x17\x9d2:\xfeތ\x11ʀU'`\xe7h\x9c\x82g\xdd\x0f\x9e\xdd/\x87\xf0\xed\x1eKF;\xe9\xb5\xdd0\xe1\x18\xbd\x87\x06qP7\xfc)-\xe8\xc4\xe1\ued201ؼ\x15|\x85Ѻ\xb9x\xe3\xe0\xd6*\xb5υ?\xad\xce\x02f\xb48\x81+qD\xb0\xb4$K\xaa\xa4\x1c\x05\x8fU&#Ⱥ5\xc3>\xc6Ö\x02GR\xc6(\xe2\xd7\xf7\xb79-d!&\xec{\x91\xff\xa4|\xf8[J\xaaEȢ\xa7y\x8f\x9a(\x7f\xb7\xcb\b\"\xc4F\xaf\x01\xc1H\x8a\xb5\xe76/\x81T\xce\x13\x8a4\xc8\xe1\xc0R\x9a{\x11c\xdeA\x90\xfc\xed\xf2\x17\xeb\x04\x90c\xb0\x14\xf0\xcf\xf9\xbf\xef\xa6\xff\xd0\xf1\x1c\x80eI.\x14ٞ\x1aR\xfeŶ\xee\x17\xe4\xa4%\xc1Ubcjz\x012\xca|\x1bֳ\xd9H\x17\x0f\xbe\xa5\bO\xd2W\x01\xa8\xd1\"\x1d\xf0)\x1c\xc1\xe3#\x81NGh\tj\xf98\xe2?\xf1\xbb\x0eU\xd3\x0e\xe6o\xec=\xbf_\xc37э\xaf\xf9\xe7u\x84\xb1M\xe0]\a\xdb\xc1\x89^f\xe5jE\xbb\xf2l\xcfX8\xe1p\xa8\xfe\x16\xb4\xe5\xb3*\xdd!\x11\b\xb3\x9eb\xa4$\xb1\a\xef\xa7W?_\xc37}\x19\x1c`%\x95\xa0\x8f\xf0\nd\xba#\x19-\xbe\x9d\xc0C\xb0\x83\x8d\xf2\xf8\x919\x95\x95v\xa4@\xabz\x13K\xe35\x81\xd3|\xb7\xa2\xba.b\xa9$\xe0\t7\xa0\x97\a\xf8d\x15\xb1i\"\x18\xb4\xfeh\xb9\x94\xe4p\xdci\xf6\xeb\x87\xfc=\xcf_B=\xf1,\xef\xfdb\xb9\xf8\x99\x92\b\x85\xf3'H\xa2{\xe9\xb8@\x12\x8f킬\"OA\x18B\x97\x8e\xe5P\x92\xf1n\xaa\xd7dג\x9e\xa6O\xda>J\xb5*\xd8\x18\x8b\xa8u7\rw\xd9\xe9W\xe1\x9fK\x0f\x1en\xbd\x9fz\xfa\xde-\xfd\U000cb039\xbb\xe9%\x12\xc8u\xee\xf3s\xd7A9\xccS\xe95\xa4\xc9>\xffTɲʷ\x9eN\xb4mP\xc4p\x8cj\xf3\x85|\x87\xe5\xdcZF\xb4)RӮ@%\xf8\xffN:\xcf\xe3\x97\b\xb6\x95\x9f\x14\\\xde߾\xfd\x92\x1e\xd5\xcaK\"Ɂj>~\x1f\x8b\x1d\xaa\xa2AS\xc4\xd5\xe8u#\xcb\xc1j\xaefo\x05+i)ɞ(\xff\xde\xf5\x16\xe7\x02u\xa4.ޮ9\xab\xfe\xf4\xb8\x1a)\xf8\xba\xfd\xccce\xe1Qy\x9d6\x85\a\\9@K\x80Рa\x8bx\xa4M\x11+\x0e\x83\x92\xcb\x05\xae\b\xb6\xfd\x1b@cj\xce鱊\x18\xa1\x98\xea\xdf$\x1et\xe1|\x87\x042\xaa\xcaܯ\x9a\x93\xe7;\xf3\x97\x13\xce\xfb\x01\x90?VP\xdbn^\xa9\xd5R\xaeZ\x1b\xeeb\xfb\x92Rm]㢦\x19x\xdb\xee\x13z\x86 \x1fx\xc9\xf1\xf3\xbf\xef,\xcd\x16~\xa2\xf58~\xaa^Cr\xff0\xa4\xdaf\x1fJ\x01\x8f\xdaH\x1c\x19\xb7\xe4\xfc\x9e\xf7\xf2\xc4\xf5\xf59>\x16\x8d\xf2\x92\xbbuz&\x18\xb9\x95&CO\x05|\xbe\x99v;ãJ?#6X\xfa\xb5\xe5\xebH\x1fw1\xde8\x18\xac\xe1;\xf3`\xc8h1\x18\xe9\x87\xc1\xc1d\xaf{\xddE\xba\xdfM\t\x8f\x12g\xf4S\xe2cK\x92iL\x8e>?\xc1p\xd9}iG\xa5\xd4|\xfd\xeauv/\xd1\xf9\xcd>\x99\xd0\t\xb5\"9\x86l8\x0et\xdek\x12㱖H\x97\\\xdc\x19j\x14\xa6F\"\\\xa3\xf8\x96\xb7DY\x93\x80\xfc(w&\x95\x05-9GG'͍\x88\x04\xef\xf0\x05\xe6\xa1\"p\xa1\xaf\xf8\xb5\xdb\xd2l\x1d\x89\xd0\xd6\x1a\x11\xc2~\xc6^j۠\x8f-\xf2\x82I\\\x16\xbdF}\xb6!\xe7pu\xcai\x7f\x8c\xabb\x7f&m\x01\\\xe8\xd6o\x1b4\xbd\x8c\xf4\xb5K\x86v^\x8fh\xb4\xf5ѷq\xf4U6\xe9e[\xd7aO7:\xec\x1el\x03\xaa\x05\x8d\xd7uG\x1aD\xc7\x00V\xe8N\x89\xea\x9e\u05ccy\xdd6\xa4\x1du;8\x12\xbe\xef\xe8idt\xef\x05\xb5;y\x93]fd\xee\xfb\xe0\rg\x9d?1\xba\xc4\xdd3H\xa8t\x9d=\\{\xacA\xb5͂,\vg\xb1\xf1\xe4\x06\x81\x1f\x95\xe8Jr\xec\xf6\xb7۟\x95\x1a)\xa5\x0eF\x89*\xb4\xde],\x13\x84t\xa6\xc6\xcd\xf6,\xa1\xe6f\xff\x1ao\xd1\xee\x8c<{\xba\xa1C5\xc4\xf1\xd6b\xc0\xf4V\xab\x03\xb7\xd4\xec\xe4R\xf9\xef\xfer\xa4h\x97\xca\xd3j\x90F\xd2<\x8b\xf3\rs\xf9\xffp8R\x039\x85\xc6U\xda߾=a\x1a\xf3\xed\xc2\xec\"\xbbz>\x04\xc4\xd0\xfdO\x8b\x92)\x8c@\xdd\x05\x9c\xb3\xfc\xb7\xff\xa0\x7f\x89\x15\xcf{\x14N\xe4\xab\xf4\xf7\x05cYaN\x06-DŽ\xf0\xb6t3|)}\x01N\x86\x068W\xbb\xb1\xfc-+T\xab\xd1\xfe\x88V\xa1\x80\xd3v\xff\xa1\x0fN&\xa0\xfe\x81>g\xee\x195\xa7\xbd\xc1\x80\\th\xa7g\x95\xeeH\xbbؾ8\xce\xe0\xb7߯\xfe\x17\x00\x00\xff\xff\x8fTl=\x19$\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z_s\x1b\xb7\x11\x7fק\xd8Q\x1e\x92\xcc\xf8\xc8\xdam3\x1d\xbe\xd9r\xd3Q\x9b\xa8\x1aS\xf6K&\x0f\xcbÒ\x87\xe8\x0e@\x01\x1ce6\xcdw\xef,p \xef\x8e )҉}/6\xf1g\xf1\xc3\xfe߅\x8a\xa2\xb8B#?\x90uR\xab\x19\xa0\x91\xf4ѓ\xe2_n\xf2\xf877\x91z\xba~y\xf5(\x95\x98\xc1M\xeb\xbcnޑӭ-\xe9--\xa5\x92^juՐG\x81\x1egW\x00\xa8\x94\xf6\xc8Î\x7f\x02\x94Zy\xab\xeb\x9al\xb1\"5yl\x17\xb4he-\xc8\x06\xe2\xe9\xe8\xf5\x9f&/\xbf\x9b\xfc\xf5\n@aC30Z\xacu\xdd6\xb4\xc0\xf2\xb15n\xb2\xa6\x9a\xac\x9eH}\xe5\f\x95L{eukf\xb0\x9b\x88{\xbbs#\xe6{->\x042o\x02\x990SK\xe7\xff\x95\x9b\xfdA:\x1fV\x98\xba\xb5X\xef\x83\b\x93N\xaaU[\xa3ݛ\xbe\x02p\xa564\x83;\x86a\xb0$q\x05\xd0]1\xc0*\x00\x85\bL\xc3\xfa\xdeJ\xe5\xc9\xde0\x85Ĭ\x02\x04\xb9\xd2J\xe3\x03S\ued40\b\x10\"Bp\x1e}\xeb\xc0\xb5e\x05\xe8\xe0\x8e\x9e\xa6\xb7\xea\xde\xea\x95%\x17\xe1\x01\xfcⴺG_\xcd`\x12\x97OL\x85\x8e\xba\xd9\xc8\xdey\x98\xe8\x86\xfc\x86A;o\xa5Z\xe5`<Ȇ\xe0\xa9\"\x05\xbe\x92\x0e\xe2m\xe1\t\x1dñ>\xdc2\x7fp\x98\xe7\xed\xcecc\x06\bn,\xe1nk\x84 \xd0S\x0e\xc0\x96\x9f\xa0\x97\xe0+b\xce\a\xc5B\xa9\xa4Z\x85\xa1(\t\xf0\x1a\x16\x14 \x92\x80\xd6d\x90\x19*'F\x8b\x89JD\a\xb0\xeeF\xa3\xa7x\xc3\xeb\x7foT\x03@\xf7Z\\\x00\xe5\xacs\xe3\xe2\xc1\xa9\x1f\xfaC'\xf5\xa3\xa2\xb0&\x1dޚZ\xa3 \xcb\xc7W\xa8DM,Y\x04oQ\xb9%\xd9\x030Ҷ\x87\x8d\x19\x82y\x9f\xe8\xf5f\xceaFg;s\xaf-\xae\b~\xd0epP\xacҖ\x06:\xed*\xdd\xd6\x02\x16\xe9\x14\x00\xe7\xb5\xcd*8#\x8e\xbb:\xba\x89\xec\xc8Άg\x1eFߣ\x9d\xfc\xe9\xa4d\x1b\x91Z\xe5-\xe8\xf5\x8a\xf2\xd6\x13\xa7\xd7/\xa3\xbb*+jp֭Ԇ\xd4\xeb\xfb\xdb\x0f\x7f\x9e\x0f\x86\x01\x8cՆ\xac\x97\xc9}Ư\x17\x1cz\xa30d\xf5\xff\x8a\xc1\x1c\x00\x1f\x10w\x81\xe0(A.\xead\x1c#\xd1a\x8a\xe2\x91\x0e,\x19K\x8eT\x8c\x1b<\x8c\n\xf4\xe2\x17*\xfddDzN\x96\xc9$A\x95Z\xad\xc9z\xb0Tꕒ\xff\xdd\xd2v\xac{|h\x8d\x9e\x9c\x87\xe0j\x15ְƺ\xa5\x17\x80J\x8c(7\xb8\x01K|&\xb4\xaaG/lpc\x1c?jK \xd5RϠ\xf2\u07b8\xd9t\xba\x92>\x85\xccR7M\xab\xa4\xdfLC\xf4\x93\x8b\xd6k릂\xd6TO\x9d\\\x15h\xcbJz*}ki\x8aF\x16\xe1\"*\x84\xcdI#\xbe\xb2]\x90u\x83c\xf7\xb4&~!ҝ!\x1e\x8e} \x1d`G*^q'\x85\xe4\xbb\xde\xfd}\xfe\x00\tI\x94T\x14\xcan\xe9\x1e_\x92|\x98\x9bR-\xd9\a\xf0\xbe\xa5\xd5M\xa0IJ\x18-\x95\x0f?\xcaZ\x92\xf2\xe0\xdaE#=\xab\xc1\x7fZr\x9eE7&{\x13\xd2\n\xf6e\xada5\x17\xe3\x05\xb7\nn\xb0\xa1\xfa\x06\x1d}fY\xb1T\\\xc1Bx\x96\xb4\xfa\xc9\xd2xqdoo\"\xa5:\aD;\xca_\xe6\x86J\x16,\xf3\x96wʥ\xec<\xddR[\xc0\xf1\xf2!\x9f\xf2\x0e\x80\xbf\xac\x97\x1b/:\xa5t\xfc\xbd\xc9\x11J\x80U\xcfa'o\xdc9\xcfz\xe8<\xfb_r\xe1\xdb=\x96\x8cv\xd2k\xbba\xc2\xd1{\x8f\x15\xe2\xa0l\xf8+Q\x95T_r\xbd\x9b\xb0\x13\xa4\x12\xccv\xda*4\xbb\xa2H5\x00\xd5j\xa5\xd9\xc4\xc6Ҁ[\xcf\xcbX\xc9\x1d\xf9\xfc]U\xa00\xda\xc9\x17\x95\nvy \xf4\xf3\xbd\xf1\xa5\x17Zׄc^*-\xe8ĝﴠ\x9c\xb0x+\xf8\n}\xc2Ƌl\xab\xd4>o\xf9\xd3\xea,q\x18-N\xe0\xeaND\xb0\xb4$K\xaa\xa4\xe4\xfb\x8f\xe5c\x19d\xfdLi\x1f\xe3a\xfb\x80#\x812\x8b\xf8\xf5\xfdm\n\x86\x89\x89\x1d\xf6\xbdxw\x92?\xfc-%\xd5\"\xe4\x0e\xa7\xcf\xcej.\x7f\xb7\xcb\b\"D\x04\xaf\x01\xc1H\x8a\x19\xf76\x1a\x83T\xce\x13\x8an\x90\x9d\xa0\xa5n\xeeE\xf4\xf4\aA\xf2\xb7\x8b\xda,\x13@\x8e\xb2%\xc7+\xb4\x04\xb5|\xcc\xd8O\xfc\xaeC\xae\xb8\x83\xf9+[\xcfo\xd7\xf0Mt^\xd7\xfc\xf3:\xc2ئ-}\x03\xdb\xc1\x89Vf\xe5jE\xbb\xa4tOY8\xccr\x80\xfa\x16\xb4\xe5\xbb*\xdd#\x11\b\xb3\x9cb| \xb1\a\xef\xa7W?_\xc37C\x1e\x1c8J*A\x1f\xe1\x15{\x9f\xc0\x1b\xa3ŷ\x13x\bz\xb0Q\x1e?\xf2Ie\xa5\x1d)Ъ\xdeĂ`M\xe04W\x94T\xd7EL\x10\x05<\xe1\x06\xf4\xf2\xc09ID\xac\x9a\b\x06\xad?\x9a$v|8n4\xfbYS\xfa\x9eg/!\x8bz\x96\xf5~\xb1\f䙜\b\xe5\xc2'p\xa2_j]\xc0\x89\xc7vAV\x91\xa7\xc0\f\xa1K\xc7|(\xc9x7\xd5k\xb2kIO\xd3'm\x1f\xa5Z\x15\xac\x8cE\x94\xba\x9b\x86\n~\xfaU\xf8\xe7ҋ\x87Z\xffSo?\xe8M|~\x16\xf0\xe9nz\t\aRv\xff\xfc\xd8u\x90\x0f\xf3.\xe1\x1c\xd3d\x9b\x7f\xaadY\xa5Z\xaf\xe7m\x1b\x14\xd1\x1d\xa3\xda|!\xdba>\xb7\x96\x11m\x8a\xaeUY\xa0\x12\xfc\x7f'\x9d\xe7\xf1K\x18\xdb\xcaOr.\xefo\xdf~I\x8bj\xe5%\x9e\xe4@\r\x13\xbf\x8f\xc5\x0eUѠ)\xe2j\xf4\xba\x91\xe5h5\xe7\U00037085\xb4\x94dO\xa4\x7f\xef\x06\x8bS\x82\x9a\xa9\x06\xb6k\xce\xca?=\xae2\t_\xbf\x8b{,-<ʯӪ\xf0\x80+\ah\t\x10\x1a4\xac\x11\x8f\xb4)b\xc6aPr\xba\xc0\x19\xc1\xb6k\x05hL\xcd1=f\x11\x19\x8a]\xfe۱\a]\xb8\xdf!\x86dE\x99\xbats\xf2^\xaa/Ȝ\xf7# \xbf/\xa3\xb6=\xccR\xab\xa5\\\xb56T\xa0\xfb\x9cRm]㢦\x19x\xdb\x1e\xaa\xb9\x8e2\xf2\x81\x97\x1c\xbf\xff\xfb\xdeҤ\xe1'\x1a\xae\xf9[\rڰ\xfb\x97!\xd56\xfbP\nx\xd4Fbfܒ\xf3{\xd6\xcb\x13\xd7\xd7\xe7\xd8XT\xcaKJ\xee\xeeq$S\x95v\x8a\xde%\xf0\xa92\xed\xf7óB?\xc37pu\xcf\xe5\xc8\x10w\x91o\x97\x8c\xd6p\xcd<\x1a2Z\x8cF\x86np49\xe8\xd9\xf7\x91\xee\xf7\x90\xc2S\xcc\x19]\xa4\xf8\xc4\xd4\xf14\x06G\x9f\x1e\x9e8\xed\xbe\xb4\x8fTj.\xbf\x06\xfd\xec\x8b\xda,\xfbdB\xff\u05ca\xce0d\xc3~\xa0\xf7J\xd5\x1d\x9ck\x04\xf5\xc9ŝ!Gaj$B\x19\xc5U\xde\x12eM\x02\xd2S\xe4\x99T\x16\xb4\xe4\x18\x1d\x8d45\":x\x87\v\x98\x87\x8a\xc0\x85n\xea\xd7nK\xb3u$B3/Ä\xfd\x88\xbdԶA\x1f\x1f\x06\n&q\x99\xf7\xca\xdalC\xce\xe1\xea\x94\xd1\xfe\x18W\xc5\xfeL\xb7\x05p\xa1[\xbfm\xd0\f\"\xd2\u05eeS\xb4\xf3zD\xd9\xd6\xc7P\xc7\xd1WI\xa5\x97m]\x87=}\xef\xb0{\xa6\x0e\xa8\x16\x94\xcf\xeb\x8e4\x88\x8e\x01\xacНb\xd5=\xaf\xc9Y\xdd֥\x1d5;8\xe2\xbe\xef\xe8)3\xba\xf7nܟ\xbcI&\x93\x99\xfb>X\xc3Y\xf7\xef\x0e\xba\xc4ܷM\xcdJ\xd7\xc9µ\xc7\x1aT\xdb,\xc82s\x16\x1bOn\xe4\xf8Q\x89>'s\xd5\xdfn\x7f\x12j\xa4\xd4u0\xba^l09\xafAHgj\xdcl\xef\x12rn\xb6\xaf|cz\xa7\xe4\xc9\xd2\r\x1d\xca!\x8e\xb7\x16\x03\xa6\xb7Z\x1d\xa8R\x93\x91K\xe5\xbf\xfbˑ\xa4]*O\xabQ\x18\xe9晝o\xf8\x94?\xe6\x84#9\x90Sh\\\xa5\xfd\xed\xdb\x13\xaa1\xdf.L&\xb2\xcb\xe7\x83C\fo\x1eݢN\x152Pw\x0e\xe7,\xfb\x1d\xfe\x19\xc3%Z<\x1fP8\x11\xaf\xba\xbf\xaa\xc8E\x859\x19\xb4\xec\x13\u008b\xda\xcd\xf8}\xf8\x058\x19\x1a\xe0\x9c\xed\xc6\xf4\xb7\xacP\xad\xb2\xfd\x11\xadB\x02\xa7\xed\xfe\xf3&\x9c\f@\xc3\v}\xceؓU\xa7\xbd\xc1\x80\\\xf4hw\x8fI\xfd\x91v\xb1}g\x9d\xc1\xaf\xbf]\xfd?\x00\x00\xff\xff\f\x19\xe2z\x0f%\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Y_\x93۶\x11\x7fקع<$\x991\xa5\xdam3\x1d\xbd\xd9\xe7\xa6smr\xbd\xb1\xce~\xc9\xe4aE\xacHD$\x80\x00\xa0d5\xcdw\xef,@R\xfc'\xe9tnl\xbe\xdc\t\x00\x17?\xfc\xf6/\x96I\x92\xcc\xd0\xc8\x0fd\x9d\xd4j\th$}\xf4\xa4\xf8\x97\x9bo\xff\xe6\xe6R/v/g[\xa9\xc4\x12n+\xe7u\xf9\x8e\x9c\xaelJoi#\x95\xf4R\xabYI\x1e\x05z\\\xce\x00P)푇\x1d\xff\x04H\xb5\xf2V\x17\x05\xd9$#5\xdfVkZW\xb2\x10d\x83\xf0f\xebݟ\xe6/\xbf\x9b\xffu\x06\xa0\xb0\xa4%\x18-v\xba\xa8J\xb2伶\xe4\xe6;*\xc8\xea\xb9\xd43g(e\xe1\x99ՕY\xc2q\"\xbe\\o\x1cA?h\xf1!\xc8y\x17儩B:\xff\xaf\xc9\xe9\x1f\xa4\xf3a\x89)*\x8b\xc5\x04\x8e0\xeb\xa4ʪ\x02\xedx~\x06\xe0Rmh\t\xf7\f\xc5`Jb\x06P\x9f3@K\x00\x85\b\xcca\xf1`\xa5\xf2doYD\xc3X\x02\x82\\j\xa5\xf1\x81\x99V\x0e\xe8\r\xf8\x9cx\xcb\xc0*J%U\x16\x86\"\x04\xf0\x1a\xd6\x045\x12\x11\x84\x01\xfc\xe2\xb4z@\x9f/a\xce\xc4͍\x16s\xd5Ȭ\xd7D\xce\xef\a\xa3\xfe\xc0\xe7p\xdeJ\x95\x9dB\xf6\x7f\x06\xd5\xc3\xf3\xa0\xc5\x13\x91<\xe6\x14\xd64h*Sh\x14dy\xf3\x1c\x95(\b\xd8@\xc1[TnC\xf6\x04\x8a\xe6\xb5ǃ\xe9#y\xdf\xc8\xeb\xcc\\\xc3\xce5Tĵ\xbd\xed?t\x87.\xed\xfb\xa0E\xfd\x02\xd4F\rΣ\xaf\x1c\xb8*\xcd\x01\x1d\xdc\xd3~q\xa7\x1e\xac\xce,97\x01#,\x9f\x9b\x1c]\x1f\xc7*L\xfc\xb186ږ\xe8\x97 \x95\xff\xee/\xa7\xb1\xd5/ͽ\xf6X\xbc9xr=\xa4\x8f\xc3ሖ\x9d-\xab\xd5\xffE\xe0\xae\x19\xd2[\xad\xfa\xbc\xbe\x19\x8cN\x81\xed\bm\xe2\xed<\xb5\x14B\xed\xa3,\xc9y,MO\xea\xeb\xac/O\xa0\x8f\x03qz\xf72\x86\xb24\xa7\x12\x97\xf5JmH\xbd~\xb8\xfb\xf0\xe7Uo\x18\xc0Xm\xc8z\xd9D\xd7\xf8t\x92Gg\x14\xfa\xcc\xfe7\xe9\xcd\x01\xf0\x06\xf1-\x10\x9cE\xc8E'\x89c$jL\xd1y\xa4\x03Kƒ#\x15\xf3\n\x0f\xa3\x02\xbd\xfe\x85R?\x1f\x88^\x91e1\xe0r]\x15!\"\xed\xc8z\xb0\x94\xeaL\xc9\xff\xb4\xb2\x1d\xfb\"oZ\xa0'\xe7\x03\xd7Va\x01;,*z\x01\xa8\xc4@r\x89\a\xb0\xc4{B\xa5:\xf2\xc2\vn\x88\xe3G\xb6\x1f\xa96z\t\xb9\xf7\xc6-\x17\x8bL\xfa&\xa5\xa6\xba,+%\xfda\x11\xb2\xa3\\W^[\xb7\x10\xb4\xa3b\xe1d\x96\xa0Ms\xe9)\xf5\x95\xa5\x05\x1a\x99\x84\x83\xa8\x90V\xe7\xa5\xf8\xca\xd6I\xd8\xf5\xb6\x1dyd|B\"\xbcB=\x9c\x19A:\xc0ZT<\xe2Q\vM|\x7f\xf7\xf7\xd5#4H\xa2\xa6\xa2R\x8eKG\xbc4\xfaa6\xa5\xdap\x84\xe6\xf76V\x97A&)a\xb4T>\xfcH\vIʃ\xab֥\xf4l\x06\xbfV\xe4<\xabn(\xf66\x94\x1d\x1c\\+\xc3f.\x86\v\xee\x14\xdcbI\xc5-:\xfa̺b\xad\xb8\x84\x95\xf0$mu\x8b\xa9\xe1\xe2Hog\xa2\xa9\x84N\xa8vXݬ\f\xa5\xacY&\x97_\x95\x1b\x99F\x9f\xdah\v8Z\xdfgj:\x04\xf0\xb3\xc6t[\x99\x95\xd7\x163\xfaAG\x99\xc3E\x97̎\x9f7S\x82\x1aĪ\x93P\xe3\x8e\xe0\xe2J(\xea\xa5\x13\"\xf79Y\xea\xbec\xc9h'\xbd\xb6\a\x16\x1cS\xf1\xd0$Nj'\xf0\xa0Ņ\xb3q.\t\x0ediC\x96TJM\xb89W&M\x80\xefT\vc\x88\xa7\xf5\x01gB\xf3$\xe0\xd7\x0fwM\xf8m\x18\xae\xa1\x8f\"\xecEz\xf8\xd9H*D\xc8V\x97\xf7\x9e4\x04~\xee6\x11D\x88A^\x03\x82\x91\x14\xcb\xe06\xfe\x83T\xce\x13\x8az\x90\xdd\xceR=\xf7\"Ɩ\x93 \xf99\xe6\tV\t \xc7:)\xe0\x9f\xab\x7f\xdf/\xfe\xa1\xe39\x00Ӕ\x9c\v\xe5\x00\x95\xa4\xfc\x8b\xb6$\x10\xe4\xa4%\xc1u\x11\xcdKTrC\xce\xcfkid\xddO\xaf~\x9e\xe6\x0f\xe0{m\x81>bi\nz\x012rކ\xcf\xc6j\xa4\x8b\ao%\xc2^\xfa<\x005Z\xd4\a܇#x\xdc\x12\xe8\xfa\b\x15A!\xb74\xcd>\xc0M(4\x8f0\x7fc\xd7\xfa\xfd\x06\xbe\x89\xcer\xc3?o\"\x8c6Qv\xbd\xef\b\xc7\xe7\xe8\xc1[\x99et\xachG\xc6\u0081\x9dCⷠ-\x9fU鎈 \x98\xf5\x14\x03\x12\x89\x11\xbc\x9f^\xfd|\x03\xdf\xf498\xb1\x95T\x82>\xc2+\x90*rc\xb4\xf8v\x0e\x8f\xc1\x0e\x0e\xca\xe3G\xde)͵#\x05Z\x15\x87xA\xd8\x118]\x12\xec\xa9(\x92X\x92\b\xd8\xe3\x01\xf4\xe6\xc4>\x8d\x8a\xd84\x11\fZ\x7f\xb6,\xa9y8\xef4\xe3<\xdd?\x05\xbc\xbb[<\x87\x81\xa6\x9e|z\xee:\xc9ê\xaep\x862\xd9\xe7\xf7\xb9L\xf3\xe6vщ\xb6%\x8a\x18\x8eQ\x1d\xbe\x90\xef0ϕeD\x87\xa4n\x9e%\xa8\x04\xff\xef\xa4\xf3<\xfe\x1cb+\xf9I\xc1\xe5\xfd\xdd\xdb/\xe9Q\x95|N$9Q5\xc7\xe7crD\x95\x94h\x92\xb8\x1a\xbd.e:X\xcd5\xe3\x9d`%m$\xd9\v\xd5\u07fb\xde\xe2\xa6z\x9d\xa8>\xdb5W\x95\x9fN\xa1q\xb9\xf6wo/\xe0X\xb5\v\x1b\fG\x1d\xd6Eg#kЙ\xba\x0eO\xf0\xad\xfbӑ\xab\x0f\xaa\xbf\xbaA\xa6\xad\xcc$߿\xdb\xf0\x11\xae$\nK\xecv$\xbbO\x89\xc6H\x95]\x85\xb5i\xf0\xad\xc8\xf35v\xa2p\xee\xb6fϕ\xd7g\xed\xee\xb2K\xbd\x1f\x00\x01\xb4\x04\xc8gb\rm\xe9\x90\xc4*Π\xe4\x12\x8c\xab\xac\xbaT]\x13\xa01\x05\xd7I\xb12\x9b\xf2\xf5\xa6]\x99j\xb5\x91Ye\xc3\xe5h̔\xaa\x8a\x02\xd7\x05-\xc1\xdbj,\xe8\x8c\xfbt;\xa5\x174\xfe\xbe\xb3\xb4Q\xf7\x85^\xed\xf4\xa9z\x1d\xdc\xf1aHU\xe5\x18J\x02[m$N\x8c\xb3\xb1\x8f\x1c\x9d'nn\xae1\xa9\xe8I\x178\xa8\x1b\x8b\x13\x17\xd9\xda\x11벞G\xf8\xf2\x18\xdcq:;^렖~\xad\xf8\x8e\xd2G\x98L\xdf\xd9\ak\x8c\x16\xb3!i\xdd\xd86\x980\xf5\x9f\xf1ע\xc1|\xfb-\xec\x8f\xd9\xe1L\x99\xe0\x83\x00\x17\x00\xa5h\xdb\xfe\xf7\x0e\x0e.$%\x90\x84d˛-^2\xa6\x80\x03\xe0\xdc\xcf\xc1\x01\xb2X,\xdeц}\x03\xa5\x99\x14W\x846\f\xbe\x1b\x10\xf6/}\xf9\xfco\xfa\x92\xc9\x0f\x9b\x8f\uf799(\xaf\xc8M\xab\x8d\xac\x1fA\xcbV\x15\xf03\xac\x98`\x86I\xf1\xae\x06CKj\xe8\xd5;B\xa8\x10\xd2P\xfbY\xdb?\t)\xa40Jr\x0ej\xb1\x06q\xf9\xdc.a\xd92^\x82B\xe0a\xea\xcd?^~\xfc\xd7\xcb\x7fyG\x88\xa05\\\x11\x05\xdaH\x05\xfar\x03\x1c\x94\xbcd\xf2\x9dn\xa0\xb00\xd7J\xb6\xcd\x15\xe9~pc\xfc|n\xad\x8fn8~\xe1L\x9b\xbf\xf4\xbf\xfe\x95i\x83\xbf4\xbcU\x94w\x93\xe1G\xcdĺ\xe5T\xc5\xcf\xef\bхl\xe0\x8a\xdc\xdbi\x1aZ@\xf9\x8e\x10\xbft\x9cv\xe1W\xbd\xf9\xe8@\x14\x15\xd4ԭ\x87\x10ـ\xb8~\xb8\xfb\xf6OO\x83τ\x94\xa0\v\xc5\x1a\x83\b\xf8\x9fE\xfcN\xc2B\tӄ\x92o\xb8Q\xbb\x1aD<1\x155DA\xa3@\x830\x9a\x98\n\bm\x1a\xce\n\xc4;\x91\xab\x1e\xa40J\x93\x95\x92u\amI\x8b\xe7\xb6!F\x12J\fUk0\xe4/\xed\x12\x94\x00\x03\x9a\x14\xbc\xd5\x06\xd4e\x04\xd4(ـ2,`ٵ\x1e\xef\xf4\xbeNm\xcc6\x8b\v7\x8a\x94\x96\x89\xc0m\xc1\xe3\x13J\x8f>\"W\xc4TLw[\r\xdb#T\x10\xb9\xfc\x1b\x14\xe6r\x0f\xf4\x13(\v\x86\xe8J\xb6\xbc\xb4\xbc\xb7\x01e\x91Uȵ`\xbfG\xd8\xdan\xdcNʩ\x01m\b\x13\x06\x94\xa0\x9cl(o\xe1\x82PQ\xeeA\xae\xe9\x8e(\xb0s\x92V\xf4\xe0\xe1\x00\xbd\xbf\x8e_\x90xb%\xafHeL\xa3\xaf>|X3\x13$\xaa\x90u\xdd\nfv\x1fP8ز5R\xe9\x0f%l\x80\x7f\xd0l\xbd\xa0\xaa\xa8\x98\x81´\n>І-p#\x02\xa5\xea\xb2.\xff.\x12u0\xad\xd9Y\x1e\xd5F1\xb1\xee\xfd\x80\x02q\x04y\xac\xa88\xc6s\xa0\xdc\x16;*\xd8O\x16u\x8f\xb7O_\xfbLɴ'J\x8f7\xc7\xe8c\xb1\xc9\xc4\n\x94\x1b\x87\xacia\x82(\x1bɄ\xc1?\n\xce@\x18\xa2\xdbe͌e\x83\xdfZЖ\xdf\xe5>\xd8\x1b\xd4:d\t\xa4mJj\xa0\xdc\xefp'\xc8\r\xad\x81\xdfP\roL+K\x15\xbd\xb0DȢV_\x97\xeewv\xe8\xed\xfd\x104\xe2\bi\xbd\x16yj\xa0\x18H\x9a\x1d\xc6VA]\xac\xa4\x1a(\x19;d\x88\xa3\xb4\xf0\xdb洈U\x8b\xfb\xbf\xccq\x99m\xff\x1eG[~\xb3+k\x05\xfb\xad\x05T\xa6N\xfc\xe1P_\xa9\x9ej\x1f6\xcbF\xfb\xd4\x1dE\xb4m\xf0\xbd\xe0m\te\xd4\xeb\a\x1b\xcc\xd9\xc6\xed\x01\x144z\x94\t+D\xd6\xfaؽ\x88\xeeWT\xe0T\x01\x11\xd2$\xe01\xe1\xe0\x11&\x10\x03I\x9a`G\x03ubœ[&D\xb4\x9c\xd3%\x87+bT{\x88F7\x96*Ew#\xd8\n\x1e\xc0\x8b\x90\x15\x81xU\xc3Y\x81$\x8f\n\x05\xf1\xf5\xe7E\x15\xd3VQ\x86]>HΊ\xdd\f\xben\x93\x83\x82\xb4z\xd9\xf5;$K\xa8\xe8\x86I\x95\x12\x03\xa9\xb0kϞwjZZ-\xe9\x81\xec۸\xcc\r'\x91UI\xf9<\xc7\x10\x9fm\x9f\xce:\x90\x02\x1dʸ\x15Omo\xbb\x97@\xe0;\x14\xadI,\x93\x90\xb2E\xd3$\x15i\xa46\xe3t\x1fW]\xa4\xef\x1c\xa5~\x9c`\x9a\x83\x9d%Y\xdd5\xaf\x84\x03Q-\x0e\x06\nY\n\xb0ۨ-Q\xbb\xbeJ\xb6\xae\xef(RȒj(\x89\x14\xa33#\xbb\xb4\x1c\xb4\x9f\xabD\xce\xe8\xf4\xd0E\xb7\x7f\xf4x\b\xa7K\xe0D\x03\x87\xc2Hu\x88\xcc\x1c\x94\xba\x96\xa3XGP\x99ЦC\t\xe860\x01\x92XN\xdfV\xac\xa8\x9c\x87a\xd9\x13\xe1\x90R\x82\xb6\xda\x04]\xe6\xdd\xd8&\xc9\x1c\xf9\xfd$Sڣk3b\xb5\x0f/\xa5Q\xba\x96\xa1\x86\xbb\x96Dm\xa7{\x0ft\x8b\xffn\xe4\xe4\xb6\xff\x7f\"6\x18\x93\x13\x98vB\xfe\t\xba\x9f\xd9<=ʷ\x18ၾ$w+\x02ucv\x17\x84\x99\xf0uN\x12(\xe7\xbd9\xfeĴ9\x9e\xe93I\x93#\x13g\"L\x9c\xe2OH\x174\x19O\xdebd\xd3\xe4\xaf\xfdQ\x17\x84\xad\"\xd2\xcb\v\xb2b܀\xda\xc3\xfeI\xaa>P\xe65\x90\x91c\xf5\b\xe6\tLQ\xdd~\xb7.\x8e\xee\x92`\x99x\xd9\x1f\xec|\xe3\x10A\f\xcd\xf3\f\\\x82\xf12SPc\x1cN\xbe\"6\xbb/\xe8T_\xdf\xff|\x18+\xef\xb7\f\xce;\xd8Ȍйv\xbd\xb7\xa3\xfe\xfa|T\x10~A\x1f(\x06U.\xe7rA(y\x86\x9ds]\xa8 \x96>4tΘ^\x01&\x7f\x90Ϟa\x87`\xd2ٜÖ\xcb\r\xae=C\xc2\xf5O\xb5\x01\x0e\xed\x9a|X\xec\xf0d? \"0\x86\xcfe\x03\u05fc($r'閩KB\v\xb8?a\x9bY\xacҟ\xa3\x9f\xfaD\x0e\xf8I;ZZ\x89\xa9\x98\xcfij@\x99\xc9%\xa8k\xdf(ge\x9c\xc8\xc9ȝ\xb8 \xf7\xd2\xd8\x7f0@\xd3\xc8(?K\xd0\xf7\xd2\xe0\x97\xb3`\xd4-\xfc\x9c\xf8t3\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3M\ue10dW\x1cJ2\xa7\xc2\xf4\xae\x9b\xceMT\xb7\x1a\xd3uB\x8a\x05\xda\xcc\xe4L\x1e\xdfR\r\xd0\xfd\xe2I\xfd\x84_\xad\xb1p\xbf\xb8$3\xa7\x05\x94!\xb2\xc4\xec'5\xb0fE\xe6|5\xa85\x90ƪ\xf0<\x8e\xc8T\xac~7DZO\x9e\xf5\xee\xb7\xef\x8b\xe7\x98/XX\x93\xb3\xf0\x10\x8c\xac3p\xe0uw9\xbf\x9f\x85\x95ٌ^\x81\x13f\xbb\x8e$Gǻ\xe6 \xe5\x05\xe8@+\x8e.\xce,uiY\xe2\x11\x1a\xe5\x0fGX\x94#x\xe1X\xd5\xd0[\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I\xae\xf1\xa4\x8c\xc3\xe07\x9f\x87\xeb\x81ɘ\xb2\xb1SY\xfe\xd9Pnm\xbfU\xe0\x82\x00w\x9e\x80\\\x1d\xf8E\x17d[I\xed\xcc\xf6\x8a\x01\xc7\xf3\x8a\xf7ϰ{\x7fa\xa7\x9f\x9d\xb2\xafd\xde߉\xf7·8P\x18\xd1ᐂ\xef\xc8{\xfc\xed\xfdK\\\xa9LN\xcd\xec6`њ6y\x1c*\x92\xc9\xfa\xae\r8\xa6\x9f\x9b\xef\x92\xf2\xdeɞ\xdam\x16\x8b6R\x9b\xcf\xe9\xbc\xe1\xc8z\x1e\u0088\xa1g\x9cȱ\xcdF\f>\x8f\x16\xf5\xbdu\"W\x06\x94\xcf%:\x1b\x10\xe2\x8f\x17Ff\xa9S\x99\xfebc2\x90\xc6\xfc\xaeE\xf0\f7\xb9\x83\x9b\x9c%\x1e\xe3\xb0Z\xbc\x1c\xe9\xed\xdf~\xef\xe53\xad\xe4ڿ\xfb\x1bym\x87\xba\x90uM\xf7O5\xb3\x96z\xe3F\x06\x9e\xf6\x80\x1c\xf5պEyε\xc8\x1d\x0f\xe1\xf9喙\x8a\tB\x83\xda\x00\xe5\x19\x8a\x92F\xa6rةVQM\x96\x00\"\xa6\xe8\x7f\x04W\xa2f\xe2\x0e' \x1f\xcf\xe0zDt\x9d\xd3ٽ\x894\x89\x94\x8f\x1f\x9c\xc9jdI\xb6\x15(\x180\xc6a\xde\x1d=U!M/eq\x84C\xda\xc8\xf2'MVLi\xd3_\x82&\xadΥ\xf5\x91\xe4\xb3\xeb\xfe\xcaj\x90\xad9'\x82o\xbbi\x06g\xcd5\xfd\xce\xea\xb6&\xb4\x96\xad3\xe6\x86\xd5\xf1TףwK\x99\x89\xc7V\x98\xbf1Ғ\xa0\xe1`\x80,a\x95>\xefM\xb5B\n\xcdJP\xa1J\xc1\x91\x8dI+\x98+\xcax\x9b:%J\xb5c#`q\xab\xd4I\x01\xf0\x177\xb2\x97w\xac\xe4v\x88\xa0̽\xe3A\x1a\x10\xb6\"\xcc\x10\x10\x85\xc58(\xa7\x92q\n\x8f\fD\r\xcb\xd5sy\n\xdc6\x10m\x9d\x87\x80\x05\n$\x13\x93)\xb7~\xf7O\x94\xf1s\x90\xcdr\xde'\xa9\x1e\x81\x96\xa7\xe4h~\xed\r' t\xab\xf0\xf0\xdf\xe9\x8e-\xe3yk\xb6\x94#\x9c\xb6\xa2\xa8\x00\x95\x90\x18\xea\x06\a\x9e\tm\x80\xe6\xf2\x82\xf5\x8aZ!\x98X\xe7\xd1.;\x11\xda5\x87\ua954\x1c\xe8\xf8)d\xd7,\xae\xdf@\x13\xfd\xdaM\xf3BM\xd4\x11\xc1\x1d\x9b#\x1d\xb2)j\x95\x16\xa1\xc6@\xdd8\x91\x93D\xb5\xa2o]Π\x88\x8e\t\xc3\xfd*^3\xbef\x82e\xd0v@\xd7;\xc1L\xdfy\xb4 \xce\xea<\xda\t\xa2;pJ\x86\xedn\x00\xc0\nh\x88Cp\xed\x91k\x8ep$\x97@hYB\xe9r\x97\xd6\x15\xf1a\x89+|\x1b)nH\xee\xeexO0\x8b\xb2\xa1\r\x82N\xccê\r,Z\xf1,\xe4V,0\x18\xd7G\xeb\x90\x13\xb3T/\x9dޜ\xac\x8c\xe6\xf5K\xbe\x9a\x9e\xd3BC~\xcd\xe7\xa9\xe0?\x9dA\xcbd\xf3\xcdQ\t\x8f).\x98\xd3k\xae\x00{\xe4\xc7\xd9UL\xcd?1\xd8\x1fJ߸b\xe9\x17\x95\xc5ݥA\xf5\x9c\xc2m\x05\xa6\x02\x15J\xb3\x17X\x92^N\x9e\x90v\xc1K\xac\x93\xb3L\x15\\dW\xfe\xb9W9\x87\xd1M\xcb\xf9\x85\xe5m\xda\xf2d8l$\x8a\xd8!geՏ\xa5=\x86\x9c\xea\x8bl<\xf6+-\x86\xf5\x85\xb1\n\"\x14\x18\xca0\xb3\xa7qj\xbfXX\xda;\xdf\x1f\x96S`\xfe/,\xff\x0f/=̨\x94\xc8Gcn\x95fDb\x02V\x82\xc1zh\xec\xea+|?_\xe8\xfbc\xe1\xd4@\xfd\xa5\xf1\x123\xea\xc2f\xa05\x01g\xaf\xde\x04\xadA\xab\x9d+\x10\xed\x80\xcf\x19\xda\xf1ׅ\xbb\x05\x11\xc0\xa4\xf8\xf5k\x05A|}\xf5>\xd3\xe4\x9fI%\xdbDU\xdf\x04\xcaf\xaa;\xe67<(\xf4\xf0\a\n`\xe8\xe6\xe3\xe5\xf0\x17#}\xd9\af\xd1\x12\x800(\xea2\xb3L\x94l\xc3ʖ\xf2 \xb5\xdd\x1d\x02\xc7@\x1d\x9f%\xa0IE\x04\xe3\x8e\x01\xc3\xf8\x01Ñ/\x8d;\x969Z\xc5M\xfb\xa2y\xd5!'ׄ\fk>F\xac\xe1\xb1\xc7\x17\xafR\x05\xfb\x87\xd4z\x1c_\xe1\x91\x13I\xccTs\x9cPÑY,\xf6\xe2\xf3\x96\x9c*\x8dcb\xee\xb3Ud\xbc~\x1dF\x16~\xe6k.\x8e\xc1\xce\xd9\xeb+ް\xaa\xe2mj)2+(^\xaf\x142/\xfa<\xa9\x14`>`\x19\xaf\x82\x98\xad}xQ@sҖfk\x1a\x8e\xa9d\x98\xa5N\x9e\x98\xbdY\xad\u009bU(\xbcm]\xc2$\x17M\xfexL\xe5A\x8c\x93~\xa1M\xc3\xc4\xfa\x90)rYg\x92m\xe6Y\xe6~o!\x03\x9e\xe9\x873]t8\x12\xfa\xba\xeb҉H2\xa4-\x990\xf2\x92\\\x8b\x9d\x87\x9b\x80\xd3\v\x1f\x854\a\x17\xd9첶\x8c\xf3\xfem-\x04;\r\xcaߙԴv\xab\x1a\xf3\xf6\x93t\x95j\xe0\x94\x9f\x148~ك\xd1ώ\xbe\xa5\xe7_\xb7ܰ\x86\x83\xf5\xe86\xacL\xde!3\x15\xec\"\x92\xff&\xf1\x86\xd4r\x87\x90\xbe]\xf4\x9eQ\xec\x9eq\x926\xbf\xc9\x13\xb6\x97Q\xcc~\\\x11{\x06\xcdrE\xf1\r\x8b\xd5߰H\xfd\xad\x8b\xd3g8k\xe6\xe7\xe3\x8a\xd0O>\x81\tG\xfd\xf7\xb2\x84\a\xa9\xcc\\p\xf2\xb0\xdf?q\x92\xda\v\xd8$/\x89\b]\x13\xbb\xc4\x10Ç\x17\xa7m*}\xe8\x19\xdc\xe9_di\xd76w\xc6\xf2\xb8\xd7\xfd\xe0\xae\xf2\n\x14\b\xf7\xcc\xc7\x7f>}\xb9\x8f\xf0S>\xaf\xf7\x8c\xf7\x9e\x97p\x1eL\xe9\x91\xe3\x8f\xe6|1\x93\xc3\x16\xfa\x00\xaf|.B\x1b\xf6\x1f\xf8\xaa\xdb\v\xd2A\xd7\x0fw\b#\xf8i\xf8L\\\xac\xa2\x88'\x96K\xb0\x16+\xa2jT,\xeeV\x03\x88Ê\xdf\xfe3JP\xba'\xb3\x82\xc5d\xa1\xc6\xcb\n\xdeÝ[\xc7\xd8,\x9f\xac\xd3(vD:\x8e\xac\x98*\x17\rUf\x87l\xa3/\x06k\bff*\x9d3\xaaX\x0f\x9f\x01K\xa27\xbc\xfe\x85g\x91\xbbfxڻ\x8f\xbbS\xd61~\xffd\xf6\xe6\xc9+\xaec\xdcb/\x10S\x89\xcf\xc9\x02\x93WK\x93yM\xf4\xf0\xed\xa4\xb4\xcbc\x1c=\xad\xe7l\x14\x1dRM\t0v<\xaa:-h\xa3\xabēK/\xd3u\xf8\x1a\x99\xa1\xa6}\xc9&\x1d\x80\xc1>YQ\xf5\xb4\xd5\x16\x82>\v\xdbFi\xc5a)\xddnm\xb3\xab{a\xfc\xa2\x97)x\x9b#\xe1\xcc\xe7\\N~\xc8šgD\xfd`\xf6˪\xb6CL\x9dp\x18<\xeb\xdae\x14\x19O;\xb1\x99π\xe4\x19\x8c\x13\x9e\xfe@|\xe5\xe2\x8a$_\x04\xc9|\xf5\xe3\x0fE\xf4\x84V\xd3E\x05e\xcb\xe1\xd47\xff\x9ez\xe3\xe7_\xfd\v\xb3e\xbc\xfbg\x91\xdd3\xd0\xd6g\x1e\xbe/\xe8)\xe1!\xf7)9\xe6\xf0ap\xe0\x9e\x17+\xdcK\x94E\x01Z\xafZ\x1e\xaa\x94\n\x05\xd4@\x19\xba3\x1dW|T\x9dM\xdbpIKP7R\xacX\xe2\x84d\x80\xd6\xff\x1at\xde\xe3\xd9\x02?\xb6\xaa{\xdaq\xf2Y\xbc\x17i\xae\x86*\xca9\xf0O\x8c\x83\xfeYn\x85]W\x86@>\xa4\xc6\xf5\xeee\x15\xad\xb2f}GD[/\xad\x93\vƌ\a\x8b+\xa9\xa6+\xa4\x1dޙ0\xb0\x86T|\xbdU\xcc\xc0SC\x95\x06\\Q\xc6\x0e~\xdd\x1b\xe2\xa2\xcf\x15\xa7kW\nW\xb2\x82\x1a\x88\x06\x18g\x18[>\x8e\xd7\b\x8b\xef\xb02I\x8e$\xbd\xb2\x85z\xecJƨX\x8f=/\x9a0\xd5\xc9\aF\x9dE.hc\xf0\x02\f\xd2\x11\x89h<\f|\xb4w\xef\x8d\xd1\x01\xd8qN\xf3e̾`N\x1bZ'\xa2\x84y\xbdss\b\x06\x9f\x05Ve\xaf\xee\xae\xff\xc0b,\xb0#[\xaac1u\xd2\xf7\xee`;0\xe8\xaa[\xd0P\x12\u0600 V\x14)\xe3PNq\xeaWL$\xab\r\xa8\x9ft\x84\x83\x95\x80\x96ş\fU&.\xfdЏYIUSsEJj`aG\x9f溥\x9fIU\xea\xc4\xe3@\xbc\xd9\xe6ţ\b\xd7n\xac\xf5s\xf7\xd1jК\xaeC\x10\xba\x05\x05d\r\xc2\xe2=\xe6\x16\x93\x1eS\xb8\xd2\xe7\x8dE,-\xb5(\xa4\x85i\xa9\x9f\xc0\xb9p\xf1\xf44\xbcO\x8cQ\xeczTE\xa7U\x85\xbf<\xf8\bT\xef?w}\x80\x8bO\xfd\xbe>I\xecv\xec\xceF\xa8+\xf0\xc4\a\x8f\r\x8b\x91uJ\xa6\x8dę\x8f2'\x95\x94\xcfYn\xf6\xe7رK'1\xe1X\t\xafL.ekz~\x8eGxb\x99\xf8\xfc\xe7+\xdb\x17\x84y\xed.P\x8d\xe5V\xf3<\xbd\xcf\x03H1\xbc\x95\x86\xf2`d,_\xc6\x0e\xd5\xc4\x03\x02O\xe1\xf1d\xcew\x17\xfb\x90\xf7^e\xef`W\xddS\x9e^\x13t\xd7\xc7\xc7Ҫ>\xeb\x97\x04\x12_\x01\xed|\x92\xb17\x17\xe7\xec\x1fB\xfd\x84\x8b\xca\xc0\xf1\xe7\xae\xf7\x18\x1e\xdd2\x9d\xc3\f\"\x1di\x12\f>L\x15%ㄥOx\xa9ME\xf5\x9c{\xfa`\xfbD\xb7\xa3g\xae\xa2\x13\xfa8\"\x95\xe9{\xae\vr\x0f\xdb\xc4W\x87,<\xfdB\xa9Jt\xb9\x13\x0fJ\xae\x15\xe8C\xa6[\xe0}F&֟\xa4z\xe0횉/\xe3\x95\xdfS\x9d\x1f\xa82\xcc2\xad[Ob\xecM\xb0q\x89\xdf\xe6G\x8f\xff\xc0\x04\xe5\xec\xf7\x94.\xef\xff87Ä\xbek<\xf2N\xb1P\x01\xf1s\n\xd0k\xe8\x9ft\xcf\xfc\x84y/ɽL\x8a\xb1? fC\xa0L\x93%h\xb3\x80\xd5J*\xe3\xf2\xf7\x8b\x05a\xab\xe0 Y\r\x81q\xa2{͞\xb0T\xe2=\x1e\xbd\x05\x87e\xe5S\x89\n\xad\x0e\x86\x9c5ݹ\x8c$-\n\x1b\x13\xc0\amh*6y\x91\x9e\xc6P\xd5\xcbJ\x8e\n\xb9\xeb\xf7\x8f9\xbe\xa8>\x10\x9cC\x1d^gw\x06\x9d\x8f\x9di\r^\xcb \xdab\xef\x14eB\x9c\x1a\xbb\x1b\x0f\xbb\xf3L\xcd\xd7\beL=\xfa\xfd\r\x1e\xe2\xf6\a\xac\xbe\x93%[QQ\xb1\x1e\xbd\xd0V)ٮ\xab\xc0\x9bc\x0e\x11)[\x8c\x9c\x1bT\x05:\xfc\xc7!\xa6U\xa2wh\xe7k,ƴt\\\uee0f\xf2\x02E\xad\xba\x8b-\x9d\xaa\x9a\xb0\xf9\xd9Y\xc2\x11\x88\xb3\xb6?\x01\x91\xea\x9d(&\xaf\xe0\xf8@\x9bM\xdc՝\xc2P\x12\tQ\x1b\xbf\x1a\x12\"\xc41$\xf4}\x89.\xe2\xf9a02棜\x88\x8ei'\x06\xb78\rj~\xd3}'h\xe8\xee\x1c\x87\x0e=\b\xfeNJ\xbb\r \x1c\x13\xf9\xe2\xdc\xe9\xb8\xf7ǍX7\xd1ۺ=9v\xfd\xb6\ac\xef\n\xa4\x8db\xbbiB\xbc\xf9\xf7l\x95\x92\x17\xf7\xbf3-9\xfc\xc3\xc1\xafo|\x95qK\x95`b}\x12F~\xf5c\x13\xf1\xbc\a{Έ>\xac\xfc\xd5b\xfa\xa4Y:\xf8\x88\f^\xf6\xf0\xecg\xf2_\xfe/\x00\x00\xff\xffP\a\xb5\x16Cm\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfa\x15\x84\xeea?B\xdd^\xc7}ą\xde|\xb2\xe7\xae\xe3\xfe\xa7Y\n\xf5f\xf7\xf6\xe2Q\xc8\xfc\x9a\xdd\xd4ƪ\xf23\x18U\xeb\f\xde\xc3FHa\x85\x92\x17%X\x9es˯/\x18\xe3R*\xcb\xf1\xb3\xc1?\x19˔\xb4Z\x15\x05\xe8\xc5\x03\xc8\xe5c\xbd\x86u-\x8a\x1c4\x01\x0f]\xef\xfe\xb4|\xfb\x1f\xcb\x7f\xbf`L\xf2\x12\xae\x99ɶ\x90\xd7\x05\x98\xe5\x0e\n\xd0j)ԅ\xa9 C\xa0\x0fZ\xd5\xd55k\x7fp\x8d|\x87\x0e\xd9;ߞ>\x15\xc2\xd8\xff\xed}\xfe(\x8c\xa5\x9f\xaa\xa2ּ\xe8\xf4G_\x8d\x90\x0fu\xc1u\xfb\xfd\x821\x93\xa9\n\xae\xd9O\xd8U\xc53\xc8/\x18\xf3\xf8S\xd7\v\xc6\xf3\x9c(\u008b[-\xa4\x05}\xa3\x8a\xba\f\x94X\xb0\x1cL\xa6Eei\xc4w\x96\xdb\xda0\xb5av\v\xdd~\xb0\xfcb\x94\xbc\xe5v{͖\x86\xea-\xab-7\xe1WG\"\a\xc0\x7f\xb2{\xc4\xcdX-\xe4\xc3Xo\xef؍V\x92\xc1\xd7J\x83A\x94YN\f\x94\x0f\xeci\v\x92Y\xc5t-\t\x95\xff\xe2\xd9c]\x8d RA\xb6\x1c\xe0\xe91\xe9\x7f\x9c\xc2\xe5~\v\xac\xe0\xc62+J`\xdcwȞ\xb8!\x1c6J3\xbb\x15f\x9a&\b\xa4\x87\xadC\xe7\xe3\xf0\xb3C(\xe7\x16<:\x1dPAx\x97\x99\x06\x92\xdb{Q\x82\xb1\xbc\xec\xc3|\xf7\x00\t\xc0\x88D\x15\xaf\r\tG\xdb\xfa\xb6\xfb\xc9\x01X+U\x00\x97\x17m\xa5\xdd['{\xd9\x16J~\xed+\xab\n\xe4\xbb\xdb\u0557\x7f\xbd\xeb}f}\x8a\xfe\xff\xa2\xf9\xce\x1an0a\x18g_h\x960\xed\xa7-\xb3[n\x99\x06\x14\x03\x90\x16kT\x1a\x16\x81\xd49S\xba\x03\xaa\x02-T.\xb2\xc0\"jl\xb6\xaa.r\xb6\x06\xe4ֲ\xa9]iU\x81\xb6\"\xccCW:\xea\xa5\xf3\xf5\x18\xfaXpĮ\x95\x13S0$\x99~\xb6A\xee\x89\xe4&\x8f0\xedx\x88\x83\xf8\x99K\xa6ֿ@f\x97\x03\xd0w\xa0\x11L\x18E\xa6\xe4\x0e4R$S\x0fR\xfc_\x03\xdb\xe0\x94\xb0$\xa9\x16\x8ce4\x9f%/؎\x175\\1.\xf3\x01\xe4\x92\xef\x99\x06\xec\x93ղ\x03\x8f\x1a\x98!\x1e?*\rLȍ\xbaf[k+s\xfd\xe6̓\xb0A\xe9f\xaa,k)\xec\xfe\r\xe9O\xb1\xae\xad\xd2\xe6M\x0e;(\xde\x18\xf1\xb0\xe0:\xdb\n\v\x99\xad5\xbc\xe1\x95X\xd0@$)\xdee\x99\xffK\xe0\xb7\xe9u{03]!\x959\x83=\xa8K\x9dt9Pn\x88-\x17\xf0\x13\x92\xee\xf3\x87\xbb\xfb\xae\xe4\t\xe3\x99\xd2\x11\xc0\x18\x7f\x90\x9aBn\xc0낍V%\xc1\x04\x99WJHK\x7fd\x85\x00i\x99\xa9ץ\xb0(\x06\xbf\xd6`,\xb2n\b\xf6\x86\f\x13\nm]\xe1\xdc͇\x15V\x92\xdd\xf0\x12\x8a\x1bn\xe0\x95y\x85\\1\vdB\x12\xb7\xba\xe6vXّ\xb7\xf3C\xb0\x99\x11\xd6\x06]qWA֛j\xd8NlD\xe6&\x14\xaa\xe4F\x95\fԲ+㳟~!\xdd7\xfc:\xc0\xc3)\xc8\xd0+\x184JvK\xccom#\x8a\x9c\x83ƔfR\xd9\x03\x98\x87\xaa\xb5C\t\x0fe\x02\x93\x03ag\a*5Œ\x8e\x00im\xebP\xbe\xa2\xac&\xbc\x1fE\xb5*K\xc8\x05\xb7P\xecOB\xbf\x0fb\x8c̊\xfaak\xa7\xe7ŦG\xf4\xbc\x06&:\xedi2\xfe9\xd48\xb4\xc6\x7f&\xcbNF\xd4Ѥ\v\xac\x96-\x0f\a\xfdHx:$\rc\xab\r\xb3\x1au\xae\xc7\xeeI\x14\x05\xcedĸ\x82\xbc\x87Z\xbc;\xb1a\u0086Ѭ9a \xd9\xd2yQ\xcb\xd6gh\xec?\"8\xc0\x8eԾ\xeb\x1f=\x15n\x99\x84\xaf\xb6\xad\x85Î\x8c`\xc3\v3\x18\x82WH\xb3\x86q\xc5ֵ=\r\x03(+\xbb\xbfrm7\xaa(\xd4\x133\xa4l\xd1\bn\xc4C\xad\xddd\xff}\x0e\x1b^\x17\xf6\xda\xe1\xfc\x87\x98\xb4\x8eO3\ve\x85&\xf3\x149\xbd\xf7mq\xc08[\xf2&\xc6\bnr\xf0C\x94w?F\x80(\xe7\xc5VZ\xedD\xee\xcd\xf9\x81\xbabGU\x16\x96̈;\xc9+\xb3U\x16%B\xd5v\xacVʨ\xb0\xdcܭ\x06\xd0:\x93\x10\xd1%W\x98\xe4\xd4*\xf6ą%\x9d{s\xb7b_0\x86\x80К\xb9\xc9\xc6l\xad%ڹH\x7f\x9f\x81\xe7\xfb{\xf5\xb3\x01\x96\xd7d\xa2\x83{{\xc5ְA{\xa6\x01a\xe0O\xa05\xeawCH\xa8\xfa\xc0ej\xd8\xe3X\x82\xb2\xe1-\xbe0\xec\xed\x9fX)dmG\xa5\xee\xa8b#\xeaq\xcbK\xb5\x03\xfd\x1c\xe2\xbe\xe7\x96\xff\x88@\x064E\xe0\x8c\xa0{\x81!\xfa\xae\xf7\xf4\xe3:\xa2\x89]Ym:P\x85a\x97\x97\xa8\r.]\xc8yy\xe5 Ԣ\xb0\v!\xbb\xfd\x04Մ=\x9dF\x10G_\xc7ts\xaf~0N\xe4\x9fE\x9f\b\xcc\x11;P\xa9\x9c\xed\xa8\x1eۈ\x02\x98\xd9\x1b\ve\xd0Z\xad\xe7\xdf\tg\x86\x85|\x85\xa2\xf0`\f\xd2\xdb\x0fj\x9c \xb2.\n\xbe.\xe0\x9a\x94\xfc\x11\x9a\x8d\xeb\x9b1\xa2}\x06cEvN\x929\x88#\x04\xd3\xfe\x87\x1ee(t\xe0\x8f\xc0x\x04\xbc\xa7'\xc6)E\xd1!z\x9fZQ\xdc*\r\x19\xfa\xb0\xd7\xde7\x16P\x90?.\x15+\x94|\x00\xed\xb0hl\x15\xeaJ@\x01\xcd\x19\xba\x9d\x1a-\x8c\x90lS\xa3G\xbad\xa8%\xa22\"\xa4\xb1\xc0#\xc2|\x06\xde\xc1\u05ec\xa8s\xc8o\x8a\xdaX\xd0w\x99\xaa \x0f\x8bL\xa3\x9a9\x95\x87\x1f\x8eB\xf6\xf1K!2@>d\xae҂\x16yb\xa2݆2\xfb\nܚ\x13\xb2\xda\x0f\xa1\x8dQ&u\x8b\x01\x8b\r/\xffxyE\x12\xd0\xef\xbdߏa\\CC\xa6Y\xba\x99,\xfex\va\xa1\x8cPwRG\xcd\xe0;ך\xef\x8fp\xbdYL{\x01\xbe\xc7`\x0f8/C\xb5o\xc4\xfba\xff\xff\x8c\xdc?/\xbf\r-:s!\x91υ0\xb6\xc7f\xe3V\xb1\x90\xacc!\xa4'\x90t0QMNq\xf5\uf118g\x9d;\xb1\xc9\xd2Ȧ\x9f\x00\xffP\x94\xdc*\xf5\x98B\xbd\xff\xc1z\xed\x12\x16\xcbhc\x84\xada\xcbwBi3\\&\x85\xaf\x90\xd56\xaaY\xb8e\xb9\xd8l@#,Z\xe6ov\x05\x8e\x11\xebx\xf8\xc2:*+Za0\xae\x96\xe9\xc8R\xa2Fl(\x14\xa0F\xa1:\a\aC\vr r\xb1\x13y\xcd\v\xf2%\xb8\xcc\xdc\xf8x\x83_L\xabM\b\xc4\x01\xfeQ\xa9v\xc594a\x90\xc8\xc4ު\x97\x92\x80>~\x89\xb1\xd1a\xd58%\xc2R\xc2Ѿ\x91\x99\xba.\xc0\xf8\xeerr\x93[\x9dt\xd52˭1\x14|\r\x053P@f\x95\x8eS(E\x0e\\IU\xba\x11\xe2\x8eh\xd9~\xb4\xd5\x0ef\x02,\xa3\x10w+\xb2\xads_Q\xd0\b\x16\xcb\x15\x18Z\x15\xe1UUDLW[&\x85\xc3w6\xa57ڒ\xa0A\x86pc\xba\xa4-\x89\xfa\xb9-\xa3do\xe7f\x9f\xea\xe3\xeb\xfc\xa3\xf8\xfe3\x11=X\x9d\x13\x85}B\x930\xda/H\x9e\x0fQ\xd2#\xc5\x05\x98eguN\xd8\xf05\x85\xa1=\xff\xf1`+\xe5\x80(\xbf-ޝ6af\xb0nrN\xbd,\xe3\x9an\xfeA\xf8F&\xeb\xce[\xacY<\xfb\xd8myE\xbb\x02\x9e!\xf9\x15ۈ\xc2\x029US\x88\xb2\x19\x9c;'\x81R-0\xa3Mb\x9bm?4{G\t-\x06\xb4\x1a\x02p\x0ez\x88r\x88\a\t Y\xe3ZЦ\xa9\xd0P\xd2f,E\x92\xdd/\xe4\n\xbe\xfb\xe9}<\xf6\xec\x96DI=\x18T¤u\xe5\xdd\xc01\xea\xe2\xeaC\x95\xf0\v\xf9kM \xe86\xe1\xaf\x18g\x8f\xb0w.\x16\x97\f\xf9\xc6C\xe5D\x144PF\x00i\x8aG\xd8\x13\xa8\xf1-\xfe\xf12GZ\\y\x84\x91]\xbfX\xe9\xd1\x15\xf1\xf3{)\x8en\xf8\x81\b\x932\x9b\xda\xd2\x10\xd5O\x9f\x91\r\xf6x\x99\xa1\x97B\t|9q\xd8\xc9\xe2\xd4\xed\xab\x9f\x14\xf3\b\xfb\xdf\x19\xc7k\x9ce[A\x9bN\x9cVo\xd4f\x16\xc3]\xf9\xc2\v\x917\x9d\xb9y\xb5\x92W\xec'e\xf1\x9f\x0f_\x85\xc1\x8ee\xce\xde+0?)K_^\x94\xcan\x10\xafAc\xd7\x13MP\xe9,\t\x12\xb1\x9b<\xe2l)\nj\xc3\x0fa\xd8JbH\xe6H4\xa3;\xca\x15r]\xba\xce\xca\xda\xd0V\xabTr\xe1\x96\xc5\xc6z\xf3\xa7N\xd1Ə\x86\x1es\x0f\xf7DȻ\x96\xcav\x96qf:ѕ\xca\x7fg\xd8Fhc\xbbh\x98#\x89U\xa3\xa0N\b=\xe5\a\xadO\x8ems\x06\x1e\x99\x92F\xe4И~/\x02J2\xce6\\\x14\xb5\x9e\xa1Ug\x93|n\x10\xe6\xb5\xc9\xf9#\xabtD\x16D\xa2\xc4u\xf6\x19^\xf0\xb4Ư\xf4c\n\x86\vZ\xe3Unu\xf0d\xbfiz\r+=\x11wn\xfam\x93-9\xed\xbe?#\xe9\xf6\xacG\xa3\xbeYZ\xediɴ\xa9+\x94\t\x89\xb3\xe9\xe9\xb2)lu%=I69BNM\x88\x9d\xbb\x02\xf1\xa2ɯ/\x93\xf2\x9aL\xb3\xb4\xf4ֹ\x14{\x95T\xd6WN`}\xbd\xb4\xd5\x19ɪ\xe7?\xf5\x92\xbe\x96~rveڲ\xcc\xf1\x84Ӥ4Ӥ\xa5\x9b\x94\x01\x9f4Ԥ\xf4ѹI\xa3I\x9cL\x9f\xae\xaf\x9a\x16\xfa\xaaɠ\xaf\x9f\x02:)m\x93\x15\xe6&y\x8e_r\x18ʴ\x03P|\v\xe1|.\x99\x94\xee\xb9\xe6ϊ;?\r`\xa1\xb0\x047\xf5\x15〲.\xac\xa8\x8a\xf6>\xb6X\xc0\xb9\x85}sY\xd1/\x8a\x8e\xc8\xfb\x9b\xba>}n$~9\x88j\xb8aOP\x14\x8c\xc7\xe6\xe6\x01\x152w\x0fh\xa6\x16\x80\xb6\x11g\xb9\xbf\x8c\xc9_\x1ez\xe5\xa6\v\xdd\x06@\x16\xb6\x8c-\xf5qy\xfc\xa6\xaf\xa3\x06,U\x8f\x1dx\xe6.ޠo\xbf֠\xf7\x8c\xee\x1dk|\xb3\xf6P\xa9\x9f\xe8\x06\x03Ӡ~\xbc:<\xb6gr\x10\xe0\xb4ꁽ\x93\xce#\x18\xe2DmP\xef\xb4\x01\x1d*U\x8cӢ\xfdD@H\xd5@\x884Mq\xfe眲|\x89\xf0\xee\x1c\x01^\x92\a4\xcf{\xfd\x86\xa7'O=5\x99\x9e\x8c\x92tJ\xf2%½9\x01\xdf,\x7f5\xfd\x14\xe4\xfc\x8d\xe7\x17>\xf5\xf8R\xa7\x1dgP/\xf5t\xe3|ڽ\xd2i\xc6W?\xc5\xf8\x9a\xa7\x17g\x9dZLNϚ\x95q0'\xb5\xea\x19\xc7\xed\xd2r\t\xa6O!&\x9e>L\xcc4H\x1b\xfc\x89\xc3N<]8\xffTa\"\x7f\xe7L\xe9W>=\xf8ʧ\x06\xbf\xc5i\xc1\x04\tL\xa82\xffT\u0cf7\xa4\x94\xceAOn\xfb͑\xdaIyM\x8d\xe5\xfa\x88\r\xf6\xb5\xc2m\xb2X\xab\x17\x03\x90Y\xf2\x17\xf9ӣ\rǶ\xc1Q2;\x1eQo_\xb2u\xd7\xfa\x0e\xb1\x7f\xcd\xc1m]\x1a\xa88\x1a\x00\n\xdc(5+\xea*|\xe0\xd9v\xd0Ö\x1b\xb6Q\xba\xe4\x96]6\x9b\xc5o\\\a\xf8\xf7咱\x1fT\x93\xabӽ/͈\xb2*\xf6\x18\x89\xb1\xcbn\x83\xe7IIT:CϷ\xaa\x10Y\xc4\xe7\x1c\xbdW\xcf58\xb8l\x88n\xfe\xcb:\xd9\"\xb1\xc0\a\x9b\x8bp\xebb\xffJfw\x9f\xfb\x89k%\xbc\x12\xffMO*\x9da\xd5\xed\xdd\xed\x8a`\x051\xa2\xb7\x9a\x9a\x04ņ\xe5k@\x97\xa1\x1d\xfb1}\xb2\xda\xf4\xa0\xf6s\x84\xbb\x8fU@\xee^&\tn\x8bW͙B\xadu\xbbr\xb8\x1c\xeb\t\xe5\x8b\xcb=S\xfe\xe9\t\xa1\xf3Eŵݻd\xa2\xab\x1e\x1e\xc1\xaeO\xad\x9a\x1d\xb5V\x87/\xaftK\x8f\xec\xe1\xd1\x15\xda\xc9\xdeW\xfd\xe4\x81!=\x9f\x83\xd3\xf1SՓ\xe7\xa9_\x00\xa7\xe3.Ԃ\xa8\x18\xf9)\x9a\x01y\xf6\x15K\xe3o\xe8\xffQ\xed\xe0}t\xe5\xb2\xff\xfaʠ\xc9Hjb\x80J\x97\xccG(\xd8\xe6#\xd2\x1d\xdf\xcfS{\xf1\\À\x8a\xbf#\xfc9\x8b\x93w}P\xe3\x0f\x92\xd0\r\xea\xa1ӘWEO=\xed\xd9\xed\x17\x8a[\x1bUꧾ\x8f[\xc3\xf2dH0\x88\xc0\x12\xf2\xe8\x1b-\xe7\"\xa3U\x9a?\xc0G\xe5\xde\xd6I\x11\x93~\x8b\xde\xcbK\xdes\v\xf9\xda~\x12\xc6\x14\xbd\x1f\xdb\x10`{>\xe3\xe0\xa2\x7f\xc4\xf6ħ\f\xac-\x9e##\xf7\xf7\x1f\xddH\xe9I\x93\xf7\xfeu\x12\xd4\xc7\x06\x90\x05\x81\x02\x0e\xda\x1a\xff\xbbUOt\x01~|\x8d9< \xd2y\xc3\f\xe8\xa0\b\xa5\xf0\x9e4̺*\x14\xcfA\xdf\xd0#*\t#\xfe\xb9\xd7`\xe0\x0e\xf4\x9fb\xf1v32\x9e\xd0\xf3\vfɠGW\x14P\xfc \n0\x0e\xf1D\xd3p{ز\xb1\x14u\xb9v\x9e\xea\x06\x7fl:9b\x99\xddPi\x83\xa1\x02\x8d~\xa2ۊ\xa8M\x90\xfc\xe3\xc4`\r\x1f\x85\xb4\xf0\x00\xe31\xf4\x84M\xd8\xf5^b\t\xb3'E\x11~\x19o\xd9q\xa6;\xf3\x98\xbc߸\xba\x8b\xc1\xe2ƨL\x90\xff\xfd$\xac\xbf\xf8\xf0\xe5n\xdb>\x16J\x1d\xa1cm\xe0ӓ\x04\xfd9\xe8j\xb3\x92\xb1\x17N\xa6\xf5\xc4\xcf\aТ/\x9bX\x85}\x8f\xc0\x18\x00`*\xec\b\x19\xf7fN؈\x12\xa6y\x06쐞\x13\x93-n\x13\xc6]\x9b\xc5\xf8\xabE\x8b\xe6u\xa5\x8b\x04r\xbb\x97\x82\xfa\x80\xc7\x1f\x7fsO\ne\xbc\xb2\xb5\x0ez\xa8\xd6t\x1f9\x02\x01w]\xf7iϿ\xb5\xaf\x82\x9d\xc2\xe0\xf6Y\xaev\xa5~\xf2\xe1\xd0\x118\xcd\x03n\xd1נ\\\xec\xe9\x1e\xf6\\ \xfc\xd3x<:c\x10\xe7;\xf7\xca\xd7\x04\x11>\xb65\xc7\x06\xdc\f\x03\x87\xec\xdf\r{Ց\xd0\xf5\xf4\x13c\xb8\xc5:\xcdyP/G\xd40\\k\x7f\x17c\xc2\xf8\xa1\xc1\x05\xfb\t\x0ec\xdb\x05\xfb q\x10\x87\x04p'\x03!\xa7M\bҎs\x86\xb8kZѱ\xcc\x11\r9-\xb6_\x060\x069\xdf\xf4=\xc0\xfe\xf2\u05cb\xbf\x05\x00\x00\xff\xffq\xe5\x82\xc1hz\x00\x00"), diff --git a/internal/credentials/local.go b/internal/credentials/local.go new file mode 100644 index 000000000..00ccc3174 --- /dev/null +++ b/internal/credentials/local.go @@ -0,0 +1,7 @@ +package credentials + +import "os" + +func DefaultStoreDirectory() string { + return os.TempDir() + "/credentials" +} diff --git a/pkg/apis/velero/v1/pod_volume_backup_types.go b/pkg/apis/velero/v1/pod_volume_backup_types.go index b3070e3dd..ced436a82 100644 --- a/pkg/apis/velero/v1/pod_volume_backup_types.go +++ b/pkg/apis/velero/v1/pod_volume_backup_types.go @@ -57,6 +57,10 @@ type PodVolumeBackupSpec struct { // +optional // +nullable UploaderSettings map[string]string `json:"uploaderSettings,omitempty"` + + // Cancel indicates request to cancel the ongoing PodVolumeBackup. It can be set + // when the PodVolumeBackup is in InProgress phase + Cancel bool `json:"cancel,omitempty"` } // PodVolumeBackupPhase represents the lifecycle phase of a PodVolumeBackup. diff --git a/pkg/cmd/cli/datamover/backup.go b/pkg/cmd/cli/datamover/backup.go index 94fe613d1..2b647bb24 100644 --- a/pkg/cmd/cli/datamover/backup.go +++ b/pkg/cmd/cli/datamover/backup.go @@ -39,6 +39,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util/filesystem" + "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" ctrl "sigs.k8s.io/controller-runtime" @@ -78,7 +79,7 @@ func NewBackupCommand(f client.Factory) *cobra.Command { f.SetBasename(fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name())) s, err := newdataMoverBackup(logger, f, config) if err != nil { - exitWithMessage(logger, false, "Failed to create data mover backup, %v", err) + kube.ExitPodWithMessage(logger, false, "Failed to create data mover backup, %v", err) } s.run() @@ -100,12 +101,6 @@ func NewBackupCommand(f client.Factory) *cobra.Command { return command } -const ( - // defaultCredentialsDirectory is the path on disk where credential - // files will be written to - defaultCredentialsDirectory = "/tmp/credentials" -) - type dataMoverBackup struct { logger logrus.FieldLogger ctx context.Context @@ -215,7 +210,7 @@ func newdataMoverBackup(logger logrus.FieldLogger, factory client.Factory, confi return s, nil } -var funcExitWithMessage = exitWithMessage +var funcExitWithMessage = kube.ExitPodWithMessage var funcCreateDataPathService = (*dataMoverBackup).createDataPathService func (s *dataMoverBackup) run() { @@ -277,7 +272,7 @@ func (s *dataMoverBackup) createDataPathService() (dataPathService, error) { credentialFileStore, err := funcNewCredentialFileStore( s.client, s.namespace, - defaultCredentialsDirectory, + credentials.DefaultStoreDirectory(), filesystem.NewFileSystem(), ) if err != nil { diff --git a/pkg/cmd/cli/datamover/data_mover.go b/pkg/cmd/cli/datamover/data_mover.go index 6786f4e7c..265d57bd8 100644 --- a/pkg/cmd/cli/datamover/data_mover.go +++ b/pkg/cmd/cli/datamover/data_mover.go @@ -15,10 +15,7 @@ package datamover import ( "context" - "fmt" - "os" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/vmware-tanzu/velero/pkg/client" @@ -45,30 +42,3 @@ type dataPathService interface { RunCancelableDataPath(context.Context) (string, error) Shutdown() } - -var funcExit = os.Exit -var funcCreateFile = os.Create - -func exitWithMessage(logger logrus.FieldLogger, succeed bool, message string, a ...any) { - exitCode := 0 - if !succeed { - exitCode = 1 - } - - toWrite := fmt.Sprintf(message, a...) - - podFile, err := funcCreateFile("/dev/termination-log") - if err != nil { - logger.WithError(err).Error("Failed to create termination log file") - exitCode = 1 - } else { - if _, err := podFile.WriteString(toWrite); err != nil { - logger.WithError(err).Error("Failed to write error to termination log file") - exitCode = 1 - } - - podFile.Close() - } - - funcExit(exitCode) -} diff --git a/pkg/cmd/cli/datamover/data_mover_test.go b/pkg/cmd/cli/datamover/data_mover_test.go deleted file mode 100644 index 206ba36ee..000000000 --- a/pkg/cmd/cli/datamover/data_mover_test.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -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 datamover - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - velerotest "github.com/vmware-tanzu/velero/pkg/test" -) - -type exitWithMessageMock struct { - createErr error - writeFail bool - filePath string - exitCode int -} - -func (em *exitWithMessageMock) Exit(code int) { - em.exitCode = code -} - -func (em *exitWithMessageMock) CreateFile(name string) (*os.File, error) { - if em.createErr != nil { - return nil, em.createErr - } - - if em.writeFail { - return os.OpenFile(em.filePath, os.O_CREATE|os.O_RDONLY, 0500) - } else { - return os.Create(em.filePath) - } -} - -func TestExitWithMessage(t *testing.T) { - tests := []struct { - name string - message string - succeed bool - args []any - createErr error - writeFail bool - expectedExitCode int - expectedMessage string - }{ - { - name: "create pod file failed", - createErr: errors.New("fake-create-file-error"), - succeed: true, - expectedExitCode: 1, - }, - { - name: "write pod file failed", - writeFail: true, - succeed: true, - expectedExitCode: 1, - }, - { - name: "not succeed", - message: "fake-message-1, arg-1 %s, arg-2 %v, arg-3 %v", - args: []any{ - "arg-1-1", - 10, - false, - }, - expectedExitCode: 1, - expectedMessage: fmt.Sprintf("fake-message-1, arg-1 %s, arg-2 %v, arg-3 %v", "arg-1-1", 10, false), - }, - { - name: "not succeed", - message: "fake-message-2, arg-1 %s, arg-2 %v, arg-3 %v", - args: []any{ - "arg-1-2", - 20, - true, - }, - succeed: true, - expectedMessage: fmt.Sprintf("fake-message-2, arg-1 %s, arg-2 %v, arg-3 %v", "arg-1-2", 20, true), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - podFile := filepath.Join(os.TempDir(), uuid.NewString()) - - em := exitWithMessageMock{ - createErr: test.createErr, - writeFail: test.writeFail, - filePath: podFile, - } - - funcExit = em.Exit - funcCreateFile = em.CreateFile - - exitWithMessage(velerotest.NewLogger(), test.succeed, test.message, test.args...) - - assert.Equal(t, test.expectedExitCode, em.exitCode) - - if test.createErr == nil && !test.writeFail { - reader, err := os.Open(podFile) - require.NoError(t, err) - - message, err := io.ReadAll(reader) - require.NoError(t, err) - - reader.Close() - - assert.Equal(t, test.expectedMessage, string(message)) - } - }) - } -} diff --git a/pkg/cmd/cli/datamover/restore.go b/pkg/cmd/cli/datamover/restore.go index 65e4e0f5e..7e11df822 100644 --- a/pkg/cmd/cli/datamover/restore.go +++ b/pkg/cmd/cli/datamover/restore.go @@ -42,6 +42,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util/filesystem" + "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" ctlcache "sigs.k8s.io/controller-runtime/pkg/cache" @@ -76,7 +77,7 @@ func NewRestoreCommand(f client.Factory) *cobra.Command { f.SetBasename(fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name())) s, err := newdataMoverRestore(logger, f, config) if err != nil { - exitWithMessage(logger, false, "Failed to create data mover restore, %v", err) + kube.ExitPodWithMessage(logger, false, "Failed to create data mover restore, %v", err) } s.run() @@ -263,7 +264,7 @@ func (s *dataMoverRestore) createDataPathService() (dataPathService, error) { credentialFileStore, err := funcNewCredentialFileStore( s.client, s.namespace, - defaultCredentialsDirectory, + credentials.DefaultStoreDirectory(), filesystem.NewFileSystem(), ) if err != nil { diff --git a/pkg/cmd/cli/nodeagent/server.go b/pkg/cmd/cli/nodeagent/server.go index c7a2576c9..5bfec7c87 100644 --- a/pkg/cmd/cli/nodeagent/server.go +++ b/pkg/cmd/cli/nodeagent/server.go @@ -76,10 +76,6 @@ const ( // the port where prometheus metrics are exposed defaultMetricsAddress = ":8085" - // defaultCredentialsDirectory is the path on disk where credential - // files will be written to - defaultCredentialsDirectory = "/tmp/credentials" - defaultHostPodsPath = "/host_pods" defaultResourceTimeout = 10 * time.Minute @@ -291,7 +287,7 @@ func (s *nodeAgentServer) run() { credentialFileStore, err := credentials.NewNamespacedFileStore( s.mgr.GetClient(), s.namespace, - defaultCredentialsDirectory, + credentials.DefaultStoreDirectory(), filesystem.NewFileSystem(), ) if err != nil { diff --git a/pkg/cmd/cli/podvolume/backup.go b/pkg/cmd/cli/podvolume/backup.go new file mode 100644 index 000000000..537caa3cb --- /dev/null +++ b/pkg/cmd/cli/podvolume/backup.go @@ -0,0 +1,291 @@ +/* +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 podvolume + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/bombsimon/logrusr/v3" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + + "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/buildinfo" + "github.com/vmware-tanzu/velero/pkg/client" + "github.com/vmware-tanzu/velero/pkg/cmd/util/signals" + "github.com/vmware-tanzu/velero/pkg/datapath" + "github.com/vmware-tanzu/velero/pkg/podvolume" + "github.com/vmware-tanzu/velero/pkg/repository" + "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" + "github.com/vmware-tanzu/velero/pkg/util/kube" + "github.com/vmware-tanzu/velero/pkg/util/logging" + + ctrl "sigs.k8s.io/controller-runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + + ctlcache "sigs.k8s.io/controller-runtime/pkg/cache" + ctlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type podVolumeBackupConfig struct { + volumePath string + pvbName string + resourceTimeout time.Duration +} + +func NewBackupCommand(f client.Factory) *cobra.Command { + config := podVolumeBackupConfig{} + + logLevelFlag := logging.LogLevelFlag(logrus.InfoLevel) + formatFlag := logging.NewFormatFlag() + + command := &cobra.Command{ + Use: "backup", + Short: "Run the velero pod volume backup", + Long: "Run the velero pod volume backup", + Hidden: true, + Run: func(c *cobra.Command, args []string) { + logLevel := logLevelFlag.Parse() + logrus.Infof("Setting log-level to %s", strings.ToUpper(logLevel.String())) + + logger := logging.DefaultLogger(logLevel, formatFlag.Parse()) + logger.Infof("Starting Velero pod volume backup %s (%s)", buildinfo.Version, buildinfo.FormattedGitSHA()) + + f.SetBasename(fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name())) + s, err := newPodVolumeBackup(logger, f, config) + if err != nil { + kube.ExitPodWithMessage(logger, false, "Failed to create pod volume backup, %v", err) + } + + s.run() + }, + } + + command.Flags().Var(logLevelFlag, "log-level", fmt.Sprintf("The level at which to log. Valid values are %s.", strings.Join(logLevelFlag.AllowedValues(), ", "))) + command.Flags().Var(formatFlag, "log-format", fmt.Sprintf("The format for log output. Valid values are %s.", strings.Join(formatFlag.AllowedValues(), ", "))) + command.Flags().StringVar(&config.volumePath, "volume-path", config.volumePath, "The full path of the volume to be backed up") + command.Flags().StringVar(&config.pvbName, "pod-volume-backup", config.pvbName, "The PVB name") + command.Flags().DurationVar(&config.resourceTimeout, "resource-timeout", config.resourceTimeout, "How long to wait for resource processes which are not covered by other specific timeout parameters.") + + _ = command.MarkFlagRequired("volume-path") + _ = command.MarkFlagRequired("pod-volume-backup") + _ = command.MarkFlagRequired("resource-timeout") + + return command +} + +type podVolumeBackup struct { + logger logrus.FieldLogger + ctx context.Context + cancelFunc context.CancelFunc + client ctlclient.Client + cache ctlcache.Cache + namespace string + nodeName string + config podVolumeBackupConfig + kubeClient kubernetes.Interface + dataPathMgr *datapath.Manager +} + +func newPodVolumeBackup(logger logrus.FieldLogger, factory client.Factory, config podVolumeBackupConfig) (*podVolumeBackup, error) { + ctx, cancelFunc := context.WithCancel(context.Background()) + + clientConfig, err := factory.ClientConfig() + if err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to create client config") + } + + ctrl.SetLogger(logrusr.New(logger)) + klog.SetLogger(logrusr.New(logger)) // klog.Logger is used by k8s.io/client-go + + scheme := runtime.NewScheme() + if err := velerov1api.AddToScheme(scheme); err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to add velero v1 scheme") + } + + if err := v1.AddToScheme(scheme); err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to add core v1 scheme") + } + + nodeName := os.Getenv("NODE_NAME") + + // use a field selector to filter to only pods scheduled on this node. + cacheOption := ctlcache.Options{ + Scheme: scheme, + ByObject: map[ctlclient.Object]ctlcache.ByObject{ + &v1.Pod{}: { + Field: fields.Set{"spec.nodeName": nodeName}.AsSelector(), + }, + &velerov1api.PodVolumeBackup{}: { + Field: fields.Set{"metadata.namespace": factory.Namespace()}.AsSelector(), + }, + }, + } + + cli, err := ctlclient.New(clientConfig, ctlclient.Options{ + Scheme: scheme, + }) + if err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to create client") + } + + var cache ctlcache.Cache + retry := 10 + for { + cache, err = ctlcache.New(clientConfig, cacheOption) + if err == nil { + break + } + + retry-- + if retry == 0 { + break + } + + logger.WithError(err).Warn("Failed to create client cache, need retry") + + time.Sleep(time.Second) + } + + if err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to create client cache") + } + + s := &podVolumeBackup{ + logger: logger, + ctx: ctx, + cancelFunc: cancelFunc, + client: cli, + cache: cache, + config: config, + namespace: factory.Namespace(), + nodeName: nodeName, + } + + s.kubeClient, err = factory.KubeClient() + if err != nil { + cancelFunc() + return nil, errors.Wrap(err, "error to create kube client") + } + + s.dataPathMgr = datapath.NewManager(1) + + return s, nil +} + +var funcExitWithMessage = kube.ExitPodWithMessage +var funcCreateDataPathService = (*podVolumeBackup).createDataPathService + +func (s *podVolumeBackup) run() { + signals.CancelOnShutdown(s.cancelFunc, s.logger) + go func() { + if err := s.cache.Start(s.ctx); err != nil { + s.logger.WithError(err).Warn("error starting cache") + } + }() + + s.runDataPath() +} + +func (s *podVolumeBackup) runDataPath() { + s.logger.Infof("Starting micro service in node %s for PVB %s", s.nodeName, s.config.pvbName) + + dpService, err := funcCreateDataPathService(s) + if err != nil { + s.cancelFunc() + funcExitWithMessage(s.logger, false, "Failed to create data path service for PVB %s: %v", s.config.pvbName, err) + return + } + + s.logger.Infof("Starting data path service %s", s.config.pvbName) + + err = dpService.Init() + if err != nil { + dpService.Shutdown() + s.cancelFunc() + funcExitWithMessage(s.logger, false, "Failed to init data path service for PVB %s: %v", s.config.pvbName, err) + return + } + + s.logger.Infof("Running data path service %s", s.config.pvbName) + + result, err := dpService.RunCancelableDataPath(s.ctx) + if err != nil { + dpService.Shutdown() + s.cancelFunc() + funcExitWithMessage(s.logger, false, "Failed to run data path service for PVB %s: %v", s.config.pvbName, err) + return + } + + s.logger.WithField("PVB", s.config.pvbName).Info("Data path service completed") + + dpService.Shutdown() + + s.logger.WithField("PVB", s.config.pvbName).Info("Data path service is shut down") + + s.cancelFunc() + + funcExitWithMessage(s.logger, true, result) +} + +var funcNewCredentialFileStore = credentials.NewNamespacedFileStore +var funcNewCredentialSecretStore = credentials.NewNamespacedSecretStore + +func (s *podVolumeBackup) createDataPathService() (dataPathService, error) { + credentialFileStore, err := funcNewCredentialFileStore( + s.client, + s.namespace, + credentials.DefaultStoreDirectory(), + filesystem.NewFileSystem(), + ) + if err != nil { + return nil, errors.Wrapf(err, "error to create credential file store") + } + + credSecretStore, err := funcNewCredentialSecretStore(s.client, s.namespace) + if err != nil { + return nil, errors.Wrapf(err, "error to create credential secret store") + } + + credGetter := &credentials.CredentialGetter{FromFile: credentialFileStore, FromSecret: credSecretStore} + + pvbInformer, err := s.cache.GetInformer(s.ctx, &velerov1api.PodVolumeBackup{}) + if err != nil { + return nil, errors.Wrap(err, "error to get controller-runtime informer from manager") + } + + repoEnsurer := repository.NewEnsurer(s.client, s.logger, s.config.resourceTimeout) + + return podvolume.NewBackupMicroService(s.ctx, s.client, s.kubeClient, s.config.pvbName, s.namespace, s.nodeName, datapath.AccessPoint{ + ByPath: s.config.volumePath, + VolMode: uploader.PersistentVolumeFilesystem, + }, s.dataPathMgr, repoEnsurer, credGetter, pvbInformer, s.logger), nil +} diff --git a/pkg/cmd/cli/podvolume/backup_test.go b/pkg/cmd/cli/podvolume/backup_test.go new file mode 100644 index 000000000..1c59af751 --- /dev/null +++ b/pkg/cmd/cli/podvolume/backup_test.go @@ -0,0 +1,216 @@ +/* +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 podvolume + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + ctlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware-tanzu/velero/internal/credentials" + cacheMock "github.com/vmware-tanzu/velero/pkg/cmd/cli/datamover/mocks" + velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +func fakeCreateDataPathServiceWithErr(_ *podVolumeBackup) (dataPathService, error) { + return nil, errors.New("fake-create-data-path-error") +} + +var frHelper *fakeRunHelper + +func fakeCreateDataPathService(_ *podVolumeBackup) (dataPathService, error) { + return frHelper, nil +} + +type fakeRunHelper struct { + initErr error + runCancelableDataPathErr error + runCancelableDataPathResult string + exitMessage string + succeed bool +} + +func (fr *fakeRunHelper) Init() error { + return fr.initErr +} + +func (fr *fakeRunHelper) RunCancelableDataPath(_ context.Context) (string, error) { + if fr.runCancelableDataPathErr != nil { + return "", fr.runCancelableDataPathErr + } else { + return fr.runCancelableDataPathResult, nil + } +} + +func (fr *fakeRunHelper) Shutdown() { + +} + +func (fr *fakeRunHelper) ExitWithMessage(logger logrus.FieldLogger, succeed bool, message string, a ...any) { + fr.succeed = succeed + fr.exitMessage = fmt.Sprintf(message, a...) +} + +func TestRunDataPath(t *testing.T) { + tests := []struct { + name string + pvbName string + createDataPathFail bool + initDataPathErr error + runCancelableDataPathErr error + runCancelableDataPathResult string + expectedMessage string + expectedSucceed bool + }{ + { + name: "create data path failed", + pvbName: "fake-name", + createDataPathFail: true, + expectedMessage: "Failed to create data path service for PVB fake-name: fake-create-data-path-error", + }, + { + name: "init data path failed", + pvbName: "fake-name", + initDataPathErr: errors.New("fake-init-data-path-error"), + expectedMessage: "Failed to init data path service for PVB fake-name: fake-init-data-path-error", + }, + { + name: "run data path failed", + pvbName: "fake-name", + runCancelableDataPathErr: errors.New("fake-run-data-path-error"), + expectedMessage: "Failed to run data path service for PVB fake-name: fake-run-data-path-error", + }, + { + name: "succeed", + pvbName: "fake-name", + runCancelableDataPathResult: "fake-run-data-path-result", + expectedMessage: "fake-run-data-path-result", + expectedSucceed: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + frHelper = &fakeRunHelper{ + initErr: test.initDataPathErr, + runCancelableDataPathErr: test.runCancelableDataPathErr, + runCancelableDataPathResult: test.runCancelableDataPathResult, + } + + if test.createDataPathFail { + funcCreateDataPathService = fakeCreateDataPathServiceWithErr + } else { + funcCreateDataPathService = fakeCreateDataPathService + } + + funcExitWithMessage = frHelper.ExitWithMessage + + s := &podVolumeBackup{ + logger: velerotest.NewLogger(), + cancelFunc: func() {}, + config: podVolumeBackupConfig{ + pvbName: test.pvbName, + }, + } + + s.runDataPath() + + assert.Equal(t, test.expectedMessage, frHelper.exitMessage) + assert.Equal(t, test.expectedSucceed, frHelper.succeed) + }) + } +} + +type fakeCreateDataPathServiceHelper struct { + fileStoreErr error + secretStoreErr error +} + +func (fc *fakeCreateDataPathServiceHelper) NewNamespacedFileStore(_ ctlclient.Client, _ string, _ string, _ filesystem.Interface) (credentials.FileStore, error) { + return nil, fc.fileStoreErr +} + +func (fc *fakeCreateDataPathServiceHelper) NewNamespacedSecretStore(_ ctlclient.Client, _ string) (credentials.SecretStore, error) { + return nil, fc.secretStoreErr +} + +func TestCreateDataPathService(t *testing.T) { + tests := []struct { + name string + fileStoreErr error + secretStoreErr error + mockGetInformer bool + getInformerErr error + expectedError string + }{ + { + name: "create credential file store error", + fileStoreErr: errors.New("fake-file-store-error"), + expectedError: "error to create credential file store: fake-file-store-error", + }, + { + name: "create credential secret store", + secretStoreErr: errors.New("fake-secret-store-error"), + expectedError: "error to create credential secret store: fake-secret-store-error", + }, + { + name: "get informer error", + mockGetInformer: true, + getInformerErr: errors.New("fake-get-informer-error"), + expectedError: "error to get controller-runtime informer from manager: fake-get-informer-error", + }, + { + name: "succeed", + mockGetInformer: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fcHelper := &fakeCreateDataPathServiceHelper{ + fileStoreErr: test.fileStoreErr, + secretStoreErr: test.secretStoreErr, + } + + funcNewCredentialFileStore = fcHelper.NewNamespacedFileStore + funcNewCredentialSecretStore = fcHelper.NewNamespacedSecretStore + + cache := cacheMock.NewCache(t) + if test.mockGetInformer { + cache.On("GetInformer", mock.Anything, mock.Anything).Return(nil, test.getInformerErr) + } + + funcExitWithMessage = frHelper.ExitWithMessage + + s := &podVolumeBackup{ + cache: cache, + } + + _, err := s.createDataPathService() + + if test.expectedError != "" { + assert.EqualError(t, err, test.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/cmd/cli/podvolume/podvolume.go b/pkg/cmd/cli/podvolume/podvolume.go new file mode 100644 index 000000000..e7f347533 --- /dev/null +++ b/pkg/cmd/cli/podvolume/podvolume.go @@ -0,0 +1,43 @@ +/* +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 podvolume + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/vmware-tanzu/velero/pkg/client" +) + +func NewCommand(f client.Factory) *cobra.Command { + command := &cobra.Command{ + Use: "pod-volume", + Short: "Run the velero pod volume backup/restore", + Long: "Run the velero pod volume backup/restore", + Hidden: true, + } + + command.AddCommand( + NewBackupCommand(f), + ) + + return command +} + +type dataPathService interface { + Init() error + RunCancelableDataPath(context.Context) (string, error) + Shutdown() +} diff --git a/pkg/cmd/cli/repomantenance/maintenance.go b/pkg/cmd/cli/repomantenance/maintenance.go index 93ebebff3..46c54f7d2 100644 --- a/pkg/cmd/cli/repomantenance/maintenance.go +++ b/pkg/cmd/cli/repomantenance/maintenance.go @@ -121,7 +121,7 @@ func initRepoManager(namespace string, cli client.Client, kubeClient kubernetes. credentialFileStore, err := credentials.NewNamespacedFileStore( cli, namespace, - "/tmp/credentials", + credentials.DefaultStoreDirectory(), filesystem.NewFileSystem(), ) if err != nil { diff --git a/pkg/cmd/server/config/config.go b/pkg/cmd/server/config/config.go index 33a40a3c4..29c53b821 100644 --- a/pkg/cmd/server/config/config.go +++ b/pkg/cmd/server/config/config.go @@ -8,6 +8,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/pflag" + "github.com/vmware-tanzu/velero/internal/credentials" "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/constant" podvolumeconfigs "github.com/vmware-tanzu/velero/pkg/podvolume/configs" @@ -44,10 +45,6 @@ const ( defaultMaxConcurrentK8SConnections = 30 defaultDisableInformerCache = false - // defaultCredentialsDirectory is the path on disk where credential - // files will be written to - defaultCredentialsDirectory = "/tmp/credentials" - DefaultKeepLatestMaintenanceJobs = 3 DefaultMaintenanceJobCPURequest = "0" DefaultMaintenanceJobCPULimit = "0" @@ -211,7 +208,7 @@ func GetDefaultConfig() *Config { DefaultSnapshotMoveData: false, DisableInformerCache: defaultDisableInformerCache, ScheduleSkipImmediately: false, - CredentialsDirectory: defaultCredentialsDirectory, + CredentialsDirectory: credentials.DefaultStoreDirectory(), PodResources: kube.PodResources{ CPURequest: DefaultMaintenanceJobCPULimit, CPULimit: DefaultMaintenanceJobCPURequest, diff --git a/pkg/cmd/velero/velero.go b/pkg/cmd/velero/velero.go index eab96d62f..a74c68fbc 100644 --- a/pkg/cmd/velero/velero.go +++ b/pkg/cmd/velero/velero.go @@ -26,6 +26,7 @@ import ( "k8s.io/klog/v2" "github.com/vmware-tanzu/velero/pkg/cmd/cli/debug" + "github.com/vmware-tanzu/velero/pkg/cmd/cli/podvolume" "github.com/vmware-tanzu/velero/pkg/cmd/cli/repomantenance" "github.com/vmware-tanzu/velero/pkg/client" @@ -126,6 +127,7 @@ operations can also be performed as 'velero backup get' and 'velero schedule cre debug.NewCommand(f), repomantenance.NewCommand(f), datamover.NewCommand(f), + podvolume.NewCommand(f), ) // init and add the klog flags diff --git a/pkg/podvolume/backup_micro_service.go b/pkg/podvolume/backup_micro_service.go new file mode 100644 index 000000000..275b57fb9 --- /dev/null +++ b/pkg/podvolume/backup_micro_service.go @@ -0,0 +1,313 @@ +/* +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 podvolume + +import ( + "context" + "encoding/json" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + + cachetool "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/cache" + + "github.com/vmware-tanzu/velero/internal/credentials" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/datapath" + "github.com/vmware-tanzu/velero/pkg/repository" + "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/util/kube" + + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +const ( + podVolumeRequestor = "snapshot-pod-volume" +) + +// BackupMicroService process data mover backups inside the backup pod +type BackupMicroService struct { + ctx context.Context + client client.Client + kubeClient kubernetes.Interface + repoEnsurer *repository.Ensurer + credentialGetter *credentials.CredentialGetter + logger logrus.FieldLogger + dataPathMgr *datapath.Manager + eventRecorder kube.EventRecorder + + namespace string + pvbName string + pvb *velerov1api.PodVolumeBackup + sourceTargetPath datapath.AccessPoint + + resultSignal chan dataPathResult + + pvbInformer cache.Informer + pvbHandler cachetool.ResourceEventHandlerRegistration + nodeName string +} + +type dataPathResult struct { + err error + result string +} + +func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient kubernetes.Interface, pvbName string, namespace string, nodeName string, + sourceTargetPath datapath.AccessPoint, dataPathMgr *datapath.Manager, repoEnsurer *repository.Ensurer, cred *credentials.CredentialGetter, + pvbInformer cache.Informer, log logrus.FieldLogger) *BackupMicroService { + return &BackupMicroService{ + ctx: ctx, + client: client, + kubeClient: kubeClient, + credentialGetter: cred, + logger: log, + repoEnsurer: repoEnsurer, + dataPathMgr: dataPathMgr, + namespace: namespace, + pvbName: pvbName, + sourceTargetPath: sourceTargetPath, + nodeName: nodeName, + resultSignal: make(chan dataPathResult), + pvbInformer: pvbInformer, + } +} + +func (r *BackupMicroService) Init() error { + r.eventRecorder = kube.NewEventRecorder(r.kubeClient, r.client.Scheme(), r.pvbName, r.nodeName, r.logger) + + handler, err := r.pvbInformer.AddEventHandler( + cachetool.ResourceEventHandlerFuncs{ + UpdateFunc: func(oldObj any, newObj any) { + oldPvb := oldObj.(*velerov1api.PodVolumeBackup) + newPvb := newObj.(*velerov1api.PodVolumeBackup) + + if newPvb.Name != r.pvbName { + return + } + + if newPvb.Status.Phase != velerov1api.PodVolumeBackupPhaseInProgress { + return + } + + if newPvb.Spec.Cancel && !oldPvb.Spec.Cancel { + r.cancelPodVolumeBackup(newPvb) + } + }, + }, + ) + + if err != nil { + return errors.Wrap(err, "error adding PVB handler") + } + + r.pvbHandler = handler + + return err +} + +func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, error) { + log := r.logger.WithFields(logrus.Fields{ + "PVB": r.pvbName, + }) + + pvb := &velerov1api.PodVolumeBackup{} + err := wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) { + err := r.client.Get(ctx, types.NamespacedName{ + Namespace: r.namespace, + Name: r.pvbName, + }, pvb) + + if apierrors.IsNotFound(err) { + return false, nil + } + + if err != nil { + return true, errors.Wrapf(err, "error to get PVB %s", r.pvbName) + } + + if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseInProgress { + return true, nil + } else { + return false, nil + } + }) + + if err != nil { + log.WithError(err).Error("Failed to wait PVB") + return "", errors.Wrap(err, "error waiting for PVB") + } + + r.pvb = pvb + + log.Info("Run cancelable PVB") + + callbacks := datapath.Callbacks{ + OnCompleted: r.OnDataPathCompleted, + OnFailed: r.OnDataPathFailed, + OnCancelled: r.OnDataPathCancelled, + OnProgress: r.OnDataPathProgress, + } + + fsBackup, err := r.dataPathMgr.CreateFileSystemBR(pvb.Name, podVolumeRequestor, ctx, r.client, pvb.Namespace, callbacks, log) + if err != nil { + return "", errors.Wrap(err, "error to create data path") + } + + log.Debug("Async fs br created") + + if err := fsBackup.Init(ctx, &datapath.FSBRInitParam{ + BSLName: pvb.Spec.BackupStorageLocation, + SourceNamespace: pvb.Spec.Pod.Namespace, + UploaderType: pvb.Spec.UploaderType, + RepositoryType: velerov1api.BackupRepositoryTypeKopia, + RepoIdentifier: "", + RepositoryEnsurer: r.repoEnsurer, + CredentialGetter: r.credentialGetter, + }); err != nil { + return "", errors.Wrap(err, "error to initialize data path") + } + + log.Info("Async fs br init") + + tags := map[string]string{} + + if err := fsBackup.StartBackup(r.sourceTargetPath, pvb.Spec.UploaderSettings, &datapath.FSBRStartParam{ + RealSource: GetRealSource(pvb), + ParentSnapshot: "", + ForceFull: false, + Tags: tags, + }); err != nil { + return "", errors.Wrap(err, "error starting data path backup") + } + + log.Info("Async fs backup data path started") + r.eventRecorder.Event(pvb, false, datapath.EventReasonStarted, "Data path for %s started", pvb.Name) + + result := "" + select { + case <-ctx.Done(): + err = errors.New("timed out waiting for fs backup to complete") + break + case res := <-r.resultSignal: + err = res.err + result = res.result + break + } + + if err != nil { + log.WithError(err).Error("Async fs backup was not completed") + } + + r.eventRecorder.EndingEvent(pvb, false, datapath.EventReasonStopped, "Data path for %s stopped", pvb.Name) + + return result, err +} + +func (r *BackupMicroService) Shutdown() { + r.eventRecorder.Shutdown() + r.closeDataPath(r.ctx, r.pvbName) + + if r.pvbHandler != nil { + if err := r.pvbInformer.RemoveEventHandler(r.pvbHandler); err != nil { + r.logger.WithError(err).Warn("Failed to remove pod handler") + } + } +} + +var funcMarshal = json.Marshal + +func (r *BackupMicroService) OnDataPathCompleted(ctx context.Context, namespace string, pvbName string, result datapath.Result) { + log := r.logger.WithField("PVB", pvbName) + + backupBytes, err := funcMarshal(result.Backup) + if err != nil { + log.WithError(err).Errorf("Failed to marshal backup result %v", result.Backup) + r.resultSignal <- dataPathResult{ + err: errors.Wrapf(err, "Failed to marshal backup result %v", result.Backup), + } + } else { + r.eventRecorder.Event(r.pvb, false, datapath.EventReasonCompleted, string(backupBytes)) + r.resultSignal <- dataPathResult{ + result: string(backupBytes), + } + } + + log.Info("Async fs backup completed") +} + +func (r *BackupMicroService) OnDataPathFailed(ctx context.Context, namespace string, pvbName string, err error) { + log := r.logger.WithField("PVB", pvbName) + log.WithError(err).Error("Async fs backup data path failed") + + r.eventRecorder.Event(r.pvb, false, datapath.EventReasonFailed, "Data path for PVB %s failed, error %v", r.pvbName, err) + r.resultSignal <- dataPathResult{ + err: errors.Wrapf(err, "Data path for PVB %s failed", r.pvbName), + } +} + +func (r *BackupMicroService) OnDataPathCancelled(ctx context.Context, namespace string, pvbName string) { + log := r.logger.WithField("PVB", pvbName) + log.Warn("Async fs backup data path canceled") + + r.eventRecorder.Event(r.pvb, false, datapath.EventReasonCancelled, "Data path for PVB %s canceled", pvbName) + r.resultSignal <- dataPathResult{ + err: errors.New(datapath.ErrCancelled), + } +} + +func (r *BackupMicroService) OnDataPathProgress(ctx context.Context, namespace string, pvbName string, progress *uploader.Progress) { + log := r.logger.WithFields(logrus.Fields{ + "PVB": pvbName, + }) + + progressBytes, err := funcMarshal(progress) + if err != nil { + log.WithError(err).Errorf("Failed to marshal progress %v", progress) + return + } + + r.eventRecorder.Event(r.pvb, false, datapath.EventReasonProgress, string(progressBytes)) +} + +func (r *BackupMicroService) closeDataPath(ctx context.Context, duName string) { + fsBackup := r.dataPathMgr.GetAsyncBR(duName) + if fsBackup != nil { + fsBackup.Close(ctx) + } + + r.dataPathMgr.RemoveAsyncBR(duName) +} + +func (r *BackupMicroService) cancelPodVolumeBackup(pvb *velerov1api.PodVolumeBackup) { + r.logger.WithField("pvb", pvb.Name).Info("PVB is being canceled") + + r.eventRecorder.Event(pvb, false, datapath.EventReasonCancelling, "Canceling for PVB %s", pvb.Name) + + fsBackup := r.dataPathMgr.GetAsyncBR(pvb.Name) + if fsBackup == nil { + r.OnDataPathCancelled(r.ctx, pvb.GetNamespace(), pvb.GetName()) + } else { + fsBackup.Cancel() + } +} diff --git a/pkg/podvolume/backup_micro_service_test.go b/pkg/podvolume/backup_micro_service_test.go new file mode 100644 index 000000000..eb3a484d1 --- /dev/null +++ b/pkg/podvolume/backup_micro_service_test.go @@ -0,0 +1,447 @@ +/* +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 podvolume + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/datapath" + "github.com/vmware-tanzu/velero/pkg/uploader" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + + clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + velerotest "github.com/vmware-tanzu/velero/pkg/test" + + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + + datapathmockes "github.com/vmware-tanzu/velero/pkg/datapath/mocks" +) + +type backupMsTestHelper struct { + eventReason string + eventMsg string + marshalErr error + marshalBytes []byte + withEvent bool + eventLock sync.Mutex +} + +func (bt *backupMsTestHelper) Event(_ runtime.Object, _ bool, reason string, message string, a ...any) { + bt.eventLock.Lock() + defer bt.eventLock.Unlock() + + bt.withEvent = true + bt.eventReason = reason + bt.eventMsg = fmt.Sprintf(message, a...) +} + +func (bt *backupMsTestHelper) EndingEvent(_ runtime.Object, _ bool, reason string, message string, a ...any) { + bt.eventLock.Lock() + defer bt.eventLock.Unlock() + + bt.withEvent = true + bt.eventReason = reason + bt.eventMsg = fmt.Sprintf(message, a...) +} +func (bt *backupMsTestHelper) Shutdown() {} + +func (bt *backupMsTestHelper) Marshal(v any) ([]byte, error) { + if bt.marshalErr != nil { + return nil, bt.marshalErr + } + + return bt.marshalBytes, nil +} + +func (bt *backupMsTestHelper) EventReason() string { + bt.eventLock.Lock() + defer bt.eventLock.Unlock() + + return bt.eventReason +} + +func (bt *backupMsTestHelper) EventMessage() string { + bt.eventLock.Lock() + defer bt.eventLock.Unlock() + + return bt.eventMsg +} + +func TestOnDataPathFailed(t *testing.T) { + pvbName := "fake-pvb" + bt := &backupMsTestHelper{} + + bs := &BackupMicroService{ + pvbName: pvbName, + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + resultSignal: make(chan dataPathResult), + logger: velerotest.NewLogger(), + } + + expectedErr := "Data path for PVB fake-pvb failed: fake-error" + expectedEventReason := datapath.EventReasonFailed + expectedEventMsg := "Data path for PVB fake-pvb failed, error fake-error" + + go bs.OnDataPathFailed(context.TODO(), velerov1api.DefaultNamespace, pvbName, errors.New("fake-error")) + + result := <-bs.resultSignal + assert.EqualError(t, result.err, expectedErr) + assert.Equal(t, expectedEventReason, bt.EventReason()) + assert.Equal(t, expectedEventMsg, bt.EventMessage()) +} + +func TestOnDataPathCancelled(t *testing.T) { + pvbName := "fake-pvb" + bt := &backupMsTestHelper{} + + bs := &BackupMicroService{ + pvbName: pvbName, + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + resultSignal: make(chan dataPathResult), + logger: velerotest.NewLogger(), + } + + expectedErr := datapath.ErrCancelled + expectedEventReason := datapath.EventReasonCancelled + expectedEventMsg := "Data path for PVB fake-pvb canceled" + + go bs.OnDataPathCancelled(context.TODO(), velerov1api.DefaultNamespace, pvbName) + + result := <-bs.resultSignal + assert.EqualError(t, result.err, expectedErr) + assert.Equal(t, expectedEventReason, bt.EventReason()) + assert.Equal(t, expectedEventMsg, bt.EventMessage()) +} + +func TestOnDataPathCompleted(t *testing.T) { + tests := []struct { + name string + expectedErr string + expectedEventReason string + expectedEventMsg string + marshalErr error + marshallStr string + }{ + { + name: "marshal fail", + marshalErr: errors.New("fake-marshal-error"), + expectedErr: "Failed to marshal backup result { false { } 0}: fake-marshal-error", + }, + { + name: "succeed", + marshallStr: "fake-complete-string", + expectedEventReason: datapath.EventReasonCompleted, + expectedEventMsg: "fake-complete-string", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pvbName := "fake-pvb" + + bt := &backupMsTestHelper{ + marshalErr: test.marshalErr, + marshalBytes: []byte(test.marshallStr), + } + + bs := &BackupMicroService{ + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + resultSignal: make(chan dataPathResult), + logger: velerotest.NewLogger(), + } + + funcMarshal = bt.Marshal + + go bs.OnDataPathCompleted(context.TODO(), velerov1api.DefaultNamespace, pvbName, datapath.Result{}) + + result := <-bs.resultSignal + if test.marshalErr != nil { + assert.EqualError(t, result.err, test.expectedErr) + } else { + assert.NoError(t, result.err) + assert.Equal(t, test.expectedEventReason, bt.EventReason()) + assert.Equal(t, test.expectedEventMsg, bt.EventMessage()) + } + }) + } +} + +func TestOnDataPathProgress(t *testing.T) { + tests := []struct { + name string + expectedErr string + expectedEventReason string + expectedEventMsg string + marshalErr error + marshallStr string + }{ + { + name: "marshal fail", + marshalErr: errors.New("fake-marshal-error"), + expectedErr: "Failed to marshal backup result", + }, + { + name: "succeed", + marshallStr: "fake-progress-string", + expectedEventReason: datapath.EventReasonProgress, + expectedEventMsg: "fake-progress-string", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pvbName := "fake-pvb" + + bt := &backupMsTestHelper{ + marshalErr: test.marshalErr, + marshalBytes: []byte(test.marshallStr), + } + + bs := &BackupMicroService{ + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + logger: velerotest.NewLogger(), + } + + funcMarshal = bt.Marshal + + bs.OnDataPathProgress(context.TODO(), velerov1api.DefaultNamespace, pvbName, &uploader.Progress{}) + + if test.marshalErr != nil { + assert.False(t, bt.withEvent) + } else { + assert.True(t, bt.withEvent) + assert.Equal(t, test.expectedEventReason, bt.EventReason()) + assert.Equal(t, test.expectedEventMsg, bt.EventMessage()) + } + }) + } +} + +func TestCancelPodVolumeBackup(t *testing.T) { + tests := []struct { + name string + expectedEventReason string + expectedEventMsg string + expectedErr string + }{ + { + name: "no fs backup", + expectedEventReason: datapath.EventReasonCancelled, + expectedEventMsg: "Data path for PVB fake-pvb canceled", + expectedErr: datapath.ErrCancelled, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pvbName := "fake-pvb" + pvb := builder.ForPodVolumeBackup(velerov1api.DefaultNamespace, pvbName).Result() + + bt := &backupMsTestHelper{} + + bs := &BackupMicroService{ + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + resultSignal: make(chan dataPathResult), + logger: velerotest.NewLogger(), + } + + go bs.cancelPodVolumeBackup(pvb) + + result := <-bs.resultSignal + + assert.EqualError(t, result.err, test.expectedErr) + assert.True(t, bt.withEvent) + assert.Equal(t, test.expectedEventReason, bt.EventReason()) + assert.Equal(t, test.expectedEventMsg, bt.EventMessage()) + }) + } +} + +func TestRunCancelableDataPath(t *testing.T) { + pvbName := "fake-pvb" + pvb := builder.ForPodVolumeBackup(velerov1api.DefaultNamespace, pvbName).Phase(velerov1api.PodVolumeBackupPhaseNew).Result() + pvbInProgress := builder.ForPodVolumeBackup(velerov1api.DefaultNamespace, pvbName).Phase(velerov1api.PodVolumeBackupPhaseInProgress).Result() + ctxTimeout, cancel := context.WithTimeout(context.Background(), time.Second) + + tests := []struct { + name string + ctx context.Context + result *dataPathResult + dataPathMgr *datapath.Manager + kubeClientObj []runtime.Object + initErr error + startErr error + dataPathStarted bool + expectedEventMsg string + expectedErr string + }{ + { + name: "no pvb", + ctx: ctxTimeout, + expectedErr: "error waiting for PVB: context deadline exceeded", + }, + { + name: "pvb not in in-progress", + ctx: ctxTimeout, + kubeClientObj: []runtime.Object{pvb}, + expectedErr: "error waiting for PVB: context deadline exceeded", + }, + { + name: "create data path fail", + ctx: context.Background(), + kubeClientObj: []runtime.Object{pvbInProgress}, + dataPathMgr: datapath.NewManager(0), + expectedErr: "error to create data path: Concurrent number exceeds", + }, + { + name: "init data path fail", + ctx: context.Background(), + kubeClientObj: []runtime.Object{pvbInProgress}, + initErr: errors.New("fake-init-error"), + expectedErr: "error to initialize data path: fake-init-error", + }, + { + name: "start data path fail", + ctx: context.Background(), + kubeClientObj: []runtime.Object{pvbInProgress}, + startErr: errors.New("fake-start-error"), + expectedErr: "error starting data path backup: fake-start-error", + }, + { + name: "data path timeout", + ctx: ctxTimeout, + kubeClientObj: []runtime.Object{pvbInProgress}, + dataPathStarted: true, + expectedEventMsg: fmt.Sprintf("Data path for %s stopped", pvbName), + expectedErr: "timed out waiting for fs backup to complete", + }, + { + name: "data path returns error", + ctx: context.Background(), + kubeClientObj: []runtime.Object{pvbInProgress}, + dataPathStarted: true, + result: &dataPathResult{ + err: errors.New("fake-data-path-error"), + }, + expectedEventMsg: fmt.Sprintf("Data path for %s stopped", pvbName), + expectedErr: "fake-data-path-error", + }, + { + name: "succeed", + ctx: context.Background(), + kubeClientObj: []runtime.Object{pvbInProgress}, + dataPathStarted: true, + result: &dataPathResult{ + result: "fake-succeed-result", + }, + expectedEventMsg: fmt.Sprintf("Data path for %s stopped", pvbName), + }, + } + + scheme := runtime.NewScheme() + velerov1api.AddToScheme(scheme) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClientBuilder := clientFake.NewClientBuilder() + fakeClientBuilder = fakeClientBuilder.WithScheme(scheme) + + fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build() + + bt := &backupMsTestHelper{} + + bs := &BackupMicroService{ + namespace: velerov1api.DefaultNamespace, + pvbName: pvbName, + ctx: context.Background(), + client: fakeClient, + dataPathMgr: datapath.NewManager(1), + eventRecorder: bt, + resultSignal: make(chan dataPathResult), + logger: velerotest.NewLogger(), + } + + if test.ctx != nil { + bs.ctx = test.ctx + } + + if test.dataPathMgr != nil { + bs.dataPathMgr = test.dataPathMgr + } + + datapath.FSBRCreator = func(string, string, kbclient.Client, string, datapath.Callbacks, logrus.FieldLogger) datapath.AsyncBR { + fsBR := datapathmockes.NewAsyncBR(t) + if test.initErr != nil { + fsBR.On("Init", mock.Anything, mock.Anything).Return(test.initErr) + } + + if test.startErr != nil { + fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartBackup", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) + } + + if test.dataPathStarted { + fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartBackup", mock.Anything, mock.Anything, mock.Anything).Return(nil) + } + + return fsBR + } + + if test.result != nil { + go func() { + time.Sleep(time.Millisecond * 500) + bs.resultSignal <- *test.result + }() + } + + result, err := bs.RunCancelableDataPath(test.ctx) + + if test.expectedErr != "" { + assert.EqualError(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, test.result.result, result) + } + + if test.expectedEventMsg != "" { + assert.True(t, bt.withEvent) + assert.Equal(t, test.expectedEventMsg, bt.EventMessage()) + } + }) + } + + cancel() +} diff --git a/pkg/podvolume/util.go b/pkg/podvolume/util.go index e9bb3075c..dab291768 100644 --- a/pkg/podvolume/util.go +++ b/pkg/podvolume/util.go @@ -17,12 +17,14 @@ limitations under the License. package podvolume import ( + "fmt" "strings" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/podvolume/configs" repotypes "github.com/vmware-tanzu/velero/pkg/repository/types" "github.com/vmware-tanzu/velero/pkg/uploader" ) @@ -143,6 +145,19 @@ func GetSnapshotIdentifier(podVolumeBackups *velerov1api.PodVolumeBackupList) ma return res } +func GetRealSource(pvb *velerov1api.PodVolumeBackup) string { + pvcName := "" + if pvb.Annotations != nil { + pvcName = pvb.Annotations[configs.PVCNameAnnotation] + } + + if pvcName != "" { + return fmt.Sprintf("%s/%s/%s", pvb.Spec.Pod.Namespace, pvb.Spec.Pod.Name, pvcName) + } else { + return fmt.Sprintf("%s/%s/%s", pvb.Spec.Pod.Namespace, pvb.Spec.Pod.Name, pvb.Spec.Volume) + } +} + func getUploaderTypeOrDefault(uploaderType string) string { if uploaderType != "" { return uploaderType diff --git a/pkg/util/kube/pod.go b/pkg/util/kube/pod.go index 04457f0d4..ba506dae1 100644 --- a/pkg/util/kube/pod.go +++ b/pkg/util/kube/pod.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "io" + "os" "time" "github.com/pkg/errors" @@ -273,3 +274,30 @@ func DiagnosePod(pod *corev1api.Pod) string { return diag } + +var funcExit = os.Exit +var funcCreateFile = os.Create + +func ExitPodWithMessage(logger logrus.FieldLogger, succeed bool, message string, a ...any) { + exitCode := 0 + if !succeed { + exitCode = 1 + } + + toWrite := fmt.Sprintf(message, a...) + + podFile, err := funcCreateFile("/dev/termination-log") + if err != nil { + logger.WithError(err).Error("Failed to create termination log file") + exitCode = 1 + } else { + if _, err := podFile.WriteString(toWrite); err != nil { + logger.WithError(err).Error("Failed to write error to termination log file") + exitCode = 1 + } + + podFile.Close() + } + + funcExit(exitCode) +} diff --git a/pkg/util/kube/pod_test.go b/pkg/util/kube/pod_test.go index 2c19091f3..c1f8ba912 100644 --- a/pkg/util/kube/pod_test.go +++ b/pkg/util/kube/pod_test.go @@ -18,14 +18,19 @@ package kube import ( "context" + "fmt" "io" + "os" + "path/filepath" "reflect" "strings" "testing" "time" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -932,3 +937,105 @@ func TestDiagnosePod(t *testing.T) { }) } } + +type exitWithMessageMock struct { + createErr error + writeFail bool + filePath string + exitCode int +} + +func (em *exitWithMessageMock) Exit(code int) { + em.exitCode = code +} + +func (em *exitWithMessageMock) CreateFile(name string) (*os.File, error) { + if em.createErr != nil { + return nil, em.createErr + } + + if em.writeFail { + return os.OpenFile(em.filePath, os.O_CREATE|os.O_RDONLY, 0500) + } else { + return os.Create(em.filePath) + } +} + +func TestExitPodWithMessage(t *testing.T) { + tests := []struct { + name string + message string + succeed bool + args []any + createErr error + writeFail bool + expectedExitCode int + expectedMessage string + }{ + { + name: "create pod file failed", + createErr: errors.New("fake-create-file-error"), + succeed: true, + expectedExitCode: 1, + }, + { + name: "write pod file failed", + writeFail: true, + succeed: true, + expectedExitCode: 1, + }, + { + name: "not succeed", + message: "fake-message-1, arg-1 %s, arg-2 %v, arg-3 %v", + args: []any{ + "arg-1-1", + 10, + false, + }, + expectedExitCode: 1, + expectedMessage: fmt.Sprintf("fake-message-1, arg-1 %s, arg-2 %v, arg-3 %v", "arg-1-1", 10, false), + }, + { + name: "not succeed", + message: "fake-message-2, arg-1 %s, arg-2 %v, arg-3 %v", + args: []any{ + "arg-1-2", + 20, + true, + }, + succeed: true, + expectedMessage: fmt.Sprintf("fake-message-2, arg-1 %s, arg-2 %v, arg-3 %v", "arg-1-2", 20, true), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + podFile := filepath.Join(os.TempDir(), uuid.NewString()) + + em := exitWithMessageMock{ + createErr: test.createErr, + writeFail: test.writeFail, + filePath: podFile, + } + + funcExit = em.Exit + funcCreateFile = em.CreateFile + + ExitPodWithMessage(velerotest.NewLogger(), test.succeed, test.message, test.args...) + + assert.Equal(t, test.expectedExitCode, em.exitCode) + + if test.createErr == nil && !test.writeFail { + reader, err := os.Open(podFile) + require.NoError(t, err) + + message, err := io.ReadAll(reader) + require.NoError(t, err) + + reader.Close() + + assert.Equal(t, test.expectedMessage, string(message)) + } + }) + } +}