From 11bfe82342c9f54c63f40d3e97313ce763b446f2 Mon Sep 17 00:00:00 2001 From: Carlisia Thompson Date: Mon, 1 Mar 2021 10:28:46 -0800 Subject: [PATCH] Convert DownloadRequest resource/controller to kubebuilder (#3004) * Migrate DownloadRequest types to kubebuilder Signed-off-by: Carlisia * Migrate controller to kubebuilder Signed-off-by: Carlisia * Migrate download request cli to kubebuilder Signed-off-by: Carlisia * Format w make update Signed-off-by: Carlisia * Remove download file Signed-off-by: Carlisia * Remove kubebuilder from backup/restore apis Signed-off-by: Carlisia * Fix test description Signed-off-by: Carlisia * Import cleanups Signed-off-by: Carlisia * Refactor for controller runtime version update Signed-off-by: Carlisia * Remove year from the copyright Signed-off-by: Carlisia * Check for expiration regardless of phase Signed-off-by: Carlisia * Fix typos and godoc Signed-off-by: Carlisia * Fix test setup and fix a test case Signed-off-by: Carlisia --- .../crd/bases/velero.io_downloadrequests.yaml | 3 +- config/crd/crds/crds.go | 2 +- config/rbac/role.yaml | 20 + .../managercontroller/managercontroller.go | 2 +- .../velero/v1/backupstoragelocation_types.go | 4 +- ...d_request.go => download_request_types.go} | 13 +- .../velero/v1/server_status_request_types.go | 4 +- pkg/builder/download_request_builder.go | 62 +++ pkg/cmd/cli/backup/describe.go | 7 +- pkg/cmd/cli/backup/download.go | 8 +- pkg/cmd/cli/backup/logs.go | 11 +- pkg/cmd/cli/restore/describe.go | 13 +- pkg/cmd/cli/restore/logs.go | 11 +- pkg/cmd/server/server.go | 39 +- .../util/downloadrequest/downloadrequest.go | 84 ++-- .../downloadrequest/downloadrequest_test.go | 225 --------- pkg/cmd/util/output/backup_describer.go | 18 +- pkg/cmd/util/output/restore_describer.go | 46 +- pkg/controller/backup_controller.go | 2 +- .../backup_storage_location_controller.go | 4 +- pkg/controller/download_request_controller.go | 314 +++++------- .../download_request_controller_test.go | 463 ++++++++---------- .../server_status_request_controller.go | 5 +- 23 files changed, 542 insertions(+), 818 deletions(-) rename pkg/apis/velero/v1/{download_request.go => download_request_types.go} (85%) create mode 100644 pkg/builder/download_request_builder.go delete mode 100644 pkg/cmd/util/downloadrequest/downloadrequest_test.go diff --git a/config/crd/bases/velero.io_downloadrequests.yaml b/config/crd/bases/velero.io_downloadrequests.yaml index 179653b91..a4dc1a659 100644 --- a/config/crd/bases/velero.io_downloadrequests.yaml +++ b/config/crd/bases/velero.io_downloadrequests.yaml @@ -32,7 +32,8 @@ spec: singular: downloadrequest preserveUnknownFields: false scope: Namespaced - subresources: {} + subresources: + status: {} validation: openAPIV3Schema: description: DownloadRequest is a request to download an artifact from backup diff --git a/config/crd/crds/crds.go b/config/crd/crds/crds.go index c62dfb26b..f38fc7820 100644 --- a/config/crd/crds/crds.go +++ b/config/crd/crds/crds.go @@ -32,7 +32,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec\\\xbcp\x99n\xe0\xb64V\xe5_ШR'\xf8\t3.\xb9\xe5J^\xe4hY\xca,\xdb\\\x000)\x95e\xee\xb3q\xff\x02$JZ\xad\x84@\xbdڣ\\\xbf\x94;ܕ\\\xa4\xa8i\x868\xff\xf1O\xeb?\xaf\xfft\x01\x90h\xa4\xe1O\xa2@\xad\xd6\\]\x98\x02\x13\x9a:M\t\x1f&\x1e4\x97\x16\xf5\xad\x12e\xee\xf1X\xc1\u007f>\xfez\xff\xc0\xeca\x03kc\x99-ͺ80\x83\x84c\x8a&Ѽ\xb0\x84\xc9O4\x01\xf8N`\xca\xe4\x00\xcc\xc0V>h\xb5\xd7h\xccͭ\xca\v\x81\x16S\x1a\xeb\xb1z\xa4\xde\xf4\xc1\x9e\n܀\xb1\x9a\xcb\xfd\xc8̨\xb5Ҧ?\xf5\xad*\xa5\x05\x95\x01\x13\x02\xa8\x13\xe4h\fۣ\x01{`\x16^Q#\xecQ\xa2f\x16SHK7\t\xe0\x1b&%q\x02\xa8\xa9\f쁛@\xaa\x06\x96w\xf5\xbc\x1eKG\xa6=\xea\x114_\x99\x96\\\xee\xe7\x10\r\xdd\xde\x17\xd5\xffnν\x04Yc\x99\xb6\x95\xd0\xf4Qv?\xc1\xeb\x01esBxe\x06hd\x8b\x9b\xb7N\x06\xc3\x17?w\xca,\xf6&.0Y\x1b\xab4\xdb\xe3g\x95\xb0jY\xady\xefY\x8e~\x99\x18E\xebя\x818ȡ\xa5\xb1\x85\x979\xa8R\xa4\xb0Cp\x13tD\xad=zV\xe8\xa2z\xae{\xaaՀ\xfaq\x8f\xfd\xe5\xee\xb5*\x8b\rԪ\xe6{\a\xcd\xf6V᧚s\x82\x1b\xfbK\xe3\xe3gn,\xfdP\x88R3Q\xe9.}3\\\xeeK\xc1\xf4\xa6\xe6}\xa1Ѡ>\xe2_\xe4\x8bT\xaf\xf2g\x8e\"5\x1bȘ =5\x89r\xb89\x82\x9a\x82%D\x14S\xeet0Hf\x03\u007f\xfb\xfb\x05\xc0\x91\t\x9e\xd2:=\x9a\xaa@\xf9\xf1a\xfb\xfc\xe7\xc7\xe4\x809\xdb\x04\xc9\x1b\xd2yn\x80\xc13\xad\x16\"X/\xcd\x1a\t9i\r12a\x85-5\xf1\xf5\x97r\x87Z\xa2E\x13\x00\x03$\xa24\x165\x99\x10\x04f\x81A\xa1\xb8\xb4\xc0%X'\x86\u007f\xf8\xf8\xb0\x05\xb5\xfb+&\xd6\x00\x93)0cT\xc2IQ\x8e\xceh\xa1\x1f\xfb\xc7u\x80YhU\xa0\xb6<\x92\u07b5\x86\xf5\xae\xbeu\x96u\xe5\xd6\xed\xfb@\xea\xec5z\xf4\x83\xd5\xc5\x14\fѤR\xc3j\x99\xac\xa1\xa3\x95\xa62\x19\x90^ã\xe3\x936QN\x13%\x8f\xa8\x1d\x99\x12\xb5\x97\xfc\xff+\xc8\x06\xac\xa2)\x05\xb3\x18\xa4!6\xb2ђ\tDZ\x12\xaf\x89\x109;\x81F7\a\x94\xb2\x01\x8d\xba\x985\xfc\x97\xd2\b\\fj\x03\ak\v\xb3\xb9\xb9\xd9s\x1b\xfdU\xa2\xf2\xbc\x94ܞn\xc8\xeb\xf0]i\x9567)\x1eQ\xdc\x18\xbe_1\x9d\x1c\xb8\xc5\xc41\xef\x86\x15|E\x88KrW\xeb<\xfd\xb7J\x96\xae\x1a\x98vt\xcb7\x12\xfeQ\xba;-\xf0\xd2\xe4\x87y\xfck\xf2\xbaO\x8e*_\xee\x1e\x9f\x9a\x92\xc6M\x9b\xe6D\xed\x86\xf0Մw\x84\xe22C\xed\x19\x97i\x95\x13D\x94\xa9\x975\x12S\xc1Q\xb6\x89n\xca]έ\xe3\xf4\xff\x95h\x9c8\xab5ܒ\xd7v֦,\x9c\xea\xa7k\xd8J\xb8e9\x8a[f\xf0\x9fNvGa\xb3r$\x9d'|3\xd8hw\xf4Ԫ>ǰ`\x90C^\xe1\x1f\vLZ\x8a\xe1\xc6\xf0\x8c\a\xb3\x9c)]\xdb\x03o\xa5\xd6\r\x80CJ\xe9'\xcaX)\xec3)\xb2yR_\xd0X\x9e\xb4\xfbt\xd0\xf948$\xa2\x83\xc6y\b{@\xedd\x85~ \xb5\xeb@\x04b\xa0\xc1\x94t\x8e\xbd \xb0\xe8N\x82\xa7.T\xb4/\x06v\xa7\x88\xe8\xba\x03\xc7Ss\xa7\x94@ֶ\x01\xf8\x96\x882Ŵ2\xc1frUw\xbd\xee\x14\r2.\x9df8o\xe1\x10\x93\xf5\xafdj\x99\xc6\xde\u009ctr顑\x15=\xe0\x00C\\\xe3\x16\xf3\x1eV#\xa2\x14`\x97B\xb0\x9d\xc0\rX]v\xa7\xf6\xe3\x98\xd6\xec4H\x89/\x95\xf3YB\x88\xaaw\xb0\r\x82'\xe4C*\v@\xb4\xf8\r\x91\xe1\xa0\xd4\xcb\xf4\xd2\xff\xc3\xf5\xa8-\x18$\xb4\x89\x80\x1d\x1eؑ+\x1d\x16[\x87;>@ľl3\v)\xcf2\xd4\x0e\n\x85\xee&\x86U\xc3$\x18SO\xd7\xf4\x18\xdbz\xf8\xd7,c\x1a\xfdz\xc7P\xf6ѥC\xa6O]\xdf\\\x88!S~\xe4i\xc9\x04pi,\x93\x89_\a\xabp\xea\xae\x03\xc6\xd9\xd9\xc3֛\xb5\x88\xb3\xa3}\xcb\xc4)\x89\xe0\xf6\x14Κ\xf7\xbb\x9aA\xf80\xba\xdc\x1ds\xb6Fy1ԥ@\x13&J\xc9r\xd6z}=\x02\xb8\xe2\x82\xf7\xfd\x82\xedP\x80A\x81\x89Uz\x88\f\xd3L\xf5m\xdeF\x8d\xd0n\xc0Z\xd5\xf6\xd7-\xb1i\xa8\xd4(L\x80\xd7\x03O\x0e\xde-;y!(\x90*4\xa4\xbf\xac(\xc4ixq0\xcdi\xdf&T\xb8n\x93\xca܅\xd5W\xeb\xba\xcdڹ\xba\xcdX\xbc6-+\xd6\xff~H\x19\r\xf7ق\xb9\xed\r|O\xc1tD\xe4.\xb4\xdef\x80yaO\xd7\xc0m\xfc\xea\"\tFɗQ\xf2Ts\xff\xe6\x18q\xaeLo\xbb\xe3\xdeQ\xa6\xbf\x91\v\xd5Կ\x19&\x90\xb1\u007f\f\xb6~!\x03>7\xc7\\\x03\xcf*\x06\xa4אqaQw81\xb5\\5͉o%\xc1\xbc\xa7r-g69ܽ\xb9\xe8\xc8\xd49\xd3E\xd4\xe8\x0e\xf51e\x8c\xaa\xdb\xcet\x12*\xd0f\x90k\xcc\xfd\x16\xf3\x89(X\u007f\xa1\xc8\xe7\xe3\xfd'Llj\x02K$\xac\xb7\x84\x8f\x1d4\x9bӆ\x10y\xd9\x02B\x90R\xed.|\xba\xe0\x1a\x18\xbc\xe0\xc9G\x17L\x82c\bsӸγ\x105R\u0382\x04\xea\x05O\x04$\xa4!f\xc6.c\xbdo/x\x9a\xef\xd4!\x9bÆ\x9b\x90Vq\xf4s\x1f\x88\x00\xb4\x87]J2\xa0$R\xb40s\x8b\x82\xa5&\"\xb6H\xed\xb3\x97W\xb1\xa9\x91t#F^\x19\xcf\x14'\xed\a^,Z\xa03\x9d`\x90t\"&\x91\x9e\x99\xe0i5\x8d\x97ﭼ\x86{e\xb7r,Xm\xb7\xbb7nB\xee\xee\x93Bs\xaf,}yw\"z\x94\xcf&\xa1\x1fF*$\xbd\x19v\xebo\xe6\xa2f\x85ط\xad\xdfaU,\xe1\x06\xb6\xd2\xed!<\xad|6\xd1O6e\xed\xdb-/\r%\x9b\xa4\x92+rv\xeb\xa1y\x02\x89\x17\nr\x93\v}\xb4\xaa)\xfdt\x8b >9\xbf\xe0G\xfb̨`I}\x90A\x99=fq\xcf\x13\xc8Q\xef\xc7\x1dA\xb3\x15\xcef/\x99~\x91-\xf5\xed,yZ\xe2\x9ac\v\xc68\x9dCc\xe5ts\xb6Od\xedL\xc7\xc1T\xdexǹu\x90\x93\xa4\xb8a\x86\x9a\xcd\xc3å\xd6{1\xe5\xfb~ۣ\xe4}\\\xce(A\xf77\xe7\xaaHh\xff\x0e\x05\xe3zVC?\xd2\x19\x8a\xc0\xd6Ȑ\x15jN\xe2\xe0s\x03\x8e\x9bG&\xba\t\xe1\x81e)g5Px7\xac\xb2^\xa4q\r\xaf\ae\xbcW\xcc8\x8a\x14\xf8T\xa4\xe5\xda\xe5\v\x9e.\xaf{:~\xb9\x95\x97\xde=\xf746\xfa\xf2\x19\xc0J\x8a\x13\\\xd2\xc8˯\x0f]\x16I݂Nt\x96\xb6,\x98\xa5sA^o\xe0\xaa3\x18\x17\x8a\x8ec\xbb@\xe6\ne\xecB$\x1e\x94\xb1>C\xd7\n\x1e\arC\xd3{\x9a\x90\x13\x02\x96\xf9s/\xa5\xe3\t\x873d\x9dT\xa5\xe3\x92\xc1\xc1\x04g\x0fb\x1a@2!\xe0\xb2\xd6Qo\x1f/\xfd\xb1\aM\xc1\x12\n\v& :Q(\xb4JИ)q\x98\xb5\xbc3\t\xb7*\xd9\xc6\xfc\xa6\xc2\x1f\"L%\xf7b[\x1a6:Ҝ\x15f߽5r\x80N\xb5\xdd\xff\xd3bv\x1eF@u\x1ey\xce䬳\xe8!w\xeb\xc7EU\b`|Ȯ\xf7%\xa9\xf1\xd2H/\b\xcd\xf7u\xb09\x97[\x02\x0e\x1f\xde\xd5\x1dC4\x89x~H}\x1bG\xd6d\xae>x\xdd,T?\xe5>\xd4b\xa9Bͩ~f\x98\xc29\xa9lc{\xbe\x8c\xd0\x1e\x8f+\x03\x19\xd7\xc66\x914t\xb0\xf5\xfe{\x14I%1g\xd3\xf3W?\xae\x91\x00:\xa8\xd7xR8r87\xd4\xe8\x18\x04\x81g\xc0-\xa0LT))\x89ᔔ&\b\xf52dLg\x9d\xacoK\x14\xdb5\x94e\xbed\xe1+\x92\x1e.'r\x1d\xcd\xce?3>\x95\xa9\x8a\xed,6Y\x9e\xa3*'\x9cZ\xddz5?\xaa\xb4\xad#ޜ\xbd\xf1\xbć\xe5\x8e؋(\xea<3ϱ\xcd_xeܒuwP\xc9\xd4[唂j\xc3\x16r?S\x9at\xd1\xf0\x14+\x97Y\xd5H\x01\x83\x8cqQ\xeaE\x16\xed\f\x8a.\x8f샒\xbfOоd\xda\x15-\u007f6M\xb9(T\x9b\xb2\xaa\x85^\x1a\xa8=h|\xcf\x10\xa9\xd0\xdcɌz\xdf()\x88\x12\x93\xa7\x1faR\x836?¤^\xfb\x11&\xb5ڏ0\xe9G\x984\xd9~\x84I?¤\xdfk\x984\x8dɊ\xf2V\x83?\xcd\xcc>{\x84:\x8e\xd8(\xe4p\xaa\u007f\xebk\xaf\x97\xd5\xe5m\x87\xc7\f\xd4]\x86\x92\xee\x15ՠ\xf7\xf9\\\x1f\xfd\xd7f\xbe*\xd4s\xc2\x1f\x85\xd7\x17\x96N\x96\xee-(\xc4\x1b\xaa͜//\x99+*i\xd7$V\x85\x1d\xb1(Q\xc5)z\xab\x8f\x95\xec.\xcclV00!\x9a\xb5)L\xd7D\xf9N\xf5\x8a\xb3\xa5\x1f3\x05\x1f\xd3e\x9b\xe3\x14\xea\x84\xf6m\x12\xe9V\x89\xe1w\xa6\xd0d]\xc6x5F8\xc9@ˎ\x1f\xd6\xed_\xac\n\xb5\x19\xf0\xca\xed\xa1\xb7\x00*\x9at[\x16\xb9o\x16GF\x99\n\xd7\a\xba\x94\x03\xa5Arq=X\x17Sݬh\x92\x13~-\xfc\xa6\xe8,}\x9b\n\xed\x97\xd4n|u\xc5F\xbb&c\xd0Ȟwر\xb4\x84tyMF\xbb\xe6b\xc4\xc9,\xa8\xc48\xbb\xd2b~\xbf5YU\xf1\x15\xb5\x14\xb1Nb\xca\xe1NTP,\x889\xe6\xab%\xbe\xaaF\x82\x0e\xf3&\xb0>\xab2\xa2Q\xf50\x01rY=\xc4\x02\x92\xcc\xd5>\x9c]\xf1Э2\x98X\xc4\\\x9d\xc3x\r\xc3\x04\xd0\xc1\xea\x86%\x95\v\x130\xab\x9a\x86w\xacW\x98\xa9Rx\x9fJ\xc2o\x8d=\xc7j\x0ef*\rf\"\xd3)\xacfj\t\x96W\x10\xcc\xd0\xe7+\xab\x05\xaaz\x80\xc19ϭ\x11hW\x01\f\x82\\X\x190r\xf6?\brA=\xc0̉\xff \xd8I\xc78!\x11\xa3?)\x9d\xa2\x9e\b#\x97\xc9\u0084\x1c\xb4\xd3(\x9d\xd9:u\xc7\xf1\x8e\x97\xeb\xd5\fK\xfb\xb4PU\xc5l\x02\xbfp\x99z\xf29\xde7\xdc \xdd]\xa4\x8a\x84\xca\x0fׁ\xca\x10\xc8N\x18l\xb0`\xfe\xce\xf7\xee\xe47\xc6f\rw,9\xb4;\u0081\x19\xb75\xca\aJ1/\xab]\xc3M\x1c\xe3\xbe\\\xae\x01~V\xd5f\xacy\u007f\xc4\xf0\xbc\x10'(\r\xc2e{\xc8\xf9A\xf1\x00\xbf\x8dd\x859\xa8xAo2.~l\xf7\x1d\xd8L\xc6\xeby\x89PeZ\xc1\x1ed\x17\x93'xx&\xa7NW\x9f\x92\xfa\xe2Wp\xdd1\xd8\xed\xde\v\xfb\xe9=7\x97\x9d[\xee\xd3\xebo\xf7m]\x83\rJ\x1cS8\xb1\xee\x85\xc5\xfb\x98\x03\x97雭Ϊ\x8e\\\x93\x1f\xbe\xca8\xa0a֊\xc9E<=}\xf6\x88[\x9e\xe3\xfaS\xe97\ueac2i\x83\x8e~qA~\xd0\xce\xfdyP\xaf=\x84\x85\n+\xfd\xa9\x8b\xafF\xca\xd9Rv`1\xd6\xfe\xeef\x14\xb0H\xa6iq|\x1e\x1e\xd3\xd8{4\x98\xe25Xec\xa3z\vl\\\\w\xbb\xbb\xe6\x1b\x06߾\x81\x1dv\xc6×}\xe9\x11\x88\xb9\xeb\xbe\xfeI\x8fpy?d\xf8KM7\n\xc3s\x1f$\x8cg\xdf\xf8\r\xe9\xcc֫%S<\xb9\xed\xf7\xa7\xab\xf3:\xf5HQ\x1a\x955ߨ\x88\t\xd3\x01\x0fV\x03\xf3\xe3(\xf8s\xb00\x05<\xa2\x04%)?J7\xf6\xfc\xdb)\xdd1\xfd|E\x03FH\xbf\x96\x85P,\x8d\x9a\x1b}Nx\x0e\xe0\x89\xec\x91>\xa2\xbe2\xa3\x10\xe9jr\xa6\xf4\xd0\xf2\xbb\x92\xe5\x1d\x83\u007f\x88b5\x00p\x81\x1d\x1b\x10)\xff\xee\xca\xf4U]\xea\xe2\xb5#9\xef9\x96ޚ\xbbo\x9et\xee\xab\xfa\x1dW\x8f\xa6\xf4:\xd4/\x1f\xf5~j\xbf\x84\xd4\xfc\xe5\x81i˙\x10'\x0f~d\xd6\xde\xe7O\xe8\xec\u0088#\x18\"`\xc0l\x9a\x86\xa1S\xbd\x85\xe0\xd2\xf3\x9a\x0e\x8fv\xaa\xb4-\x85\xeb=Rԟo\r\xf7\xcab\xcc\x13\xf16D\xe7\x01\xd1\xd8\x15f\x99\xd2\xd6\xefWV+\xe0Yp,=\xa8\xce:S\xa6ӿ\xbe\x01\xdcֻ\xf6Z6)\x16\xd4\xc8\fɦ\xa5\x17@蘁%\x89\x8bO\xf0\xc6X&z6\xe0\xabӛ䯝ta\xfa\x97\x9e;\xeb\x11y\xdb\xec]\xd55\x97\xf9\xce\xefI\b\x98\xa7\x17\x1d\xe5z\xab'\x86\xb7\xf0;D\t\xaf\x9a[\xeb\xecM3\x01\f\xd6Y\x18!\xc0(\xc8\xd8\xe0\x1d\xf1q\x9bG\xbf*\xcb\xc4v,\x81\xd1\x0e\x01\xab\xaeq94\xb8\xbf(\xe5ذ\xa3\xa5\x0f.\xc7\x17\xf3p\x13G:\xc6%\a&\xf7N\x80\xb4*\xf7\x87(\x81#\x9eb8i[:\x84\xa0\x10\xe5މtH\xa4\xdaR\xcb\xc6\xee3\xa4V\xd3\x06\xaa,y\x81\xb2\x18\xae4\xf0o\x03\x85Ǟn\xc2\xdd\xefU\xa6U\xbe\n\xf4\xa7\x1c\xe9u\xd8\x19j\xae\\\xc8D{\x9ap\xfdr\x04,\xb1\xbd(P\x023\x01\x97\xd9:\xa3)F\x8eo\xd4Z\xaf\x80\xcd\xecS\x9a]g\xe2\xaf\xf0F\xd8\x1a\x1e\xc3\uedaf\xc4Z\xe5\xfe\xe1\xb0f\\\xe3v\xa62>\"\xe5\xf7Ҟ\xf5ƅe\x1ai\xdb\xe2\xef\\\xf6 \xb6\x02\xaaV\x00\xd5F\xfd_\x13;\xd5\xcfj\xdd\xcdGQϝΝ\x833\xa7\xc15\xbc\x18\xfb\xfc\x81g\xfd\xfdEQ\b\x9e8l\xff\xf8\x9d\x0eĎ\v\xa2\x8a\xabɀ\x82\xa2\x87*6\x80OXhL\x9cV\xf6\x91\u007f\x10\xe8\xfc\xbdAlG*W\x8b\x03\xbb\xf6\x16\xd1|\xb4\x16\xf3b`\xae\x89=b=h\xcc\xf0\xb1ء\xb7\x80\xf8zY\x04\x15*?F7\x85\x8b\x17R\x85\x1a\xe7,\xa4\x1a4\xb6\x10S&\xce\x00e\xe5\x90+\xaa\xf6\\︪\xf8\xa4\xe2\xe4*\xe2ۇ\x03\xbb\x90%o-\x9e\xbd\x0filC\"~\xff\xa2\x8dȀ\x1d\xef|\xaa\x1f\b\xfdP\xff\x17\x9e\xf4\xf4/\x17\x1e}\xfd Y˴\xa1\xda\x01\x95\xf0\xa5N\x10\xb0$A'\xbb\xf7ݗ\f//\xe9\x9f\xf8X!\xfd\x9b(\xe9}\xa9\xd9\xc0\xff\xfc\xef\x05\x84<\xd3s\xc4\xc3}\xfcG\x00\x00\x00\xff\xff/\x18y\xfcLU\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYKo#\xb9\x11\xbe\xebW\x14f\x0f\xbe\x8cZ3\xd9K\xa0K\xa0\x91\xb3\xc0$\x9e\xb11\xf2:\x87$\xc0R\xcdj\x89\x11\x9b\xec\xf0!\xad\x12\xe4\xbf\aE\xb2\xdf-\xc9^\xecn_\xecn\x92ŏU\xf5Ճ\x9a\xcd\xe7\xf3\x19\xab\xc4\v\x1a+\xb4Z\x02\xab\x04\xfe\xecPћ\xcd\x0e\u007f\xb4\x99Ћ\xe3\xc7-:\xf6qv\x10\x8a/a\xed\xad\xd3\xe57\xb4ڛ\x1c\xef\xb1\x10J8\xa1լD\xc78sl9\x03`Ji\xc7賥W\x80\\+g\xb4\x94h\xe6;T\xd9\xc1oq\xeb\x85\xe4h\xc2\x0e\xf5\xfe\xc7\x0f\xd9\xf7ه\x19@n0,\u007f\x16%Z\xc7\xcaj\t\xcaK9\x03P\xac\xc4%lY~\xf0\x95uڰ\x1dJ\x9dǽ\xb2#J4:\x13zf+\xcc\x03\x12\xce\x03<&\x9f\x8cP\x0e\xcdZK_FXs\xf8\xcb\xe6\xf1\xeb\x13s\xfb%d\xb4 \xab\x8c>\n\x8e&`\xe6hs#*\x17\x90=\xa5\x11\xd0\x05\xb8=&\x00\x90\x10\x84\xf9\x11\xd9SW\x84;W\xb8\x04\xeb\x8cP\xbb\xc9\r\xf5\xf6_\x98\xbbM\x94\x92m}~@7\xde\xfcS\xf8\x0eN\x83\xb7\b\x856\x10\xd7Ml\xff\xa9\x15qusǜ\xb7Y\xb5g\x16'\xf6\x8b\x87K\xb0\xe0!\xe9\x17\xe2*\xb0>\xdf\x03\xb3\xb0:2!\xd9V\xe2\xe2G\xc5\xea\xff\xbb\xaah\xa4\xbf\x02\x8adֽ0)xc\xf71\xae\x87\xd1\x1c\x106\x98\x83V\x83\xa3\x0f\x03\xe3 \xd4\xde\x01'f\x83H\x80c\x94\x81\xbc\x03\x96d\xc3Ko \xa2\xa6\xf7I۱\x81\xf4\tM)l\xa0Q\xb0\xd7\xd8e\x1a\\\x1d\f\xab \x11\x1a\x91\xd7\xd4V\xd3-\x1bQ\xa5+p\x87\xaf9\tǂy9\xe1x\xf7q\xe0\x15\xd0\xef;\"\xe2n[\xad%2\x9a\xb33\xdaWKh\xc9\x19\x17\xa5\xd0\x10\xc3Jt\xb9\xe4q\x0f]\xf9RX\xf7\xd7\xcbs\x1e\x84\x8d\xbbV\xd2\x1b&/\x85\x860\xc5\xee\xb5q_ۭ簵2\x8e\b\xb5\xf3\x92\x99\v\xcbg\x00\x95A\x8b\xe6\x88?\xaa\x83\xd2'\xf5\x83@\xc9\xed\x12\n&\x83\x8f\xdb\\ӡ\x83\xf0\x8a\xe5\xc1\x83\xacߚ\x14'ӆ\xd1ח\xf0\xdf\xff\xcd\x1a/$E\x87A]\xa1Z=}~\xf9~\x93\xef\xb1d\xcb\xe4\xac\x13\xcc\x1c\xa8\x80H\xc0:~\xbeG\x83\xf0\x12\xb4\x1d9`ө\x92DHᣦCet\x85Ɖ\x1a%=\x9d\xac\xd0|\x1b`\xb9#\xb0q\x0ep\xca\x03\x18\xb9\x98\xa29r\xb0\xe1 1d\n\v\x06\x83\x12\x95k\x8d\xdb\x00*\x80\xa9\x04+\x83\r)\xdaX\xb2\x97\x97\x9c\x92\xc7\x11\x8d\x03\x83\xb9\xde)\xf1\x9fF\xb2\xa5\x90\x18\xe9\xef0\xb9A\xfd\x84`\xaf\x98$5{|\x0fLq(\xd9\x19\f\x86\xc8\xe9UGZ\x98b3\xf8B\xf1B\xa8B/a\xef\\e\x97\x8b\xc5N\xb8:\x0f\xe6\xba,\xbd\x12\xee\xbc\b\xd9Ll\xbd\xd3\xc6.8\x1eQ.\xac\xd8͙\xc9\xf7\xc2a\xee\xbc\xc1\x05\xab\xc4<\x00W15\x95\xfc\xbb\xc6\x19\xee:H\a\x1c\x8fO\xe0\xc4E\xbd\x13\x1b\xa2\xcd㲈\xbfU/}\"\xad|\xfb\xf3\xe6\x19\xeaM\x83\t\xfa:\x8f9\xa4Yf[œ\xa2\x84*\xd0D\xc3\x15F\x97A\"*^i\xa1\\xɥ@\xd5W\xba\xf5\xdbR8\xb2\xf4\xbf=ZG\xf6\xc9`\x1d\xaa\x01\xd8\"\xf8*\x04\xd7\f>+X\xb3\x12\xe5\x9aY\xfc\xcd\xd5N\x1a\xb6sR\xe9m\xc5w\x8b\x98\xfeĨ\xad\xe6s]_LZh\x92\xa5\x9b\n\xf3\x1eO8Zaȗ\x1ds\x18\x18\x90H\xdbS\xe9\xe5\xc0x\x99\xbc\x81\xc0Mv\xea\u007f\x1f@]5\xd3zت\x9b\xf9k \x14\x9a\xf8\x93\rFP\xf9r\ba\x0eߐ\xf1G%ϓ\x03\u007f3\xc2\r7\x984\x17=\x11\xd6\xe6\xac\xf2'4B\xf3\xab\xc7\xfd4\x98\xdc\x1cz\xafOP\x04\xb7UN\x9e)\xaeسʇq\xb3~VO\x9f\xeb\x18\x1a\xc9ѯ\xc72X%N\xea\x02>\x00\x17\x96*#\x1bD\x0e\xd5Ce-\x8d.\xc1\x19\xff\xeaC\xe7Z\x15b7d\xef~\xe7\x1eC2\xeb\xa8i@\xfe\r\x8fbx+2\xd6\xe6\xc3h~M\xdeƵ\xe9姺\xdd\\\x984\xed\xa7\xd1\xf1\v!\xa9\x16\xbbv\x838q\x03\xfai\xf3pgC\xc9L\x8d\xfdH\xe8\x89\xccg\x03@\xaa\x99u\xea\xe7\xbduh&\x8c\xdd\xd8JXP\x1a\xa4V\xbb\x1e\x15⓺{\xaa\x92\xa2\xebh\x03\x1c\xa91'\x96\xe7{\xa6v\xd8\xde\xd8$\xec\x1d\x94\xe4\x18c\xa4}\xefh\xbdA\xa8iWx\x85\r\x9fŸ4\x1e\xdd3\xb7S\xa7\xef\x98\x1b\xd4ɖ\x17\x9a\x89\x1b\xba\x1e̮s()r\xee\xea;\xf0\xf6\xf9e\xcd\xe2\xf8j\xfd\xe6\xe9\u007f\xf1-\xfb\xf8\xf8̶\xf7\xed\xbf\xff\xd9\xc3/\x1c\xd7\xd3+ͨO\x98{C-P\x1bw\x03\x99\xa6b\xef\xeb\xae9V\xbd\x9fE\xba#ßLn\x9ee\"\xdf\f>\xb5?\xa3}l\xdf\xd2/]\xf1\x96>\fP[Iɥ\xa3\xc8\x14Qҗ6\x89Q\xf6\xa8\x1c\xf2\xaf\xc3\xeb\xfaw\x91v\xf5\x9d{x\xcd)\x9f\xc7\xdf\xfc\xe0\xef\xff\x9cE\xa9\xc8_j\x1c\xf4\xf1\xff\x01\x00\x00\xff\xff\xe4\x1a\x03\xe4r\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\x80\xef\xfe\x15\x98}\x0f{y-\xef\xce^:\xbam\xb3ۙ\xf4#\xf5\xc4\xdb\\:=\xd0$l\xb1\xa1H\x16\x00\x9d\xa6\x9d\xfe\xf7\x0eIɶ\x1c%i\x0fՍ\x10H\x80\x0f>\x88\xc5r\xb9\\\xa8h\xef\x90\xd8\x06߂\x8a\x16\u007f\x17\xf4y\xc5\xcd\xfdW\xdcذ:\xbcߢ\xa8\xf7\x8b{\xebM\vW\x89%\xf4\xb7\xc8!\x91\xc6O\xb8\xb3ފ\r~ѣ(\xa3D\xb5\v\x00\xe5}\x10\x95Ŝ\x97\x00:x\xa1\xe0\x1c\xd2r\x8f\xbe\xb9O[\xdc&\xeb\fR\xb10\xda?\xbck>4\xef\x16\x00\x9a\xb0l\xffb{dQ}l\xc1'\xe7\x16\x00^\xf5\u0602A\x87\x82[\xa5\xefS$\xfc-!\v7\atH\xa1\xb1a\xc1\x11u\xf1Ø\xe2\x9crk\xb2^\x90\xae\x82K}uj\t\xdfn~\xbcY+\xe9ZhX\x94$nb\xa7\x18\x8b\xc3\x06Y\x93\x8dR\xdc\xfaT\xac\xc1\xd7\xc5\x1c\xdcV{P\xf7\x00'݁b\xb8\xf6k\n{B\xe6՚\x82Ff4\xe5\xa8\xea\xf1\xa6h\x17\x81\x81Ϡ\xac\xdf!\xd5\xc0\xed(\xf4\xe5D\xf4&\x06\xeb\xa5,\xb4\xb3\xe8\xa7\xd09m{+<\xa6_\x8eO\x03W\xa5U\xe7\x1e\x92b.j\xd3\xc0\xb5\x87+գ\xbbR\x8c\xff9\xf6L\x98\x97\x19\xe9\xeb\xe0\xcf_\x98\xa9b\xa5u\x14\x8f\xed\u007f6B3e\xb9\x89\xa8s\xcc2\xb8\xbc\xd7\xee\xac.e\x00\xbb@\xf0\xd0Yݍe9!z,\xe0\xe6L\xf5\x97V\x96p\x83\x0fOd\xa7\x11\xe5ɯ\xe9\xc8\xf2*\x93\x99\x84\xbb\x10\x9df\xba\xf7\xa7\xd50xՉ\xa2\xfc\x00(\xaf\xb39\x03\xcb\x12H\xedGԧ,VZc\x1447\x97\x13ƛ7\x93Q\xa1,u\xf0u\xf4\xe3\x16~\xfeeQOEs7\xfa\x91\x85\u007f\a\x00\x00\xff\xff\xb0\x1aq.\xff\n\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WAs\xdc6\x0f\xbd\xef\xaf\xc0\xe4;\xe4\xebL\xa4M&\x97\x8en\xa9\x93ΤI]\x8f\xed\xf8\xd2\xe9\x01\xa2\xb0\x12k\x8ad\tr\x1d\xb7\xd3\xff\xde!)i\xb5Zy\x9d\x1e\xaa\x9bH\x10xzx\x00\xa1MQ\x14\x1b\xb4\xf2\x8e\x1cK\xa3+@+\xe9\xab'\x1d߸\xbc\xff\x9eKi\xb6\xfb75y|\xb3\xb9\x97\xba\xa9\xe0\"\xb07\xfd5\xb1\tN\xd0{\xdaI-\xbd4zӓ\xc7\x06=V\x1b\x00\xd4\xdax\x8c\xcb\x1c_\x01\x84\xd1\xde\x19\xa5\xc8\x15-\xe9\xf2>\xd4T\a\xa9\x1ar)\xc2\x18\u007f\xff\xba|[\xbe\xde\x00\bG\xe9\xf8\xad\xec\x89=\xf6\xb6\x02\x1d\x94\xda\x00h쩂\xc6\x10\x1f\x89\x92zG.'.\xa9,z$\xddX#\xb5O/BI\xd2Ǥs\xa8{\xe9y\x94m\xccO\t\x17\xe9\xf6\x80\x9a \xd8X\xf8M\t\x1f5\\`O\xea\x02\x99\xfes\xda#\xc3\\DJ\x9f'~~\xe9\x1d\x1bf\xb6\xa6\xe5\xf1VZ\xcdТ\x94o,\x89\x98\xafHZ<'wR\xa4\x12\x80\x9dq\x80\x87\xca\x1eh+g~\xd7j3\x81J\r\xfaxm\xd9\xe6s\x0f\x97\f\x0f\x1d\x1e\xb7\x90\xffSٖ\xb1\x0f\xf0\x00!w\x86\xefʅ\xbf\xa7\xa2\xafit\x15\xc3(\xd5\xf8\xe9\xfe\x89kg\x194>\xa4C\xbf漀\x1f\x12\xd2Ϧ=\xb3{a\xb4\x8f\x82>cr\x17\x87\a\xba\xd1h\xb93g-\xc7\xd1h\xba[\x96f\xd7\x14[-=\x05iؾ&\x0ej5Ъ\x10\xc7'ݞϱ\x9cf\x8a\x81e=\x9b/\xeeO\x87\nx\x90\xbe\x83\x87N\x8an\xc5+\xa4c)A\xb1\xbfL\xe3\xc9Z\x8a\xce\xc0\x8e:\x96\x8eN\xe4Q\xc04\x90\xcc\x17\xa7Ai\xe9|Qs뎋\xa1\x16\x9e\xad\xd84\xc1}s\xcd\xe6\xb1p U\x04\xe7HO\xc3b\xbc\xad\x96\a\xbe\xa5hG\xc5\u007f\xb9\xfe|\xb6r\xdf\x1f\xecҤ\x8dRg\x1c\xd6Q\xc1\xb2\x8dwk܋\xb5\x9b*kI@~\xe6w\xfc\xb3Y\xa3\xafV\xba\xd9\xc8\xf2\x04\xb4\x0f\x93Yn,\xa4\xf3\x15\xb1\x9c^\x92;\xe2t\xed\n\xd4'\xd8j\x82\x86\x14\xc5ѷ~̝\xf1\x91=\xf5K\xbc;\xe3z\xf4yb,\xbc<\x11J\xfc\x8b\xc0ZQ\x05ޅu\x15\xad|l\xfa78\xfb\x9dW\xd1b-\xfdSq\x9d\xc9?<\xd1\xc1\x8a\xf8{q\xb2v\xfc\xbb\xf1,\xfa\x15q/\x96\x0e\xbf_o\x0eo\xc3\u007fR\x9e\xc6\xd3\x06@\x9at\x9b\x19u\xc3\xcc8\xac\x1c*\x06\x85 멹\\N\xe7/^\x1c\x8d\xdb\xe9U\x18\x9d\xffԸ\x82_\u007f\xdbd\xaf\xd4܍8\xe2\xe2?\x01\x00\x00\xff\xff\x9cДê\x0e\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WMo\xdc6\x13\xbe\xef\xaf\x18\xe4=\xe4-\x10i\x13\xe4R\xe8\x96:)\x90&u\r\xdb\xf1\xa5\xe8aD\xcdJ\xac)\x92\xe5\x90\xeb\xb8E\xff{AR\xd2j\xb5\xf2:=T7\x91Ù\x873\xcf|pS\x14\xc5\x06\xad\xbc#\xc7\xd2\xe8\n\xd0J\xfa\xeaI\xc7?.\xef\xbf\xe7R\x9a\xed\xfeMM\x1e\xdfl\xee\xa5n*\xb8\b\xecM\u007fMl\x82\x13\xf4\x9evRK/\x8d\xde\xf4\xe4\xb1A\x8f\xd5\x06\x00\xb56\x1e\xe32\xc7_\x00a\xb4wF)rEK\xba\xbc\x0f5\xd5A\xaa\x86\\\xb20\xda߿.ߖ\xaf7\x00\xc2Q:~+{b\x8f\xbd\xad@\a\xa56\x00\x1a{\xaa\xa01\x0fZ\x19l\x1c\xfd\x11\x88=\x97{R\xe4L)͆-\x89\x84\xa1i\x120TWNjO\xee¨\xd0g@\x05\xfct\xf3\xcb\xe5\x15\xfa\xae\x82\x92=\xfa\xc0\xa5\xed\x90)\x81m\x88\x85\x93\xd6'H\xef\aK\xd7\xd9\x12di\xe0 :@\x86Kz\xd8^9#\x88\x99\x9at:\x03\xbcIbi\xc1?Z\xaa\x80\xbd\x93\xba=\xb1mI\x94\x1e]K\xbe\x8c\aO\xed_bO`v\xe0;\x02d6B\xa2\xa7\x06>\x85\x9a\x9c&O\fn\x88\xc5\xcc\xfamҘ\xce\xfe+\b1ħ\x10n\x1fm\x82\xb0\x93\x8a\xc0\x9b\xc9\xf9\xa7\x06?\x8d\xe7\xcf\x19\x1c\x89R\x9e\x04y\xa6\xf0];Gޠ\x8f\xbf\xad3\xc1Vp\x88u\x96\x1e8\x96\xf9\xb9\x88W\xdaQ\x92\xfd\xa7\xb5\xdd\xcfr\x90\xb0*8T\xa7\xbcJ\x9b,u\x1b\x14\xba\x93\xed\r\x80u\xc4\xe4\xf6\xf4E\xdfk\xf3\xa0\u007f\x94\xa4\x1a\xae`\x87*\x91\x89\x85\x89\xf8c آH\x14\xe1P\x8f!\x1b\x90gNU\xf0\xd7\xdf\x1b\x80=*\xd9$\xbf\xe4McI\xbf\xbb\xfax\xf7\xf6Ft\xd4c^|\x86\xa4\x92\x01a\x009\x8f\x18\xa0\x06t^\xeePx\xd89\xd3C\x8d\xe2>\xd8A'\x80\xa9\u007f'\x11In\x1c\xb6\xf4j\xa29\x0e\x82\xa0L\x9bxP\x0eG\xac3\x96\x9c\x97\xe3U\xe27\xab(\xd3\xda\x02\xf0\xcbx\xa3,\x03M\xac!ĉ\xe1C%\xa0\x068\xdd63_F\x92'O\xeb\\Ufj!\x8a\xa0\x1e\x90\x97p\x13\xa3\xe1\x18\xb83A5\xb1\xf0\xec\xc9yp$L\xab埓f\x8e~\x89&\x15\xfa\x91'\xe3\x97ʅF\x15c\x11\xe8\x15\xa0n\xa0\xc7Gp\x94\xbc\x13\xf4L[\x12\xe1\x12~6\x8e@ꝩ\xa0\xf3\xder\xb5ݶҏ5T\x98\xbe\x0fZ\xfa\xc7m\xaa\x84\xb2\x0e\xde8\xde6\xb4'\xb5e\xd9\x16\xe8D'=\t\x1f\x1cm\xd1\xca\"\x01ש\x84\x96}\xf3\xbf\x891/gH\x17Y\x96\xbf\x94\x06O\xfa=\xa6A\xa6G>\x96\xf1\x1f\xdc\x1b\x97\xa2W\xae?\xdc\xdcN\x95%\x85\xe0\xd8\xe7\x99'\xd31>8>:J\xea\x1d\xb9\x1c\xb8IJ\xa8\x91tc\x8d\xd4>\xfd\b%I\x1f;\x9dC\xddK\xcf#mc|J\xb8H\x9d\x04j\x82`c\x11hJ\xf8\xa8\xe1\x02{R\x17\xc8\xf4\x9f\xbb=z\x98\x8b\xe8\xd2\xe7\x1d?o\x80ǂ\xd9[\xd3\xf2ءV#\xb4H\xe5\x1bK\"\xc6+:-\x9e\x93;)R\n\xc0\xce8\xc0Cf\x0fn+gz\xd7r3\x81J\xc5\xfaxmY\xf2s=\x97\f\x0f\x1d\x1e\x97\x90\xffSٖ\xb1\x0e\xf0\x00!W\x86\xefʅ\xbe\xa7\xac\xafqt\x15\xc3H\xd5xu\xffD\vZ\x1a\x8d\x1f\xe9Я)/\xe0\x87\x84\xf4\xb3i\xcf\xec^\x18\xed#\xa1ψ\xdc\xc5A\x82n4Z\xee\xccY\xc9qL\x9a\xfa\xccR\xec\x9ab\xa9\xa5\xa7 \r\xdb\xd7\xc4A\xad\x1aZ%\xe2\xf8\xa5N\xfa\x9c\x97\xd3|1xY\xcff\x8d\xfb\xd3\x01\x03\x1e\xa4\xefࡓ\xa2[\xd1\n\xe9X\nP\xac/Ө\xb2\x16\xa23\xb0#\x8f\xa5\xa3\x13z\x140\r'\xf3\xc5ihZ*_\xe4ܺ\xe2bȅg367\xe8o\xcd\xd9<\"\x0eN\x15\xc19\xd2\xd3\xe0\x18\xbb\xd5\xf2\xc0\xb7$\xed\xc8\xf8/ן\xcff\xee\xfb\x83\\\x9a\xbaQ\xea\x8c\xc3:*X\xb6\xb1\xb7ƽ\x98\xbb)\xb3\x96\x0e\xc8\u07fc\xc7?\x1b5\xfaj\xa5\x9b\x8d,O@\xfb0\x89\xe5\xc2B:\xb7\x88\xe5\xf4\x92\xd4\x11\xa7\xb6+P\x9f`\xab\t\x1aR\x14\xc7\xe0\xfa1W\xc6G\xf6\xd4/\xf1\xee\x8c\xeb\xd1\xe7\xe9\xb1\xf0\xf2\x84(\xf1E\x81\xb5\xa2\n\xbc\v\xeb,Z\xb9lz'\x9c\xbd\xe7U\x94X\v\xff\x94\\g\xe2\x0fOT\xb0\">5N֎\x9f\x1eϢ_!\xf7b\xe9\xf0\x14{s\xf8\x1b\xdeLy2O\x1b\x00i\xeamf\xae\x1bf\xc6a\xe5\x901(\x04YO\xcd\xe5rR\u007f\xf1\xe2h\xf4N\xbf\xc2\xe8\xfcj\xe3\n~\xfdm\x93\xb5Rs7∋\xff\x04\x00\x00\xff\xff\x01NS+\xb6\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs#\xb7\x11\xbe\xf3Wt\xad\x0f\x8a\xabġ\xbdN\xa5R\xbc\xedJٔ\x12[\xab\x12e]\xb6\xf6\x00\x0e\x9aC\x843\x00\x82Ɛ˸\xfc\xdfS\r`\xc8y\x91\xa2\xec\xac3\x17\x89x\xf4\v\xfd\xf8ИL\xa7Ӊ\xb0\xea\x19\x1d)\xa3\xe7 \xac\xc2/\x1e5\xff\xa2l\xf3Wʔ\x99m\xbf_\xa2\x17\xdfO6J\xcb9\xdc\xd4\xe4M\xf5\x88dj\x97\xe3-\xae\x94V^\x19=\xa9\xd0\v)\xbc\x98O\x00\x84\xd6\xc6\v\x1e&\xfe\t\x90\x1b\xed\x9d)Kt\xd3\x02u\xb6\xa9\x97\xb8\xacU)\xd1\x05\x0e\r\xff\xedw\xd9\x0f\xd9w\x13\x80\xdca\xd8\xfe\xa4*$/*;\a]\x97\xe5\x04@\x8b\n\xe7`\x8dܚ\xb2\xaep)\xf2Mm)\xdbb\x89\xced\xcaL\xc8b\x1ed\x902\b&\xca\a\xa7\xb4Gw\xc3\x1b\xa2@S\xf8\xc7\xe2\xe3\xfd\x83\xf0\xeb9d䅯)\xb3kA\x18\x84\x95H\xb9S\xd6\a\x91\x1e\x8c\x84\xe7\xc0\n\xde\a^\x10\xd7\x03\xd5\xf9\x1a\x04\xc1=\xeefw\xfa\xc1\x99\xc2!Q \x10e\\\x84ua\xc0\xef-\u0381\xbcS\xba8\xc1\x9e\xbcp\xfe\xa0\xeeP\x0e\x9e\x82\xdd\x1a5\xf8\xb5\"\x88z\xc3N\x10\x84\x9d([\x9co\xd8zi$\xb2\x96\xc2〱\xc5<\xb3Ff\xbc\x89\xac\xc8G\xb4\xbfo\xa6\xc0\xac\xc0\xaf\x91\r\x1f\x0eS(\xadt\x11\x86\xe2A\x807\xb0\xc4 \x17J\xa8mK\x9c\xfb\x0e\xfd\xb3\xb6h\x8b4.\xcd\xef\x11\xe4\xc1\xc8\xcbD\x88\x94\xce\v\xf0\"\xb7\xe7#\x91\x17\x19:\xb4\xe6N\xa2\xf6j\xa5\xd0\r\x19?\"y\x95\x03/#\xe5\x8dۃ:\xac\x86\x95qm\xa7h\x89\x90\xb6=\xa25\x97\xc9\x11),\xbcq\xa2\xc0\x1fM\x1e\x82\xf0\xbc\x1dRT\xa4=\xd0lb_u\xd8qVZ\x9b\xba\x94l.\xf2\xc6u<\xb6\xbf\xfbEi\x9bl\x93\r2E\x8b\xea\xbb\x02\x871P8S\xdb9\x1c\x13F\\\x9d\x12ULr\x0fF\xc6\xd3{\u007f\xb4h\xa9\xc8\xffsl\xf6GE>\xac\xb0e\xedD9LNa\x92\x94.\xeaR\xb8\xc1\xf4\x04\xc0:$t[\xfcYo\xb4\xd9\xe9\x0f\nKIsX\x892d$ʍm\x87\x11\x1b\x8e\xea\xa5K9\x98\xe6\xf0˯\x13\x80\xad(\x95\f\xb6\x88\xaa\x18\x8b\xfa\xdd\xc3\xdd\xf3\x0f\x8b|\x8d\x95\x88\x83\xcc\xccXt^5\x1a\xf3ת\x01\x87\xb1ޑ_1\xa9\xb8\x06$g}\xa4\x18\x06q\f%P`\x13\xddB\x11\xfb*\xab\xa5\xfd\xf1@\x9bϬ@h0\xcb\u007fa\xee3X\xb0\xea\x8e\x1a\xf7ȍޢ\xf3\xe007\x85V\xff9P&\x8e5fY\n\x8f\xc9\xe4\xcd\x17\x12\xbc\x16%\x1b\xa1\xc6k\x10ZB%\xf6\xe0\x90y@\xad[\xd4\xc2\x12\xca\xe0'\xe3\x10\x94^\x999\xac\xbd\xb74\x9f\xcd\n囪\x97\x9b\xaa\xaa\xb5\xf2\xfbY\xa8]jY{\xe3h&q\x8b\xe5\x8cT1\x15._+\x8f\xb9\xaf\x1d΄U\xd3 \xb8\x0eE/\xab\xe47\x87\xe3\xb9jI\xdas\xe9\xf8\x05\x9f;iw\xf69P\x04\"m\x8b\xf2\x1f\xcd\xdbd\xbfǿ-\x9e\xa0a\x1a\x8e\xa0k\xf3`\xed\xe36:\x1a\x9e\r\xa5\xf4\nS\x16Y9S\x05\x8a\xa8\xa55J\xfb\xf0#/\x15\xea\xaeѩ^V\xca\xf3I\xff\xbbF\xf2|>\x19܄\xda\xcfA^[\x8e8\x99\xc1\x9d\x86\x1bQay#\b\xbf\xba\xd9\xd9\xc24e\x93\xbel\xf86d\xe9.\x8c\xd6:\f7\x98b\xf4\x84z\xe9`a1\xe7\xf3b\xa3\xf1>\xb5R)#r\x9e\x16\xfd\xe5Y\x8b\xecXh\xf27\x9a\x95\xbbKz2\xbd\x1f\xdb\xd1H\xa5[ٻI\xcdqe\x8f$@\xd9\xcf\xe6\xecx\x83RD)\xa1g\xbd\xfd\xa3F\xe7O\x1b\x89g\xe5\xbf7\x12\xc7\xc4\xe5\x8d\xe0\xd7\"\xfa$c3\xce4\xb5\x0e\x18\xc0\xe8\x8b\x05\xb0F\x9e\xe5\x9f(\vp\xb8B\x87:\xc7&\xf9\x9c\xc3\x1d\x03\xf3\xb5\x91A_\xb6S\x87\r'\xf3\xf1\xa8\xa4\xef\x1e\xee\x9a\x1c\xdc\x18)\xc9\xec\xfb\x1c\xcfZ\x84\xbf\x15\x17\x9eP`_\xe2zu\xb7\x8alBF\xf2\x06\x04X\x85\x11&\x1eR;(M\x1e\x85\x8c\x83#$\x018p\x1d\xa6\xf5\xd71\xff\xa44w,\alk\x10\xb1\xbe\x05\f0\xfb\xbb\x89\xb2\x8e\xd2\x14y\x8e\x14`\xb1\xc7\n\xb5\xbf>@u\x89\xa4\x1cJ\x06\xe6\x98UB\xab\x15\x92\xcf\x12\at\xf4\xe9\xed\xe71\x9b\x01|0\x0e\xf0\x8b\xa8l\x89נ\xa2\x95\x0f\t\xb5q\x10E\xd1\x10\az\xb0S~\xad\xc6\x15\x17\xecHI\xe1]Pԋ\r\x82I\x8a\xd6\b\xa5\xda\xe0\x1c\xde\x04Xv\x14\xf1\x17\x8e\x86_ߌ\xd2\xfcS\f\xd27\xbc\xe4M\x14\xecP3\xdbAt\x140F\x92SE\x81\r\x1e\xeb\u007f\xa1\x10p\x82\xfd\x16\x8ccݵi\x11\bd\xf9\xccb\xa2C9\x10\xf8\xd3\xdb\xcf'\xa4\xed\xda\t\x94\x96\xf8\x05ނJ7\x1ck\xe4\xb7\x19<\x05\x8f\xd8k/\xbe0\x9f|m\b5\x18]\xeeǥ5\xb0\x16[\x042|[²\x9cF\xac\"a'\xf6\xac\u007fs\\\xeca\x02\xacp\xbe\x8bFF\xa9>}\xbc\xfd8\x8fR\xb1\v\x15!\x93r\x95[)\xc6\x1c\f6b\xe5d\x9f\f樣sx\x03\xf9Z\xe8\x91\xc4\n\x01\xb4\x04\xeb\xaej\xaee\xd9\xd5k\xa3\xb5\x0f\x1b\x9ao\x04>\xf4\x13\xc3\xff\xa9\b_\xa4V\x80\xee/\xaaվ\x81\x9cUkS/\xd1i\xf4\x184\x93&'V*G\xebif\xb6\xe8\xb6\nw\xb3\x9dq\x1b\xa5\x8b);\xe24z\x02\xcd\u00ad`\xf6M\xf8\xf3\x9b\xb4\b`\xfd2U:w쯩\x0f\xf3\xa1٫\xd5ip\xe5\xa5U\xe9j\x91\x90O\u007f'\x87\xc4n\xad\xf2usI8f\xcf\xd1\x18\xa9\x84\x8c)W\xe8\xfdWw[6d\xedX\x9e\xfd4u\xac\xa6BK\xfe\x9f\x14y\x1e\u007f\xb5\xe5juA\x90\xfe|w\xfb\xc78s\xad^\x1d\x91\xa3\x808\xfaD\xbbgq\x16N=v\x966\xc0n\x04I\x1e\xd6\\\x8c\xe4\xbc(\x06\x00\xaa\xdd\xfa;\r\xb2\xce\xe8\xdc\xed\xbc\x89\x82@8\x04\x01\x95\xb0|N\x1b\xdcOc\x91\xb6Bq\x8d\xe52z\xecn\bkK5RNS)Np1!o\xbe֊\x82\xc6\xf5\x1d\xb1z\xdc}\xd6ک]9\x02\x9f\x13\xeb\x88K\x0e\x10\xba\xdd\xc2\x1a\xfa\xef\x00\xb8\x9e\xb0\x1b\xdf\x02\x19]\xb5E\x9b\x8e_]:+\x18\xd2w\x06\xac\x91\x9d\xdf#\xbd\xb1f\xaaէ;c\xb6\xd8a\xbd\xf8\xfe\x16ۻ\xc9z1\x1f\xf8\xa6\xe9\xcbX\xe1\xb7\xdc\xe0r\xc3ر\xdb\xd1>w\x847\xc3\xf5\xa1!\xe2d\x14˫\x8a\xfd\xb1\xd5\x05N\x1c\x86\x970h\x11\x8b\xfbB\xdeeZ(\x03\xb4cԹ\x12\xaaD\tM;\xbd\xbfg@\xb3Mc\x89+NU\xb5-\x8d\x90ͥ(\x89\xd64y\x9e\xf86\x1c\xfa\rWt\x92bM(\xc3-yD\xfd~yX\x19W\t\x1f\xbbz\xd3\x11\x82\xba.K\xb1,q\x0e\xde\xd5\xfdɓ\xa1_!\x91(·\xd7OqM\xbc\x1f\xa6\r \x96\xa6\xf6\x87\vb'į(y\xcf\xe5\xb7ӑ+X\xd7e\x05\x03fJ\xf0\xb1,ÎvX\x1f\xdf[\x82O \xf5Z\x9f\x1b9x\xf0\xbf\x01\x00\x00\xff\xff-\xbc\x85&\xc9\x1f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Y_\x8f\x1b\xb7\x11\u007fק\x18\\\x1e\xae\x01\xacU\xec\x14E\xa17\xe7\\\x17\xd7&g\xe1t\xf1\x8b\xe1\aj9Ҳ\xda%\xb7\xe4\xacd5\xc8w/\x86\xe4\xfe_ɺC\x9d\xee\x8b-r8\xf3\x9b\xe1\xfc\xe3\xdcl>\x9f\xcfD\xa9>\xa2u\xca\xe8%\x88R\xe1\x17BͿ\\\xb2\xff\xabK\x94Y\x1c^o\x90\xc4\xeb\xd9^i\xb9\x84\xbbʑ)\x1eљʦ\xf8\x0e\xb7J+RF\xcf\n$!\x05\x89\xe5\f@hmH\xf0\xb2\xe3\x9f\x00\xa9\xd1dM\x9e\xa3\x9d\xefP'\xfbj\x83\x9bJ\xe5\x12\xad\x97P\xcb?\xfc\x90\xfc\x98\xfc0\x03H-\xfa\xe3O\xaa@G\xa2(\x97\xa0\xab<\x9f\x01hQ\xe0\x12J#\x0f&\xaf\n\xb4\xe8\xc8Xt\xc9\x01s\xb4&Qf\xe6JL=\b)=2\x91\xaf\xac҄\xf6\x8eO\x04Ds\xf8\xc7\xfa\xc3\xc3JP\xb6\x84đ\xa0\xca%e&\x1cz\xb4\x12]jUI\x1e\xd3c\x90\x00\x81\n\\\x95f \x1c\xdc\xeb\x955;\x8b\xce-\xeeLQ\xe6H(\xfd\xe1\x00p\xed\xa9\xfd\x02\x9dJ\\\x82#\xab\xf4n$\xba\xc44)\x8dL\xf8\x94+E:\x01\xe0\xa1\xde\x02\xb3eŽ1\x85\xd2J\xef\x802\x84`\b \x03\x1b\x84h\x8f.\x94\x87\x1e\xeb\xab\xd1L\x03y!\x86\x95\x91\xd7I\x0f\x8c\xce\xcbn\x05E)\x1d!\x1f۳\x17\xe5\xc4\xeb\x8eחlN\x84\xee\x9d\xd1SR\xabb\x83\x96\xe5z\xa2)\xcd~\xf2\x1b\xcd\xf1\xe7\b&C\"\xf7\xe7ǒ\x9fx\x0f\xf4@\xfeY\xfb\x06\xf2\x96\xd7\x150\x1c\tKM|M P\x05\xc21C\xed/8\xca\x04S\xa2\xf5\x91\tG\xe1\xc0\xf3\x18\xfa}\xb3\x12@HAx\x06B\x1a\x02\xa7\x1b\xe7/ÑND`?*\xcfc\xa9\xf3V2\xca9\x1dfow8f\xb3\xb3\xa6*\x97\xd0f\x9e@\x1dS^H\x97+#\x83[>v\xdc5W\x8e\xfe9\xb9\xfd\xb3r\xe4Iʼ\xb2\"\x9fHt~\xd7)\xbd\xabra\xc7\xfb3\x80ҢC{\xc0_\xf5^\x9b\xa3~\xaf0\x97n\t[\x91\xfb\xf4\xe6RSv\xb3\x02\xdb\xc7U\x1b\x1b3\xba[\xc2o\xbf\xcf\x00\x0e\"W\xd2\xdb#\xa8cJ\xd4oW\xf7\x1f\u007f\\\xa7\x19\x16\",\xb20\xbe\tR\xb5\xd6\xfcu*J\xb36\xb8\xd4[f\x15h@r\ra\xe7\xe6<\x12\xd6P\x82\xf3b\xd8\xf5)S\x1cy^-\x1d\xaaJ\x87-0\x89\xd0`6\xff\u0094\x12X\xb3\xeaց\xcbL\x95\xfb}x\xf7a\x19P\xb1\v\xed|\x1e\xe3*\xb3U\\\xf3\xb9؇\xca\xc5>\xe9\xcdQ\x05\xe7 \x03i&\xf4DZ\x03\xdf4x\xebn+\xae%\xc9\xeds\xa3uX\xb6\xebo\xa2|\x0f\x13\xc3\xff\xa9\b^\xa5\x96o\x9f\xbf\xaa\xd6Cǟ/\xaa\xb5\xaf6h5\x12zͤI\x1d+\x95bIna\x0eh\x0f\n\x8f\x8b\xa3\xb1{\xa5wsv\xc4y\xf0\x04\xb7\xf0\x9d\xf9\xe2;\xffϋ\xb4\xf0\xcd\xf2u\xaa4\xaf\xf5o\xad\x0f\xcbq\x8bg\xabS\xf7u\xd7V\xa5\xdbul<\x86'9$\x8e\x99J\xb3\xbaIo\xb3\xe7d\x8c\x14B\x86\x94+\xf4電-\x1b\xb2\xb2\x8c\xe74\x8f\xf3\xa7\xb9В\xff\xef\x94#^\u007f\xb6\xe5*uE\x90\xfez\xff\xee\x8fq\xe6J=;\"'\x1b\xd2\xe0\x13\xa5\xb9\x97l\xbe\xadB{\xb1\x9bz\xec\x91\xd6]\xe0D\x1f\xd7\xd0\\\xdd\xc89-J\x97\x19\xba\u007fw\x11\xc1\xba!\xab\xa5\xb7&\x8f\xed[\xcdi03\xb9\x1aI`s\x11E軧\xba\xe0\x88!t\f~\x85;\xd0\x17!\xe1\xe7\x10\xb79]$\xf3\xe9\x0e\xbeGQ\x1a\xd9\xfbݿ\xdf\xdeVk\xf4\xderg\x14\xd7E8|\xcc\xf8Q\xce\xf5ϙ0E\x8d6\v\xf1I\xf5l\x95k\xf7\x8b\x1e4\x13\x83\xa4\x8b7w7\xa6\xf7\x13\x02+\x03.R\x05\xfa\xd7B\x186\xf5FL㦢\xe5\x16\x0e\xfaL\xc8\xccP\xfaf\x8b\xfb\xc0\xadP9Jh\xc6\xd5\xf0\xc4\xef9\xffd\xbe\x1d\xe7ʚM\xe5P\xfaw\xde\x04\xe0ᩭ\xb1\x85\xa00\x9c\x9a3\x83\xc1\xbe\xae\xf2\\lr\\\x02\xd9j\xb8y6\f\ntN\xec.\xc7\xc1/\x81&\xbc\xb0\xe2\x01\x10\x1bSQ\xf3Ċ\x01\x11տu\xf1Ư\u007f\xe0e\xc2]\x06\xb1b\x8a)\xbfj\x82\xf2\x92c\x81\u007f\xbdT\xc5P\xc4\x1c\x1e\xf08Zkg\xff\xa3\xad\xfeԱ\xbb\xf3\xde{\xc0\xd5\nG\x01\x97u\x8eD\x90\x99\xbc\xf6\xdcɹq\xb4@\x1d\xe8\xe3\x17\xaa\xefy[\xbb\xb5\xe7\xebl\x15\x18\xc5\x0e>\x15ڏd\xd8;ɀT\xae\xccŸ\x85\xafu\xf0e\x8f\x9d\x93#\xa4\xf5\x8b\xd1(\xf79o\xeafh?U\x8e\xebPP\x9a\xfe\xf2\xe7\xb3\xf5Qi\xc2]/\x15\xc6\xddf,\xff\xbf\xe6}\xb6\xf8\xf6g\xf1\x97K_\x8f\xf4kY+\x0e\xe8\xc79\xab\x9b~\xc6\xe9\xa6/\xe4\x8f\xc84\x13\xa6\x19,\xb5\u007f$|\xdd\xfe\x8a\u007f\xcc\vCz\xbf\x01A-\xd9\x11\x1e\x87Qq\xa5-X\"\xe5^\v\xe5\xc3ph\u007fsӛ\xc1\xfb\x9f\xa9\xd1\xe1ωn\t\x9f>\xcf \x8e\xa8>\xd68x\xf1\xbf\x01\x00\x00\xff\xff\a\x89\x1b\xd1P\x1d\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WM\x8f\xe36\x0f\xbe\xe7W\x10\xfb\x1e\xf6\xf2\xc6\xd9\xc5^\n\xdf\xdai\x17\xd8vf:\x98l\xe7R\xf4@\xcbt\xa2\x8e,\xb9\"\x95iZ\xf4\xbf\x17\x92\xec\xc4q\x9c\xec`\x81\xfa&\x8a\x12\x1f>\xfc\x10\xbdX.\x97\v\xec\xf4\x13y\xd6Ζ\x80\x9d\xa6?\x85l\\q\xf1\xfc\r\x17ڭv\xef+\x12|\xbfxֶ.\xe1&\xb0\xb8\xf6\x91\xd8\x05\xaf\xe8{j\xb4բ\x9d]\xb4$X\xa3`\xb9\x00@k\x9d`\x14s\\\x02(g\xc5;c\xc8/7d\x8b\xe7PQ\x15\xb4\xa9\xc9'\v\x83\xfdݻ\xe2C\xf1n\x01\xa0<\xa5\xe3\x9fuK,\xd8v%\xd8`\xcc\x02\xc0bK%xb\xd1\xcaS\xe7X\x8b\xf3\x9a\xb8ؑ!\xef\n\xed\x16ܑJ(\xea:AC\xf3\xe0\xb5\x15\xf27΄6CZ\u008f\xeb\x9f\xef\x1fP\xb6%\x14,(\x81\x8bn\x8bL\tnM\xac\xbc\xee$\x81zL\xb6\xe0q0\xb6\x87\xac\x0f\x1c\xd4\x16\x90\xe1\x9e^V\x8f\x84\xf5>\x9d\xcd\x00\xd7I%\td\xdfQ\t,^\xdb\xcd\x05\xcb\x06Y\xee0b\xb4h\x15E\xaf\xcfq\xdc\"\v\x88n\tڣ*\xbc \x83\x0fvd:\xe9\x8dn\x1b\x81\xa8Q\xe2r\xe3]\xe8J82\x96\x8f\xf6\xb1\xcaq\xce^\x1f\x9dN[F\xb3\xfc4\xbb}\xabY\x92Jg\x82G3\x17\xa1\xb4\xcd\xdan\x82A\u007f\xa6\x10\rt\x9e\x98\xfc\x8e~\xb1\xcfֽ؏\x9aL\xcd%4hR\\X\xb9\xe8\xc4}\x84ڡ\xa2:\xcaB\xe5\xfb\\\xe4\x12\xfe\xfeg\x01\xb0C\xa3\xeb\x94=\xd9\x1fב\xfd\xf6\xe1\xd3Ӈ\xb5\xdaR\x8bY\x18\x8d\xb9\x8e\xbc\xe8\xc1\xed\xf8\x8dj\xe1 \x9bD\xe1m\xbc*\xeb@\x1d\xb3\x9f\x18dK\xd0\xe70\xd5\xc0\xc9\f\xb8\x06d\xab\x19<%\xb7l\xae\x87ѵ\x10UЂ\xab~'%\x05\xac\xa3랁\xb7.\x98:\x96̎\xbc\x80'\xe56V\xffu\xb8\x99A\\2iP\xa8g}\xf8R\x9a[4\x91\x84@\xff\a\xb45\xb4\xb8\aO\xd1\x06\x04;\xba-\xa9p\x01w\xce\x13h۸\x12\xb6\"\x1d\x97\xab\xd5F\xcbP\xfdʵm\xb0Z\xf6\xabTú\n\xe2<\xafjڑY\xb1\xde,ѫ\xad\x16R\x12<\xad\xb0\xd3\xcb\x04ܦ\xe2/\xda\xfa\u007f\x87\xf0\xbc\x1d!\x9d\xd4D\xfeR\xe2]\xe4=\xe6\x1dh\x06\xec\x8fe\xfcGz\xa3(\xb2\xf2\xf8\xc3\xfa3\fFS\bN9Ol\x1f\x8f\xf1\x91\xf8H\x94\xb6\r\xf9\x1c\xb8ƻ6\xddH\xb6\ue736\x92\x16\xcah\xb2\xa7\xa4s\xa8Z-1\xd2\u007f\x04b\x89\xf1)\xe0&\xf5@\xa8\bB\x17ˮ.\xe0\x93\x85\x1bl\xc9\xdc \xd3\u007fN{d\x98\x97\x91\xd2/\x13?nݧ\x8a\x99\xad\x83x謳\x11\x9a\xb6\x84uG*\x06,\xb2\x16\x0f\xeaF\xabT\x03\xd08\x0fx\xa6_\x8c.\x9e+\xce\xf8U\xa8\x9eC\xb7\x16\xe7qC\xb7N\x8d\xca\xfc\x02\xaa\xef\xe6N\f\xb0b\xd7˅J\U000ca4db\x01d\x8b2\xaaPAm\x0fe>\xe3\xc7E\xca\x13\xed\xc7\x1e\xfd1\xe5\x8eU\xfb\xab\xbe\xdc\xcd\x1c\x88\xael\xdd\v\xb8FȞ\xbc\f=ʊΜ\xf0\xc1\xbe\x1adnӟ\xea\x98Z\x8d&\u007f\x15\xe0\xe3Dy\xe0\xb9\t\xc6\xf47-\x95k;\x14]\x19\x1a\n\xb9q\xfe\f\xa2\xcew\xecsU\u007f\x1d\xbf\xbb\xf8\xdc\xd3Ṹ\x8a\xfc\xe9Tw\x9c YЃH\xa3\x80?}\x15\xc7_\x9f\x13\f\x9d\xab{\x00}\xd2r\xf4\xf3\x95\xd8cp\xb5\xa7\x93n\xb8\x9cO\xfe\x13\x8d\xb9\x8c:Q\x98F\xf3ds\xc2\xd7\x17\x9bA\x9a]^\xdf\x0e\xf2\xd0\xd4\x13\xab\x82\xf7de\x18\xa5\xe2K\xf8U\rafr\xba\x1a\xe7\xdbs\xfd\x01\x92\xb96_M\x03\xd78ߢ\xe4\x89j)üv\xfcⴊ\x95\xa1\x12ć\xe9\xe6\xe5\x8e@̸\xb9\xee\xc1]\xd6\xc9Oa\u007f\x00\xb0rA.\x10\x9b\x1e\xc5+\xd4^E\x94f\xe2\xabx\x1e\xa2\xc6\\X\xe9\xb5\xc6Ɇvjb\x19\xa7\xea3\xd9q\xca>\xd1t2\xb7q\xc1\xa7\x99\\\x9e\x88\x8e\xbf#\uf3eb\xfe\xaf!\x8f\xd8i\x03 \r\xab\xf5(Ĝk\xb3\x97\x1c\v\x04\x95\xa2N\xa8\xbe\x9fN\xd9oޜ\f\xcdi\xa9\x9c\xcd\xff-\\¯\xbf-\xf2\xadT?\r8\xa2\xf0\xdf\x00\x00\x00\xff\xff\"c\x04\x9d\xba\r\x00\x00"), diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7d2c8aafc..c26177569 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -26,6 +26,26 @@ rules: - get - patch - update +- apiGroups: + - velero.io + resources: + - downloadrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - velero.io + resources: + - downloadrequests/status + verbs: + - get + - patch + - update - apiGroups: - velero.io resources: diff --git a/internal/util/managercontroller/managercontroller.go b/internal/util/managercontroller/managercontroller.go index be7391e2b..bb8082606 100644 --- a/internal/util/managercontroller/managercontroller.go +++ b/internal/util/managercontroller/managercontroller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO(2.0) After converting all controllers to runttime-controller, +// TODO(2.0) After converting all controllers to runtime-controller, // the functions in this file will no longer be needed and should be removed. package managercontroller diff --git a/pkg/apis/velero/v1/backupstoragelocation_types.go b/pkg/apis/velero/v1/backupstoragelocation_types.go index d891071e3..46b6b17b9 100644 --- a/pkg/apis/velero/v1/backupstoragelocation_types.go +++ b/pkg/apis/velero/v1/backupstoragelocation_types.go @@ -90,7 +90,7 @@ type BackupStorageLocationStatus struct { AccessMode BackupStorageLocationAccessMode `json:"accessMode,omitempty"` } -// TODO(2.0) After converting all resources to use the runttime-controller client, +// TODO(2.0) After converting all resources to use the runtime-controller client, // the genclient and k8s:deepcopy markers will no longer be needed and should be removed. // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -116,7 +116,7 @@ type BackupStorageLocation struct { Status BackupStorageLocationStatus `json:"status,omitempty"` } -// TODO(2.0) After converting all resources to use the runttime-controller client, +// TODO(2.0) After converting all resources to use the runtime-controller client, // the k8s:deepcopy marker will no longer be needed and should be removed. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:object:root=true diff --git a/pkg/apis/velero/v1/download_request.go b/pkg/apis/velero/v1/download_request_types.go similarity index 85% rename from pkg/apis/velero/v1/download_request.go rename to pkg/apis/velero/v1/download_request_types.go index aaa0f282a..8b6014969 100644 --- a/pkg/apis/velero/v1/download_request.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -1,5 +1,5 @@ /* -Copyright 2017 the Velero contributors. +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. @@ -77,8 +77,14 @@ type DownloadRequestStatus struct { Expiration *metav1.Time `json:"expiration,omitempty"` } +// TODO(2.0) After converting all resources to use the runtime-controller client, +// the k8s:deepcopy marker will no longer be needed and should be removed. // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="DownloadRequest status such as New/Processed" // +kubebuilder:printcolumn:name="Target Name",type="string",JSONPath=".spec.target.name",description="Name of the associated Kubernetes resource" // +kubebuilder:printcolumn:name="Target Kind",type="string",JSONPath=".spec.target.kind",description="Type of file to download" @@ -99,7 +105,12 @@ type DownloadRequest struct { Status DownloadRequestStatus `json:"status,omitempty"` } +// TODO(2.0) After converting all resources to use the runtime-controller client, +// the k8s:deepcopy marker will no longer be needed and should be removed. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests/status,verbs=get;update;patch // DownloadRequestList is a list of DownloadRequests. type DownloadRequestList struct { diff --git a/pkg/apis/velero/v1/server_status_request_types.go b/pkg/apis/velero/v1/server_status_request_types.go index 917105b14..41f44e575 100644 --- a/pkg/apis/velero/v1/server_status_request_types.go +++ b/pkg/apis/velero/v1/server_status_request_types.go @@ -20,7 +20,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// TODO(2.0) After converting all resources to use the runttime-controller client, +// TODO(2.0) After converting all resources to use the runtime-controller client, // the genclient and k8s:deepcopy markers will no longer be needed and should be removed. // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -89,7 +89,7 @@ type ServerStatusRequestStatus struct { Plugins []PluginInfo `json:"plugins,omitempty"` } -// TODO(2.0) After converting all resources to use the runttime-controller client, +// TODO(2.0) After converting all resources to use the runtime-controller client, // the k8s:deepcopy marker will no longer be needed and should be removed. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:object:root=true diff --git a/pkg/builder/download_request_builder.go b/pkg/builder/download_request_builder.go new file mode 100644 index 000000000..05d5c0daa --- /dev/null +++ b/pkg/builder/download_request_builder.go @@ -0,0 +1,62 @@ +/* +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 builder + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +// DownloadRequestBuilder builds DownloadRequest objects. +type DownloadRequestBuilder struct { + object *velerov1api.DownloadRequest +} + +// ForDownloadRequest is the constructor for a DownloadRequestBuilder. +func ForDownloadRequest(ns, name string) *DownloadRequestBuilder { + return &DownloadRequestBuilder{ + object: &velerov1api.DownloadRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: velerov1api.SchemeGroupVersion.String(), + Kind: "DownloadRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + }, + } +} + +// Result returns the built DownloadRequest. +func (b *DownloadRequestBuilder) Result() *velerov1api.DownloadRequest { + return b.object +} + +// Phase sets the DownloadRequest's status phase. +func (b *DownloadRequestBuilder) Phase(phase velerov1api.DownloadRequestPhase) *DownloadRequestBuilder { + b.object.Status.Phase = phase + return b +} + +// Target sets the DownloadRequest's target kind and target name. +func (b *DownloadRequestBuilder) Target(targetKind velerov1api.DownloadTargetKind, targetName string) *DownloadRequestBuilder { + b.object.Spec.Target.Kind = targetKind + b.object.Spec.Target.Name = targetName + return b +} diff --git a/pkg/cmd/cli/backup/describe.go b/pkg/cmd/cli/backup/describe.go index 0f8d302b4..f266d6467 100644 --- a/pkg/cmd/cli/backup/describe.go +++ b/pkg/cmd/cli/backup/describe.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -56,6 +56,9 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { veleroClient, err := f.Client() cmd.CheckError(err) + kbClient, err := f.KubebuilderClient() + cmd.CheckError(err) + var backups *velerov1api.BackupList if len(args) > 0 { backups = new(velerov1api.BackupList) @@ -99,7 +102,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { } } - s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) + s := output.DescribeBackup(context.Background(), kbClient, &backup, deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) if first { first = false fmt.Print(s) diff --git a/pkg/cmd/cli/backup/download.go b/pkg/cmd/cli/backup/download.go index b6f787caa..464be2579 100644 --- a/pkg/cmd/cli/backup/download.go +++ b/pkg/cmd/cli/backup/download.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -28,7 +28,7 @@ import ( "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" @@ -115,7 +115,7 @@ func (o *DownloadOptions) Complete(args []string) error { } func (o *DownloadOptions) Run(c *cobra.Command, f client.Factory) error { - veleroClient, err := f.Client() + kbClient, err := f.KubebuilderClient() cmd.CheckError(err) backupDest, err := os.OpenFile(o.Output, o.writeOptions, 0600) @@ -124,7 +124,7 @@ func (o *DownloadOptions) Run(c *cobra.Command, f client.Factory) error { } defer backupDest.Close() - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), o.Name, v1.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify, o.caCertFile) + err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), o.Name, velerov1api.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify, o.caCertFile) if err != nil { os.Remove(o.Output) cmd.CheckError(err) diff --git a/pkg/cmd/cli/backup/logs.go b/pkg/cmd/cli/backup/logs.go index 02c4a5a7d..554fa1713 100644 --- a/pkg/cmd/cli/backup/logs.go +++ b/pkg/cmd/cli/backup/logs.go @@ -1,5 +1,5 @@ /* -Copyright 2017 the Velero contributors. +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. @@ -26,7 +26,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" @@ -52,6 +52,9 @@ func NewLogsCommand(f client.Factory) *cobra.Command { veleroClient, err := f.Client() cmd.CheckError(err) + kbClient, err := f.KubebuilderClient() + cmd.CheckError(err) + backup, err := veleroClient.VeleroV1().Backups(f.Namespace()).Get(context.TODO(), backupName, metav1.GetOptions{}) if apierrors.IsNotFound(err) { cmd.Exit("Backup %q does not exist.", backupName) @@ -60,14 +63,14 @@ func NewLogsCommand(f client.Factory) *cobra.Command { } switch backup.Status.Phase { - case v1.BackupPhaseCompleted, v1.BackupPhasePartiallyFailed, v1.BackupPhaseFailed: + case velerov1api.BackupPhaseCompleted, velerov1api.BackupPhasePartiallyFailed, velerov1api.BackupPhaseFailed: // terminal phases, do nothing. default: cmd.Exit("Logs for backup %q are not available until it's finished processing. Please wait "+ "until the backup has a phase of Completed or Failed and try again.", backupName) } - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), backupName, v1.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) + err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), backupName, velerov1api.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) cmd.CheckError(err) }, } diff --git a/pkg/cmd/cli/restore/describe.go b/pkg/cmd/cli/restore/describe.go index 1796fb6d2..3a8c797a6 100644 --- a/pkg/cmd/cli/restore/describe.go +++ b/pkg/cmd/cli/restore/describe.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -24,7 +24,7 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" @@ -51,9 +51,12 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { veleroClient, err := f.Client() cmd.CheckError(err) - var restores *api.RestoreList + kbClient, err := f.KubebuilderClient() + cmd.CheckError(err) + + var restores *velerov1api.RestoreList if len(args) > 0 { - restores = new(api.RestoreList) + restores = new(velerov1api.RestoreList) for _, name := range args { restore, err := veleroClient.VeleroV1().Restores(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{}) cmd.CheckError(err) @@ -72,7 +75,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting PodVolumeRestores for restore %s: %v\n", restore.Name, err) } - s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) + s := output.DescribeRestore(context.Background(), kbClient, &restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) if first { first = false fmt.Print(s) diff --git a/pkg/cmd/cli/restore/logs.go b/pkg/cmd/cli/restore/logs.go index 7cc6c37d3..66514144c 100644 --- a/pkg/cmd/cli/restore/logs.go +++ b/pkg/cmd/cli/restore/logs.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -26,7 +26,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" @@ -52,6 +52,9 @@ func NewLogsCommand(f client.Factory) *cobra.Command { veleroClient, err := f.Client() cmd.CheckError(err) + kbClient, err := f.KubebuilderClient() + cmd.CheckError(err) + restore, err := veleroClient.VeleroV1().Restores(f.Namespace()).Get(context.TODO(), restoreName, metav1.GetOptions{}) if apierrors.IsNotFound(err) { cmd.Exit("Restore %q does not exist.", restoreName) @@ -60,14 +63,14 @@ func NewLogsCommand(f client.Factory) *cobra.Command { } switch restore.Status.Phase { - case v1.RestorePhaseCompleted, v1.RestorePhaseFailed, v1.RestorePhasePartiallyFailed: + case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed: // terminal phases, don't exit. default: cmd.Exit("Logs for restore %q are not available until it's finished processing. Please wait "+ "until the restore has a phase of Completed or Failed and try again.", restoreName) } - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), restoreName, v1.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) + err = downloadrequest.Stream(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) cmd.CheckError(err) }, } diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 08d80a645..c8e433433 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -731,24 +731,6 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string } } - downloadrequestControllerRunInfo := func() controllerRunInfo { - downloadRequestController := controller.NewDownloadRequestController( - s.veleroClient.VeleroV1(), - s.sharedInformerFactory.Velero().V1().DownloadRequests(), - s.sharedInformerFactory.Velero().V1().Restores().Lister(), - s.mgr.GetClient(), - s.sharedInformerFactory.Velero().V1().Backups().Lister(), - newPluginManager, - persistence.NewObjectBackupStoreGetter(), - s.logger, - ) - - return controllerRunInfo{ - controller: downloadRequestController, - numWorkers: defaultControllerWorkers, - } - } - enabledControllers := map[string]func() controllerRunInfo{ controller.BackupSync: backupSyncControllerRunInfo, controller.Backup: backupControllerRunInfo, @@ -757,12 +739,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string controller.BackupDeletion: deletionControllerRunInfo, controller.Restore: restoreControllerRunInfo, controller.ResticRepo: resticRepoControllerRunInfo, - controller.DownloadRequest: downloadrequestControllerRunInfo, } // Note: all runtime type controllers that can be disabled are grouped separately, below: - enabledRuntimeControllers := map[string]struct{}{ - controller.ServerStatusRequest: {}, - } + enabledRuntimeControllers := make(map[string]struct{}) + enabledRuntimeControllers[controller.ServerStatusRequest] = struct{}{} + enabledRuntimeControllers[controller.DownloadRequest] = struct{}{} if s.config.restoreOnly { s.logger.Info("Restore only mode - not starting the backup, schedule, delete-backup, or GC controllers") @@ -839,6 +820,20 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string } } + if _, ok := enabledRuntimeControllers[controller.DownloadRequest]; ok { + r := controller.DownloadRequestReconciler{ + Scheme: s.mgr.GetScheme(), + Client: s.mgr.GetClient(), + Clock: clock.RealClock{}, + NewPluginManager: newPluginManager, + BackupStoreGetter: persistence.NewObjectBackupStoreGetter(), + Log: s.logger, + } + if err := r.SetupWithManager(s.mgr); err != nil { + s.logger.Fatal(err, "unable to create controller", "controller", controller.DownloadRequest) + } + } + // TODO(2.0): presuming all controllers and resources are converted to runtime-controller // by v2.0, the block from this line and including the `s.mgr.Start() will be // deprecated, since the manager auto-starts all the caches. Until then, we need to start the diff --git a/pkg/cmd/util/downloadrequest/downloadrequest.go b/pkg/cmd/util/downloadrequest/downloadrequest.go index f302ba8fd..73b9b392b 100644 --- a/pkg/cmd/util/downloadrequest/downloadrequest.go +++ b/pkg/cmd/util/downloadrequest/downloadrequest.go @@ -1,5 +1,5 @@ /* -Copyright 2017 the Velero contributors. +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. @@ -29,74 +29,50 @@ import ( "time" "github.com/pkg/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" + "k8s.io/apimachinery/pkg/util/wait" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" ) // ErrNotFound is exported for external packages to check for when a file is // not found var ErrNotFound = errors.New("file not found") -func Stream(client velerov1client.DownloadRequestsGetter, namespace, name string, kind v1.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool, caCertFile string) error { - req := &v1.DownloadRequest{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: fmt.Sprintf("%s-%s", name, time.Now().Format("20060102150405")), - }, - Spec: v1.DownloadRequestSpec{ - Target: v1.DownloadTarget{ - Kind: kind, - Name: name, - }, - }, - } +func Stream(ctx context.Context, kbClient kbclient.Client, namespace, name string, kind velerov1api.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool, caCertFile string) error { + reqName := fmt.Sprintf("%s-%s", name, time.Now().Format("20060102150405")) + created := builder.ForDownloadRequest(namespace, reqName).Target(kind, name).Result() - req, err := client.DownloadRequests(namespace).Create(context.TODO(), req, metav1.CreateOptions{}) - if err != nil { + if err := kbClient.Create(context.Background(), created, &kbclient.CreateOptions{}); err != nil { return errors.WithStack(err) } - defer client.DownloadRequests(namespace).Delete(context.TODO(), req.Name, metav1.DeleteOptions{}) - listOptions := metav1.ListOptions{ - FieldSelector: "metadata.name=" + req.Name, - ResourceVersion: req.ResourceVersion, - } - watcher, err := client.DownloadRequests(namespace).Watch(context.TODO(), listOptions) - if err != nil { - return errors.WithStack(err) - } - defer watcher.Stop() + ctx, cancel := context.WithCancel(ctx) + defer cancel() - expired := time.NewTimer(timeout) - defer expired.Stop() + key := kbclient.ObjectKey{Name: created.Name, Namespace: namespace} + checkFunc := func() { + updated := &velerov1api.DownloadRequest{} + if err := kbClient.Get(ctx, key, updated); err != nil { + return + } -Loop: - for { - select { - case <-expired.C: - return errors.New("timed out waiting for download URL") - case e := <-watcher.ResultChan(): - updated, ok := e.Object.(*v1.DownloadRequest) - if !ok { - return errors.Errorf("unexpected type %T", e.Object) - } + // TODO: once the minimum supported Kubernetes version is v1.9.0, remove the following check. + // See http://issue.k8s.io/51046 for details. + if updated.Name != created.Name { + return + } - switch e.Type { - case watch.Deleted: - errors.New("download request was unexpectedly deleted") - case watch.Modified: - if updated.Status.DownloadURL != "" { - req = updated - break Loop - } - } + if updated.Status.DownloadURL != "" { + created = updated + cancel() } } - if req.Status.DownloadURL == "" { + wait.Until(checkFunc, 25*time.Millisecond, ctx.Done()) + + if created.Status.DownloadURL == "" { return ErrNotFound } @@ -134,7 +110,7 @@ Loop: ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, } - httpReq, err := http.NewRequest("GET", req.Status.DownloadURL, nil) + httpReq, err := http.NewRequest("GET", created.Status.DownloadURL, nil) if err != nil { return err } @@ -164,7 +140,7 @@ Loop: } reader := resp.Body - if kind != v1.DownloadTargetKindBackupContents { + if kind != velerov1api.DownloadTargetKindBackupContents { // need to decompress logs gzipReader, err := gzip.NewReader(resp.Body) if err != nil { diff --git a/pkg/cmd/util/downloadrequest/downloadrequest_test.go b/pkg/cmd/util/downloadrequest/downloadrequest_test.go deleted file mode 100644 index d6940fadc..000000000 --- a/pkg/cmd/util/downloadrequest/downloadrequest_test.go +++ /dev/null @@ -1,225 +0,0 @@ -/* -Copyright 2017 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 downloadrequest - -import ( - "bytes" - "compress/gzip" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - core "k8s.io/client-go/testing" - - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake" -) - -func TestStream(t *testing.T) { - tests := []struct { - name string - kind v1.DownloadTargetKind - timeout time.Duration - createError error - watchError error - watchAdds []runtime.Object - watchModifies []runtime.Object - watchDeletes []runtime.Object - updateWithURL bool - statusCode int - body string - deleteError error - expectedError string - }{ - { - name: "error creating req", - createError: errors.New("foo"), - kind: v1.DownloadTargetKindBackupLog, - expectedError: "foo", - }, - { - name: "error creating watch", - watchError: errors.New("bar"), - kind: v1.DownloadTargetKindBackupLog, - expectedError: "bar", - }, - { - name: "timed out", - kind: v1.DownloadTargetKindBackupLog, - timeout: time.Millisecond, - expectedError: "timed out waiting for download URL", - }, - { - name: "unexpected watch type", - kind: v1.DownloadTargetKindBackupLog, - watchAdds: []runtime.Object{&v1.Backup{}}, - expectedError: "unexpected type *v1.Backup", - }, - { - name: "other requests added/updated/deleted first", - kind: v1.DownloadTargetKindBackupLog, - watchAdds: []runtime.Object{ - newDownloadRequest("foo").DownloadRequest, - }, - watchModifies: []runtime.Object{ - newDownloadRequest("foo").DownloadRequest, - }, - watchDeletes: []runtime.Object{ - newDownloadRequest("foo").DownloadRequest, - }, - updateWithURL: true, - statusCode: http.StatusOK, - body: "download body", - }, - { - name: "http error", - kind: v1.DownloadTargetKindBackupLog, - updateWithURL: true, - statusCode: http.StatusInternalServerError, - body: "some error", - expectedError: "request failed: some error", - }, - } - - const testTimeout = 30 * time.Second - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - client := fake.NewSimpleClientset() - - created := make(chan *v1.DownloadRequest, 1) - client.PrependReactor("create", "downloadrequests", func(action core.Action) (bool, runtime.Object, error) { - createAction := action.(core.CreateAction) - created <- createAction.GetObject().(*v1.DownloadRequest) - return true, createAction.GetObject(), test.createError - }) - - fakeWatch := watch.NewFake() - client.PrependWatchReactor("downloadrequests", core.DefaultWatchReactor(fakeWatch, test.watchError)) - - deleted := make(chan string, 1) - client.PrependReactor("delete", "downloadrequests", func(action core.Action) (bool, runtime.Object, error) { - deleteAction := action.(core.DeleteAction) - deleted <- deleteAction.GetName() - return true, nil, test.deleteError - }) - - timeout := test.timeout - if timeout == 0 { - timeout = testTimeout - } - - var server *httptest.Server - var url string - if test.updateWithURL { - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(test.statusCode) - if test.statusCode == http.StatusOK { - gzipWriter := gzip.NewWriter(w) - fmt.Fprintf(gzipWriter, test.body) - gzipWriter.Close() - return - } - fmt.Fprintf(w, test.body) - })) - defer server.Close() - url = server.URL - } - - output := new(bytes.Buffer) - errCh := make(chan error) - go func() { - err := Stream(client.VeleroV1(), "namespace", "name", test.kind, output, timeout, false, "") - errCh <- err - }() - - for i := range test.watchAdds { - fakeWatch.Add(test.watchAdds[i]) - } - for i := range test.watchModifies { - fakeWatch.Modify(test.watchModifies[i]) - } - for i := range test.watchDeletes { - fakeWatch.Delete(test.watchDeletes[i]) - } - - var createdName string - if test.updateWithURL { - select { - case r := <-created: - createdName = r.Name - r.Status.DownloadURL = url - fakeWatch.Modify(r) - case <-time.After(testTimeout): - t.Fatalf("created object not received") - } - - } - - var err error - select { - case err = <-errCh: - case <-time.After(testTimeout): - t.Fatal("test timed out") - } - - if test.expectedError != "" { - require.EqualError(t, err, test.expectedError) - return - } - - require.NoError(t, err) - - if test.statusCode != http.StatusOK { - assert.EqualError(t, err, "request failed: "+test.body) - return - } - - assert.Equal(t, test.body, output.String()) - - select { - case name := <-deleted: - assert.Equal(t, createdName, name) - default: - t.Fatal("download request was not deleted") - } - }) - } -} - -type downloadRequest struct { - *v1.DownloadRequest -} - -func newDownloadRequest(name string) *downloadRequest { - return &downloadRequest{ - DownloadRequest: &v1.DownloadRequest{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: v1.DefaultNamespace, - Name: name, - }, - }, - } -} diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index 264a9f3ae..86d686ba2 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -1,5 +1,5 @@ /* -Copyright 2021 the Velero contributors. +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. @@ -18,6 +18,7 @@ package output import ( "bytes" + "context" "encoding/json" "fmt" "sort" @@ -28,6 +29,7 @@ import ( snapshotv1beta1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1beta1" "github.com/fatih/color" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" @@ -38,6 +40,8 @@ import ( // DescribeBackup describes a backup in human-readable format. func DescribeBackup( + ctx context.Context, + kbClient kbclient.Client, backup *velerov1api.Backup, deleteRequests []velerov1api.DeleteBackupRequest, podVolumeBackups []velerov1api.PodVolumeBackup, @@ -90,7 +94,7 @@ func DescribeBackup( DescribeBackupSpec(d, backup.Spec) d.Println() - DescribeBackupStatus(d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile) + DescribeBackupStatus(ctx, kbClient, d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile) if len(deleteRequests) > 0 { d.Println() @@ -241,7 +245,7 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) { } // DescribeBackupStatus describes a backup status in human-readable format. -func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { +func DescribeBackupStatus(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { status := backup.Status // Status.Version has been deprecated, use Status.FormatVersion @@ -280,7 +284,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool } if details { - describeBackupResourceList(d, backup, veleroClient, insecureSkipTLSVerify, caCertPath) + describeBackupResourceList(ctx, kbClient, d, backup, insecureSkipTLSVerify, caCertPath) d.Println() } @@ -291,7 +295,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool } buf := new(bytes.Buffer) - if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { + if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { d.Printf("Velero-Native Snapshots:\t\n", err) return } @@ -312,9 +316,9 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool d.Printf("Velero-Native Snapshots: \n") } -func describeBackupResourceList(d *Describer, backup *velerov1api.Backup, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { +func describeBackupResourceList(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) { buf := new(bytes.Buffer) - if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { + if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { if err == downloadrequest.ErrNotFound { // the backup resource list could be missing if (other reasons may exist as well): // - the backup was taken prior to v1.1; or diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index c3b26c62a..03b3597a4 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019 the Velero contributors. +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. @@ -18,40 +18,42 @@ package output import ( "bytes" + "context" "encoding/json" "fmt" "sort" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fatih/color" - v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" pkgrestore "github.com/vmware-tanzu/velero/pkg/restore" ) -func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertFile string) string { +func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *velerov1api.Restore, podVolumeRestores []velerov1api.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertFile string) string { return Describe(func(d *Describer) { d.DescribeMetadata(restore.ObjectMeta) d.Println() phase := restore.Status.Phase if phase == "" { - phase = v1.RestorePhaseNew + phase = velerov1api.RestorePhaseNew } phaseString := string(phase) switch phase { - case v1.RestorePhaseCompleted: + case velerov1api.RestorePhaseCompleted: phaseString = color.GreenString(phaseString) - case v1.RestorePhaseFailedValidation, v1.RestorePhasePartiallyFailed, v1.RestorePhaseFailed: + case velerov1api.RestorePhaseFailedValidation, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseFailed: phaseString = color.RedString(phaseString) } resultsNote := "" - if phase == v1.RestorePhaseFailed || phase == v1.RestorePhasePartiallyFailed { + if phase == velerov1api.RestorePhaseFailed || phase == velerov1api.RestorePhasePartiallyFailed { resultsNote = fmt.Sprintf(" (run 'velero restore logs %s' for more information)", restore.Name) } @@ -79,7 +81,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor } } - describeRestoreResults(d, restore, veleroClient, insecureSkipTLSVerify, caCertFile) + describeRestoreResults(ctx, kbClient, d, restore, insecureSkipTLSVerify, caCertFile) d.Println() d.Printf("Backup:\t%s\n", restore.Spec.BackupName) @@ -143,7 +145,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor }) } -func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { +func describeRestoreResults(ctx context.Context, kbClient kbclient.Client, d *Describer, restore *velerov1api.Restore, insecureSkipTLSVerify bool, caCertPath string) { if restore.Status.Warnings == 0 && restore.Status.Errors == 0 { return } @@ -151,7 +153,7 @@ func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clie var buf bytes.Buffer var resultMap map[string]pkgrestore.Result - if err := downloadrequest.Stream(veleroClient.VeleroV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { + if err := downloadrequest.Stream(ctx, kbClient, restore.Namespace, restore.Name, velerov1api.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { d.Printf("Warnings:\t\n\nErrors:\t\n", err, err) return } @@ -186,7 +188,7 @@ func describeRestoreResult(d *Describer, name string, result pkgrestore.Result) } // describePodVolumeRestores describes pod volume restores in human-readable format. -func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, details bool) { +func describePodVolumeRestores(d *Describer, restores []velerov1api.PodVolumeRestore, details bool) { if details { d.Printf("Restic Restores:\n") } else { @@ -198,10 +200,10 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det // go through phases in a specific order for _, phase := range []string{ - string(v1.PodVolumeRestorePhaseCompleted), - string(v1.PodVolumeRestorePhaseFailed), + string(velerov1api.PodVolumeRestorePhaseCompleted), + string(velerov1api.PodVolumeRestorePhaseFailed), "In Progress", - string(v1.PodVolumeRestorePhaseNew), + string(velerov1api.PodVolumeRestorePhaseNew), } { if len(restoresByPhase[phase]) == 0 { continue @@ -230,15 +232,15 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det } } -func groupRestoresByPhase(restores []v1.PodVolumeRestore) map[string][]v1.PodVolumeRestore { - restoresByPhase := make(map[string][]v1.PodVolumeRestore) +func groupRestoresByPhase(restores []velerov1api.PodVolumeRestore) map[string][]velerov1api.PodVolumeRestore { + restoresByPhase := make(map[string][]velerov1api.PodVolumeRestore) - phaseToGroup := map[v1.PodVolumeRestorePhase]string{ - v1.PodVolumeRestorePhaseCompleted: string(v1.PodVolumeRestorePhaseCompleted), - v1.PodVolumeRestorePhaseFailed: string(v1.PodVolumeRestorePhaseFailed), - v1.PodVolumeRestorePhaseInProgress: "In Progress", - v1.PodVolumeRestorePhaseNew: string(v1.PodVolumeRestorePhaseNew), - "": string(v1.PodVolumeRestorePhaseNew), + phaseToGroup := map[velerov1api.PodVolumeRestorePhase]string{ + velerov1api.PodVolumeRestorePhaseCompleted: string(velerov1api.PodVolumeRestorePhaseCompleted), + velerov1api.PodVolumeRestorePhaseFailed: string(velerov1api.PodVolumeRestorePhaseFailed), + velerov1api.PodVolumeRestorePhaseInProgress: "In Progress", + velerov1api.PodVolumeRestorePhaseNew: string(velerov1api.PodVolumeRestorePhaseNew), + "": string(velerov1api.PodVolumeRestorePhaseNew), } for _, restore := range restores { diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 0da92ab9c..8fdcb332e 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. diff --git a/pkg/controller/backup_storage_location_controller.go b/pkg/controller/backup_storage_location_controller.go index 4f7d3dad9..dc9c430a7 100644 --- a/pkg/controller/backup_storage_location_controller.go +++ b/pkg/controller/backup_storage_location_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2020 the Velero contributors. +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. @@ -60,7 +60,7 @@ func (r *BackupStorageLocationReconciler) Reconcile(ctx context.Context, req ctr locationList, err := storage.ListBackupStorageLocations(r.Ctx, r.Client, req.Namespace) if err != nil { log.WithError(err).Error("No backup storage locations found, at least one is required") - return ctrl.Result{Requeue: true}, err + return ctrl.Result{}, err } pluginManager := r.NewPluginManager(log) diff --git a/pkg/controller/download_request_controller.go b/pkg/controller/download_request_controller.go index ea7d81599..b83b91ab1 100644 --- a/pkg/controller/download_request_controller.go +++ b/pkg/controller/download_request_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2017 the Velero contributors. +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. @@ -18,230 +18,154 @@ package controller import ( "context" - "encoding/json" - "time" - jsonpatch "github.com/evanphx/json-patch" "github.com/pkg/errors" "github.com/sirupsen/logrus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/clock" - "k8s.io/client-go/tools/cache" - "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1" - velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1" - velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1" "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" - "github.com/vmware-tanzu/velero/pkg/util/kube" ) -type downloadRequestController struct { - *genericController +// DownloadRequestReconciler reconciles a DownloadRequest object +type DownloadRequestReconciler struct { + Scheme *runtime.Scheme + Client kbclient.Client + Clock clock.Clock + // use variables to refer to these functions so they can be + // replaced with fakes for testing. + NewPluginManager func(logrus.FieldLogger) clientmgmt.Manager + BackupStoreGetter persistence.ObjectBackupStoreGetter - downloadRequestClient velerov1client.DownloadRequestsGetter - downloadRequestLister velerov1listers.DownloadRequestLister - restoreLister velerov1listers.RestoreLister - clock clock.Clock - kbClient client.Client - backupLister velerov1listers.BackupLister - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager - backupStoreGetter persistence.ObjectBackupStoreGetter + Log logrus.FieldLogger } -// NewDownloadRequestController creates a new DownloadRequestController. -func NewDownloadRequestController( - downloadRequestClient velerov1client.DownloadRequestsGetter, - downloadRequestInformer velerov1informers.DownloadRequestInformer, - restoreLister velerov1listers.RestoreLister, - kbClient client.Client, - backupLister velerov1listers.BackupLister, - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, - backupStoreGetter persistence.ObjectBackupStoreGetter, - logger logrus.FieldLogger, -) Interface { - c := &downloadRequestController{ - genericController: newGenericController(DownloadRequest, logger), - downloadRequestClient: downloadRequestClient, - downloadRequestLister: downloadRequestInformer.Lister(), - restoreLister: restoreLister, - kbClient: kbClient, - backupLister: backupLister, +// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=velero.io,resources=downloadrequests/status,verbs=get;update;patch +func (r *DownloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithFields(logrus.Fields{ + "controller": "download-request", + "downloadRequest": req.NamespacedName, + }) - // use variables to refer to these functions so they can be - // replaced with fakes for testing. - newPluginManager: newPluginManager, - backupStoreGetter: backupStoreGetter, - - clock: &clock.RealClock{}, - } - - c.syncHandler = c.processDownloadRequest - - downloadRequestInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - key, err := cache.MetaNamespaceKeyFunc(obj) - if err != nil { - downloadRequest := obj.(*velerov1api.DownloadRequest) - c.logger.WithError(errors.WithStack(err)). - WithField(DownloadRequest, downloadRequest.Name). - Error("Error creating queue key, item not added to queue") - return - } - c.queue.Add(key) - }, - }, - ) - - return c -} - -// processDownloadRequest is the default per-item sync handler. It generates a pre-signed URL for -// a new DownloadRequest or deletes the DownloadRequest if it has expired. -func (c *downloadRequestController) processDownloadRequest(key string) error { - log := c.logger.WithField("key", key) - - log.Debug("Running processDownloadRequest") - ns, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - log.WithError(err).Error("error splitting queue key") - return nil - } - - downloadRequest, err := c.downloadRequestLister.DownloadRequests(ns).Get(name) - if apierrors.IsNotFound(err) { - log.Debug("Unable to find DownloadRequest") - return nil - } - if err != nil { - return errors.Wrap(err, "error getting DownloadRequest") - } - - switch downloadRequest.Status.Phase { - case "", velerov1api.DownloadRequestPhaseNew: - return c.generatePreSignedURL(downloadRequest, log) - case velerov1api.DownloadRequestPhaseProcessed: - return c.deleteIfExpired(downloadRequest) - } - - return nil -} - -const signedURLTTL = 10 * time.Minute - -// generatePreSignedURL generates a pre-signed URL for downloadRequest, changes the phase to -// Processed, and persists the changes to storage. -func (c *downloadRequestController) generatePreSignedURL(downloadRequest *velerov1api.DownloadRequest, log logrus.FieldLogger) error { - update := downloadRequest.DeepCopy() - - var ( - backupName string - err error - ) - - switch downloadRequest.Spec.Target.Kind { - case velerov1api.DownloadTargetKindRestoreLog, velerov1api.DownloadTargetKindRestoreResults: - restore, err := c.restoreLister.Restores(downloadRequest.Namespace).Get(downloadRequest.Spec.Target.Name) - if err != nil { - return errors.Wrap(err, "error getting Restore") + // Fetch the DownloadRequest instance. + log.Debug("Getting DownloadRequest") + downloadRequest := &velerov1api.DownloadRequest{} + if err := r.Client.Get(ctx, req.NamespacedName, downloadRequest); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Unable to find DownloadRequest") + return ctrl.Result{}, nil } - backupName = restore.Spec.BackupName - default: - backupName = downloadRequest.Spec.Target.Name + log.WithError(err).Error("Error getting DownloadRequest") + return ctrl.Result{}, errors.WithStack(err) } - backup, err := c.backupLister.Backups(downloadRequest.Namespace).Get(backupName) + // Initialize the patch helper. + patchHelper, err := patch.NewHelper(downloadRequest, r.Client) if err != nil { - return errors.WithStack(err) + log.WithError(err).Error("Error getting a patch helper to update this resource") + return ctrl.Result{}, errors.WithStack(err) } - backupLocation := &velerov1api.BackupStorageLocation{} - if err := c.kbClient.Get(context.Background(), client.ObjectKey{ - Namespace: backup.Namespace, - Name: backup.Spec.StorageLocation, - }, backupLocation); err != nil { - return errors.WithStack(err) + defer func() { + // Always attempt to Patch the downloadRequest object and status after each reconciliation. + if err := patchHelper.Patch(ctx, downloadRequest); err != nil { + log.WithError(err).Error("Error updating download request") + return + } + }() + + if downloadRequest.Status != (velerov1api.DownloadRequestStatus{}) && downloadRequest.Status.Expiration != nil { + if downloadRequest.Status.Expiration.Time.Before(r.Clock.Now()) { + + // Delete any request that is expired, regardless of the phase: it is not + // worth proceeding and trying/retrying to find it. + log.Debug("DownloadRequest has expired - deleting") + if err := r.Client.Delete(ctx, downloadRequest); err != nil { + log.WithError(err).Error("Error deleting an expired download request") + return ctrl.Result{}, errors.WithStack(err) + } + return ctrl.Result{Requeue: false}, nil + + } else if downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed { + + // Requeue the request if is not yet expired and has already been processed before, + // since it might still be in use by the logs streaming and shouldn't + // be deleted until after its expiration. + log.Debug("DownloadRequest has not yet expired - requeueing") + return ctrl.Result{Requeue: true}, nil + } } - pluginManager := c.newPluginManager(log) - defer pluginManager.CleanupClients() + // Process a brand new request. + backupName := downloadRequest.Spec.Target.Name + if downloadRequest.Status.Phase == "" || downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseNew { - backupStore, err := c.backupStoreGetter.Get(backupLocation, pluginManager, log) - if err != nil { - return errors.WithStack(err) - } + // Update the expiration. + downloadRequest.Status.Expiration = &metav1.Time{Time: r.Clock.Now().Add(persistence.DownloadURLTTL)} - if update.Status.DownloadURL, err = backupStore.GetDownloadURL(downloadRequest.Spec.Target); err != nil { - return err - } - - update.Status.Phase = velerov1api.DownloadRequestPhaseProcessed - update.Status.Expiration = &metav1.Time{Time: c.clock.Now().Add(persistence.DownloadURLTTL)} - - _, err = patchDownloadRequest(downloadRequest, update, c.downloadRequestClient) - return errors.WithStack(err) -} - -// deleteIfExpired deletes downloadRequest if it has expired. -func (c *downloadRequestController) deleteIfExpired(downloadRequest *velerov1api.DownloadRequest) error { - log := c.logger.WithField("key", kube.NamespaceAndName(downloadRequest)) - log.Info("checking for expiration of DownloadRequest") - if downloadRequest.Status.Expiration.Time.After(c.clock.Now()) { - log.Debug("DownloadRequest has not expired") - return nil - } - - log.Debug("DownloadRequest has expired - deleting") - return errors.WithStack(c.downloadRequestClient.DownloadRequests(downloadRequest.Namespace).Delete(context.TODO(), downloadRequest.Name, metav1.DeleteOptions{})) -} - -// resync requeues all the DownloadRequests in the lister's cache. This is mostly to handle deleting -// any expired requests that were not deleted as part of the normal client flow for whatever reason. -func (c *downloadRequestController) resync() { - list, err := c.downloadRequestLister.List(labels.Everything()) - if err != nil { - c.logger.WithError(errors.WithStack(err)).Error("error listing download requests") - return - } - - for _, dr := range list { - key, err := cache.MetaNamespaceKeyFunc(dr) - if err != nil { - c.logger.WithError(errors.WithStack(err)).WithField(DownloadRequest, dr.Name).Error("error generating key for download request") - continue + if downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreLog || + downloadRequest.Spec.Target.Kind == velerov1api.DownloadTargetKindRestoreResults { + restore := &velerov1api.Restore{} + if err := r.Client.Get(ctx, kbclient.ObjectKey{ + Namespace: downloadRequest.Namespace, + Name: downloadRequest.Spec.Target.Name, + }, restore); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + backupName = restore.Spec.BackupName } - c.queue.Add(key) + backup := &velerov1api.Backup{} + if err := r.Client.Get(ctx, kbclient.ObjectKey{ + Namespace: downloadRequest.Namespace, + Name: backupName, + }, backup); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + + location := &velerov1api.BackupStorageLocation{} + if err := r.Client.Get(ctx, kbclient.ObjectKey{ + Namespace: backup.Namespace, + Name: backup.Spec.StorageLocation, + }, location); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + + pluginManager := r.NewPluginManager(log) + defer pluginManager.CleanupClients() + + backupStore, err := r.BackupStoreGetter.Get(location, pluginManager, log) + if err != nil { + log.WithError(err).Error("Error getting a backup store") + return ctrl.Result{}, errors.WithStack(err) + } + + if downloadRequest.Status.DownloadURL, err = backupStore.GetDownloadURL(downloadRequest.Spec.Target); err != nil { + return ctrl.Result{Requeue: true}, errors.WithStack(err) + } + + downloadRequest.Status.Phase = velerov1api.DownloadRequestPhaseProcessed + + // Update the expiration again to extend the time we wait (the TTL) to start after successfully processing the URL. + downloadRequest.Status.Expiration = &metav1.Time{Time: r.Clock.Now().Add(persistence.DownloadURLTTL)} } + + // Requeue is mostly to handle deleting any expired requests that were not + // deleted as part of the normal client flow for whatever reason. + return ctrl.Result{Requeue: true}, nil } -func patchDownloadRequest(original, updated *velerov1api.DownloadRequest, client velerov1client.DownloadRequestsGetter) (*velerov1api.DownloadRequest, error) { - origBytes, err := json.Marshal(original) - if err != nil { - return nil, errors.Wrap(err, "error marshalling original download request") - } - - updatedBytes, err := json.Marshal(updated) - if err != nil { - return nil, errors.Wrap(err, "error marshalling updated download request") - } - - patchBytes, err := jsonpatch.CreateMergePatch(origBytes, updatedBytes) - if err != nil { - return nil, errors.Wrap(err, "error creating json merge patch for download request") - } - - res, err := client.DownloadRequests(original.Namespace).Patch(context.TODO(), original.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) - if err != nil { - return nil, errors.Wrap(err, "error patching download request") - } - - return res, nil +func (r *DownloadRequestReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&velerov1api.DownloadRequest{}). + Complete(r) } diff --git a/pkg/controller/download_request_controller_test.go b/pkg/controller/download_request_controller_test.go index e00d2bb74..af16f2a3e 100644 --- a/pkg/controller/download_request_controller_test.go +++ b/pkg/controller/download_request_controller_test.go @@ -18,306 +18,249 @@ package controller import ( "context" - "testing" "time" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" + ctrl "sigs.k8s.io/controller-runtime" + + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" - "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake" - informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions" persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" velerotest "github.com/vmware-tanzu/velero/pkg/test" - kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" ) -type downloadRequestTestHarness struct { - client *fake.Clientset - informerFactory informers.SharedInformerFactory - pluginManager *pluginmocks.Manager - backupStore *persistencemocks.BackupStore - - controller *downloadRequestController -} - -func newDownloadRequestTestHarness(t *testing.T, fakeClient client.Client) *downloadRequestTestHarness { - var ( - client = fake.NewSimpleClientset() - informerFactory = informers.NewSharedInformerFactory(client, 0) - pluginManager = new(pluginmocks.Manager) - backupStore = new(persistencemocks.BackupStore) - controller = NewDownloadRequestController( - client.VeleroV1(), - informerFactory.Velero().V1().DownloadRequests(), - informerFactory.Velero().V1().Restores().Lister(), - fakeClient, - informerFactory.Velero().V1().Backups().Lister(), - func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, - NewFakeSingleObjectBackupStoreGetter(backupStore), - velerotest.NewLogger(), - ).(*downloadRequestController) - ) - - clockTime, err := time.Parse(time.RFC1123, time.RFC1123) - require.NoError(t, err) - controller.clock = clock.NewFakeClock(clockTime) - - pluginManager.On("CleanupClients").Return() - - return &downloadRequestTestHarness{ - client: client, - informerFactory: informerFactory, - pluginManager: pluginManager, - backupStore: backupStore, - controller: controller, +var _ = Describe("Download Request Reconciler", func() { + type request struct { + downloadRequest *velerov1api.DownloadRequest + backup *velerov1api.Backup + restore *velerov1api.Restore + backupLocation *velerov1api.BackupStorageLocation + expired bool + expectedReconcileErr string + expectGetsURL bool + expectedRequeue ctrl.Result } -} - -func newDownloadRequest(phase velerov1api.DownloadRequestPhase, targetKind velerov1api.DownloadTargetKind, targetName string) *velerov1api.DownloadRequest { - return &velerov1api.DownloadRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: "a-download-request", - Namespace: velerov1api.DefaultNamespace, - }, - Spec: velerov1api.DownloadRequestSpec{ - Target: velerov1api.DownloadTarget{ - Kind: targetKind, - Name: targetName, - }, - }, - Status: velerov1api.DownloadRequestStatus{ - Phase: phase, - }, - } -} - -func newBackupLocation(name, provider, bucket string) *velerov1api.BackupStorageLocation { - return &velerov1api.BackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: velerov1api.DefaultNamespace, - }, - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: provider, - StorageType: velerov1api.StorageType{ - ObjectStorage: &velerov1api.ObjectStorageLocation{ - Bucket: bucket, - }, - }, - }, - } -} - -func TestProcessDownloadRequest(t *testing.T) { defaultBackup := func() *velerov1api.Backup { return builder.ForBackup(velerov1api.DefaultNamespace, "a-backup").StorageLocation("a-location").Result() } - tests := []struct { - name string - key string - downloadRequest *velerov1api.DownloadRequest - backup *velerov1api.Backup - restore *velerov1api.Restore - backupLocation *velerov1api.BackupStorageLocation - expired bool - expectedErr string - expectGetsURL bool - }{ - { - name: "empty key returns without error", - key: "", + DescribeTable("a Download request", + func(test request) { + // now will be used to set the fake clock's time; capture + // it here so it can be referenced in the test case defs. + now, err := time.Parse(time.RFC1123, time.RFC1123) + Expect(err).To(BeNil()) + now = now.Local() + + rClock := clock.NewFakeClock(now) + + const signedURLTTL = 10 * time.Minute + + var ( + pluginManager = &pluginmocks.Manager{} + backupStores = make(map[string]*persistencemocks.BackupStore) + ) + pluginManager.On("CleanupClients").Return(nil) + + Expect(test.downloadRequest).ToNot(BeNil()) + + // Set .status.expiration properly for all requests test cases that are + // meant to be expired. Since "expired" is relative to the controller's + // clock time, it's easier to do this here than as part of the test case definitions. + if test.expired { + test.downloadRequest.Status.Expiration = &metav1.Time{Time: rClock.Now().Add(-1 * time.Minute)} + } + + fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme) + err = fakeClient.Create(context.TODO(), test.downloadRequest) + Expect(err).To(BeNil()) + + if test.backup != nil { + err := fakeClient.Create(context.TODO(), test.backup) + Expect(err).To(BeNil()) + } + + if test.backupLocation != nil { + err := fakeClient.Create(context.TODO(), test.backupLocation) + Expect(err).To(BeNil()) + backupStores[test.backupLocation.Name] = &persistencemocks.BackupStore{} + } + + if test.restore != nil { + err := fakeClient.Create(context.TODO(), test.restore) + Expect(err).To(BeNil()) + } + + // Setup reconciler + Expect(velerov1api.AddToScheme(scheme.Scheme)).To(Succeed()) + r := DownloadRequestReconciler{ + Client: fakeClient, + Clock: rClock, + NewPluginManager: func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + BackupStoreGetter: NewFakeObjectBackupStoreGetter(backupStores), + Log: velerotest.NewLogger(), + } + + if test.backupLocation != nil && test.expectGetsURL { + backupStores[test.backupLocation.Name].On("GetDownloadURL", test.downloadRequest.Spec.Target).Return("a-url", nil) + } + + actualResult, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: test.downloadRequest.Name, + }, + }) + + Expect(actualResult).To(BeEquivalentTo(test.expectedRequeue)) + if test.expectedReconcileErr == "" { + Expect(err).To(BeNil()) + } else { + Expect(err.Error()).To(Equal(test.expectedReconcileErr)) + } + + instance := &velerov1api.DownloadRequest{} + err = r.Client.Get(ctx, kbclient.ObjectKey{Name: test.downloadRequest.Name, Namespace: test.downloadRequest.Namespace}, instance) + + if test.expired { + Expect(instance).ToNot(Equal(test.downloadRequest)) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } else { + if test.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed { + Expect(instance).To(Equal(test.downloadRequest)) + } else { + Expect(instance).ToNot(Equal(test.downloadRequest)) + } + Expect(err).To(BeNil()) + } + + if test.expectGetsURL { + Expect(string(instance.Status.Phase)).To(Equal(string(velerov1api.DownloadRequestPhaseProcessed))) + Expect(instance.Status.DownloadURL).To(Equal("a-url")) + Expect(velerotest.TimesAreEqual(instance.Status.Expiration.Time, r.Clock.Now().Add(signedURLTTL))).To(BeTrue()) + } }, - { - name: "bad key format returns without error", - key: "a/b/c", - }, - { - name: "no download request for key returns without error", - key: "nonexistent/key", - }, - { - name: "backup contents request for nonexistent backup returns an error", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"), - backup: builder.ForBackup(velerov1api.DefaultNamespace, "non-matching-backup").StorageLocation("a-location").Result(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), - expectedErr: "backup.velero.io \"a-backup\" not found", - }, - { - name: "restore log request for nonexistent restore returns an error", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"), - restore: builder.ForRestore(velerov1api.DefaultNamespace, "non-matching-restore").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), + + Entry("backup contents request for nonexistent backup returns an error", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a1-backup").Result(), + backup: builder.ForBackup(velerov1api.DefaultNamespace, "non-matching-backup").StorageLocation("a-location").Result(), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), + expectedReconcileErr: "backups.velero.io \"a1-backup\" not found", + expectedRequeue: ctrl.Result{Requeue: false}, + }), + Entry("restore log request for nonexistent restore returns an error", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(), + restore: builder.ForRestore(velerov1api.DefaultNamespace, "non-matching-restore").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), + backup: defaultBackup(), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), + expectedReconcileErr: "restores.velero.io \"a-backup-20170912150214\" not found", + expectedRequeue: ctrl.Result{Requeue: false}, + }), + Entry("backup contents request for backup with nonexistent location returns an error", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(), + backup: defaultBackup(), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "non-matching-location").Provider("a-provider").Bucket("a-bucket").Result(), + expectedReconcileErr: "backupstoragelocations.velero.io \"a-location\" not found", + expectedRequeue: ctrl.Result{Requeue: false}, + }), + Entry("backup contents request with phase '' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), - expectedErr: "error getting Restore: restore.velero.io \"a-backup-20170912150214\" not found", - }, - { - name: "backup contents request for backup with nonexistent location returns an error", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"), - backup: defaultBackup(), - backupLocation: newBackupLocation("non-matching-location", "a-provider", "a-bucket"), - expectedErr: "backupstoragelocations.velero.io \"a-location\" not found", - }, - { - name: "backup contents request with phase '' gets a url", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupContents, "a-backup"), - backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "backup contents request with phase 'New' gets a url", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindBackupContents, "a-backup"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("backup contents request with phase 'New' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupContents, "a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "backup log request with phase '' gets a url", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindBackupLog, "a-backup"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("backup log request with phase '' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupLog, "a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "backup log request with phase 'New' gets a url", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindBackupLog, "a-backup"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("backup log request with phase 'New' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "restore log request with phase '' gets a url", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("restore log request with phase '' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(), restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "restore log request with phase 'New' gets a url", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("restore log request with phase 'New' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindRestoreLog, "a-backup-20170912150214").Result(), + backup: defaultBackup(), + restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), + expectGetsURL: true, + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("restore results request with phase '' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214").Result(), restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "restore results request with phase '' gets a url", - downloadRequest: newDownloadRequest("", velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("restore results request with phase 'New' gets a url", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214").Result(), restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "a-location").Provider("a-provider").Bucket("a-bucket").Result(), expectGetsURL: true, - }, - { - name: "restore results request with phase 'New' gets a url", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseNew, velerov1api.DownloadTargetKindRestoreResults, "a-backup-20170912150214"), - restore: builder.ForRestore(velerov1api.DefaultNamespace, "a-backup-20170912150214").Phase(velerov1api.RestorePhaseCompleted).Backup("a-backup").Result(), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("request with phase 'Processed' and not expired is not deleted", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseProcessed).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(), backup: defaultBackup(), - backupLocation: newBackupLocation("a-location", "a-provider", "a-bucket"), - expectGetsURL: true, - }, - { - name: "request with phase 'Processed' is not deleted if not expired", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseProcessed, velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214"), - backup: defaultBackup(), - }, - { - name: "request with phase 'Processed' is deleted if expired", - downloadRequest: newDownloadRequest(velerov1api.DownloadRequestPhaseProcessed, velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214"), + expectedRequeue: ctrl.Result{Requeue: true}, + }), + Entry("request with phase 'Processed' and expired is deleted", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseProcessed).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(), backup: defaultBackup(), expired: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var fakeClient client.Client - if tc.backupLocation != nil { - fakeClient = velerotest.NewFakeControllerRuntimeClient(t, tc.backupLocation) - } else { - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) - } - - harness := newDownloadRequestTestHarness(t, fakeClient) - - // set up test case data - - // Set .status.expiration properly for processed requests. Since "expired" is relative to the controller's - // clock time, it's easier to do this here than as part of the test case definitions. - if tc.downloadRequest != nil && tc.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed { - if tc.expired { - tc.downloadRequest.Status.Expiration = &metav1.Time{Time: harness.controller.clock.Now().Add(-1 * time.Minute)} - } else { - tc.downloadRequest.Status.Expiration = &metav1.Time{Time: harness.controller.clock.Now().Add(time.Minute)} - } - } - - if tc.downloadRequest != nil { - require.NoError(t, harness.informerFactory.Velero().V1().DownloadRequests().Informer().GetStore().Add(tc.downloadRequest)) - - _, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Create(context.TODO(), tc.downloadRequest, metav1.CreateOptions{}) - require.NoError(t, err) - } - - if tc.restore != nil { - require.NoError(t, harness.informerFactory.Velero().V1().Restores().Informer().GetStore().Add(tc.restore)) - } - - if tc.backup != nil { - require.NoError(t, harness.informerFactory.Velero().V1().Backups().Informer().GetStore().Add(tc.backup)) - } - - if tc.expectGetsURL { - harness.backupStore.On("GetDownloadURL", tc.downloadRequest.Spec.Target).Return("a-url", nil) - } - - // exercise method under test - key := tc.key - if key == "" && tc.downloadRequest != nil { - key = kubeutil.NamespaceAndName(tc.downloadRequest) - } - - err := harness.controller.processDownloadRequest(key) - - // verify results - if tc.expectedErr != "" { - require.Equal(t, tc.expectedErr, err.Error()) - } else { - assert.Nil(t, err) - } - - if tc.expectGetsURL { - output, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Get(context.TODO(), tc.downloadRequest.Name, metav1.GetOptions{}) - require.NoError(t, err) - - assert.Equal(t, string(velerov1api.DownloadRequestPhaseProcessed), string(output.Status.Phase)) - assert.Equal(t, "a-url", output.Status.DownloadURL) - assert.True(t, velerotest.TimesAreEqual(harness.controller.clock.Now().Add(signedURLTTL), output.Status.Expiration.Time), "expiration does not match") - } - - if tc.downloadRequest != nil && tc.downloadRequest.Status.Phase == velerov1api.DownloadRequestPhaseProcessed { - res, err := harness.client.VeleroV1().DownloadRequests(tc.downloadRequest.Namespace).Get(context.TODO(), tc.downloadRequest.Name, metav1.GetOptions{}) - - if tc.expired { - assert.True(t, apierrors.IsNotFound(err)) - } else { - assert.NoError(t, err) - assert.Equal(t, tc.downloadRequest, res) - } - } - }) - } -} + expectedRequeue: ctrl.Result{Requeue: false}, + }), + Entry("request with phase '' and expired is deleted", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase("").Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(), + backup: defaultBackup(), + expired: true, + expectedRequeue: ctrl.Result{Requeue: false}, + }), + Entry("request with phase 'New' and expired is deleted", request{ + downloadRequest: builder.ForDownloadRequest(velerov1api.DefaultNamespace, "a-download-request").Phase(velerov1api.DownloadRequestPhaseNew).Target(velerov1api.DownloadTargetKindBackupLog, "a-backup-20170912150214").Result(), + backup: defaultBackup(), + expired: true, + expectedRequeue: ctrl.Result{Requeue: false}, + }), + ) +}) diff --git a/pkg/controller/server_status_request_controller.go b/pkg/controller/server_status_request_controller.go index a10ba5fc5..f1872b304 100644 --- a/pkg/controller/server_status_request_controller.go +++ b/pkg/controller/server_status_request_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2018 the Velero contributors. +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. @@ -71,12 +71,11 @@ func (r *ServerStatusRequestReconciler) Reconcile(ctx context.Context, req ctrl. statusRequest := &velerov1api.ServerStatusRequest{} if err := r.Client.Get(r.Ctx, req.NamespacedName, statusRequest); err != nil { if apierrors.IsNotFound(err) { - log.WithError(err).Error("ServerStatusRequest not found") + log.Debug("Unable to find ServerStatusRequest") return ctrl.Result{}, nil } log.WithError(err).Error("Error getting ServerStatusRequest") - // Error reading the object - requeue the request. return ctrl.Result{}, err }