From 4b9dbfa416bbcddc17064e0fec3300da34e614e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wenkai=20Yin=28=E5=B0=B9=E6=96=87=E5=BC=80=29?= Date: Fri, 2 Sep 2022 10:59:09 +0800 Subject: [PATCH] Support pause/unpause schedules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support pause/unpause schedule Fixes #2363 Signed-off-by: Wenkai Yin(尹文开) --- changelogs/unreleased/5279-ywk253100 | 1 + config/crd/v1/bases/velero.io_schedules.yaml | 6 + config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/schedule_types.go | 5 + pkg/cmd/cli/delete_options.go | 30 ++--- pkg/cmd/cli/schedule/create.go | 3 + pkg/cmd/cli/schedule/pause.go | 122 ++++++++++++++++++ pkg/cmd/cli/schedule/schedule.go | 2 + pkg/cmd/cli/schedule/unpause.go | 55 ++++++++ pkg/cmd/cli/select_option.go | 69 ++++++++++ pkg/cmd/cli/select_option_test.go | 45 +++++++ pkg/cmd/util/output/schedule_describer.go | 3 + pkg/cmd/util/output/schedule_printer.go | 2 + .../backup_storage_location_controller.go | 15 +-- pkg/controller/backup_sync_controller.go | 35 ++--- pkg/controller/gc_controller.go | 6 +- pkg/controller/schedule_controller.go | 22 ++-- pkg/util/kube/periodical_enqueue_source.go | 22 ++-- .../kube/periodical_enqueue_source_test.go | 14 +- pkg/util/kube/predicate.go | 51 ++++++++ pkg/util/kube/predicate_test.go | 19 +++ 21 files changed, 440 insertions(+), 89 deletions(-) create mode 100644 changelogs/unreleased/5279-ywk253100 create mode 100644 pkg/cmd/cli/schedule/pause.go create mode 100644 pkg/cmd/cli/schedule/unpause.go create mode 100644 pkg/cmd/cli/select_option.go create mode 100644 pkg/cmd/cli/select_option_test.go diff --git a/changelogs/unreleased/5279-ywk253100 b/changelogs/unreleased/5279-ywk253100 new file mode 100644 index 0000000000..51dd99deaf --- /dev/null +++ b/changelogs/unreleased/5279-ywk253100 @@ -0,0 +1 @@ +Support pause/unpause schedules \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index c3e0a69fa4..246bafb3f7 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -32,6 +32,9 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + - jsonPath: .spec.paused + name: Paused + type: boolean name: v1 schema: openAPIV3Schema: @@ -53,6 +56,9 @@ spec: spec: description: ScheduleSpec defines the specification for a Velero schedule properties: + paused: + description: Paused specifies whether the schedule is paused or not + type: boolean schedule: description: Schedule is a Cron expression defining when to run the Backup. diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index efdbadcf47..14cd3c51ba 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -37,7 +37,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z\xdds\x1b\xb7\x11\u007f\xe7_\xb1\xe3<\xa8\x991\x8f\x89\xdbi;|s\xa4\xa6\xa36\x915\x96\xad\x17\x8f\x1f\xc0\xc3\xf2\x0e\xd1\x1d\x80\x028\xcal&\xff{g\xf1q\xbc\x0f\x90\x944ur/6\xf1\xb1\xf8a\xbfw\xa1\xc5r\xb9\\0-\xee\xd1X\xa1\xe4\x1a\x98\x16\xf8š\xa4_\xb6x\xf8\xbb-\x84Z\xed\xbe_<\b\xc9\xd7p\xd9Y\xa7\xda\xf7hUgJ\xbc\u00ad\x90\xc2\t%\x17-:ƙc\xeb\x05\x00\x93R9FÖ~\x02\x94J:\xa3\x9a\x06ͲBY\xf38\xfa\x01\xed\xe4\xfa\x8b\x99\xdb\x1e\xd1~[aޞ\xc3\xf4\xee\xfb\xe0@\xcb\x1a[\xb6\x8e+\x95F\xf9\xf6\xf6\xfa\xfe\xcfw\xa3a\x00m\x94F\xe3Dr\xe8\xe1\x1bı\xc1(\x8cY}A\x04\xc3*\xe0\x14\xc0\xd0\x06\xab\bc\xc8#\x86 \x0eaIu\rZ\x94nȒ\xf4\xa9-0\tj\xf3\v\x96\xae\x80;4D&\t\xa6Tr\x87Ɓ\xc1RUR\xfc\xb7\xa7mI\xd7\xe8І9\x8cq\xe5\xf0y\xd7/Y\x03;\xd6t\xf8\x1a\x98\xe4в=\x18\xa4S\xa0\x93\x03z~\x89-\xe0ge\x10\x84ܪ5\xd4\xcei\xbb^\xad*\xe1R\xfc.U\xdbvR\xb8\xfdʇb\xb1\xe9\x9c2v\xc5q\x87\xcdʊj\xc9LY\v\x87\xa5\xeb\f\xae\x98\x16K\x0f]\xfa\x18^\xb4\xfc\x1b\x13#\xbe\xbd\x18a\x9d)F\xf8|x=!\x01\n\xb0 ,\xb0\xb85\xdc\xe2\xc0\xe8\xe4 \xdf\xff\xe3\xee\x03\xa4\xa3\xbd0\xa6\xdc\xf7|?l\xb4\a\x11\x10Ä\xdcbt0[\xa3ZO\x13%\xd7JH\xe7\u007f\x94\x8d@9e\xbf\xed6\xadp$\xf7\xffth\x1dɪ\x80K\x9fԐ\xc3\xec4i./\xe0Z\xc2%k\xb1\xb9d\x16\xbf\xba\x00\x88\xd3vI\x8c}\x9a\b\x86\xf9\xd8tq\xe0\xda`\"%MG\xe45Ʉ\xee4\x96$=b \xed\x14[\x11=\x14\xb9s6]^\x8c\b\xe7\r\x97\xbe\xacw\x9a.\x82\\p\x99\xecI\xd8\xe4\xc0\xa7&\x87\x19VΈ\x024S/\xdb\xef\x19F.\x1b\x1dl1\xa3pD\f\xf4I\xc5\xf1\xcc=n\x14\xc7\x1cl\xda\n\xaefA[)\xe3#\u007f\xd4I9?\x85>%\x9f\x05L+~\x06W<\x91\x81\xc1-\x1a\x94%&\xc7u*\x9d\xc9 \x1b&\x1as\x8cǕ\x02Nx\xf5,ⷷ\xd7ɓ'&F\xecn~\xee\x19\xfeз\x15\xd8p\x1f\xe8Ο}q\xbd\r\x87y\x9f\xe6\x140\xd0\x02Cb\xda\a\t\x10\xd2:d\x1c\xd46K\x91\xca' \xc37\x18w\xbc\x0e\x1e,\xba\xcaCh!\xde\x03#\xdf)8\xfc\xeb\xee\xdd\xcd\xea\x9f9\xd6\xf7\xb7\x00V\x96h}^\xee\xb0E\xe9^\xf7\xa5\x02G+\frJ\xfc\xb1h\x99\x14[\xb4\xae\x88g\xa0\xb1\x9f\xde|\xces\x0f\xe0Ge\x00\xbf\xb0V7\xf8\x1aD\xe0x\uf593\xd2\b\x1b\xd8\xd1S\x84G\xe1j1\r\xa6=\aH\xbd\xe2\xb5\x1f\xfdu\x1d{@P\xf1\xba\x1dB#\x1ep\r\xaf|Zs\x80\xf9+\xd9\xceo\xaf\x8eP\xfdS0\xedW\xb4\xe8U\x00\xd7\xc7\xe1\xa1\xd1\x1d@\x06\xcb3\xa2\xaa\xf0\x90UM?\x1fT\xc8U\u007f\v\xca\x10\a\xa4\x1a\x90\xf0\x84Iz\xc1Q\"\x9f\x81\xfe\xf4\xe6\xf3Q\xc4c~\x81\x90\x1c\xbf\xc0\x1b\x10\xb1\xd8Ҋ\u007f[\xc0\a\xaf\x1d{\xe9\xd8\x17:\xa9\xac\x95\xc5c\x9cU\xb2ه\xf6\x881\tʄyp\xcdL\ueffa*\x13C;C\x88\xf6\xcb\xd8\xf6[2\xc9\xe9\xffVXG\xe3/\xe2`'\x9ed\xbe\x1f\xaf\xaf~\x1f\x05\xefċl\xf5H\x02\x1etd\xd8\xe58\x93\x98\xbd\x1f-N\xa9c&c\xed\xd7<+3t\xacʤb\xc3\xf6䩄\xed$\aƭ\x18VY`\x06\x81A\xcb4I\xee\x01\xf7\xcb\x10\xe25\x13\x14\x9f)\x04\xf7}\x0e`Z7\"\x1b\x8ac \x8fIh\xe4\x04\x15ڬ\xb2\xc7\ue795ð\xafsF\n\x1f\aK\x93\f\xcet\x96\\\x9d\xb3\xd4Q\xbfi\x8e\x16e\xd7Ρ,\xe1Ai\xc12\xe3\x06\xad\x13ef\xe2\xd5<\xd38!\xac\xc0\xcb3<\x88-\xe8L\xf1\x12E\x112\xbd\xbe\x80\xf1]\xc7\\\x85p\xbc<8\n\x91*t\xca[\xc7\x10\x97\xf9Rr\xb2\x86J\xabɐV|1ed\xa6\xf3\x98&G\x9d\xd1!\xd2y}\xed\x1b\xdeϨ\xb0C#?\xf24\xf8S\x97\xda\xfbTL\xbc\xb4\xc6.\x15\xe5\xe9㧕\xd3⽜\xef\xf0\xed,ã\xba\x8b\x96\xacw\xd0\xf6\x8fg\xe4\x8ad\x18\x90\v;}\x04#j\xc8}\x12M9\xfe\x96\x89\x069\xa4\xb7\x9d\xe9\x9e\f\xd5!\x95\rn\xc9\xdd\a\xd3K\xa5i\x84\xd7'\xaa5\x82\xf5}\xa2\v{\x82fg\x91\xfb\x9eF\x86\t\xf3\xe4u\xabL\xcb\\\xe8k.\xb3De\xd74l\xd3\xe0\x1a\x9c\xe9\xe6\xd3',\xb1EkYu\xce\x14\u007f\x0e\xabB\xc5\x1e\xb7\x00ۨ\xce\xf5%\xfb\xc8=^بS\xcf\xeb\x1ad\x8b\xe1\xb1:3*VlLڛ\xc6\xef\x19:\x82Ã\xa0G\xb5\xc1|\xd0\u007f\x89O\x00\xf0\x0fZ\xe7\x10Қ\x9c\x81\xf5\xde뤅\xc1\t\xa7|\x83\x8f\x99\xd1\xd9C\xdcp\xf22\x99Lf\xeeGo\rϺ\u007f<\xe8\x1c\v\xe22\xa8U\x93\x8cY9ր\xec\xda\r\x1a\xe2\xc3f\xefЎ\xddy\xae?\xe3\xeb\xba\x03\x1b\a\xfb\x93\xfc\x02\xa5X\xaa\x96L\xfa>*Y\x97S\xc0\x85\xd5\r\xdbg\b\xa7\x8b\xf8܍\x8c\x8b\\\xc0A\x9f\x93Qk4~\xea\xb9}%\x8f\xe9J\xc9#\x95F\xb2g!\xdd_\xffr\"\xd3\x13\xd2a5\t\x0eq\x9e\xd8\xf9\x03\x9d\xf2uN8\x91\xc4Xɴ\xad\x95\xbb\xbe:\xa3\x05w\xfd\xc2d\r\xb3\xe79\xec\xa9EUȉ\xaa\xf7-\xcf2\xd5\xf1\x13\xf09\xa8\xa3\xc5g\xa2P||\xceŠ;\xd4̐\xa5\xfb7\x81\xcb\xe9\xa3\xd5k\xb0\xc27:)\xf3\f\xa9hhCX\nN\x94Z)\x83\x19\x97\t\xf3\xb02\n\"c\xf8\xbfg\xfc\xc8\xea\xc9l\xd0#\xe7\x03ڱY>\x1c\xe96\xfdC\xd0\x1a~\xfdmqHlXI\xc5\x13\xf2\x9b\xe9\x1fYĔ3\xfdՄ\xffY*\x19*\t\xbb\x86O\x9f\x17\xe9\xd9\xf2>\xfd1\x04\r\xfe/\x00\x00\xff\xff\xb0\xddǼ\x99\"\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\x1b\xb9\x11\xbe\xf3Wti\x0f\xcaV\x99\xc3]'\x95\xa4x\xb3\xa5lJɮ\xac2e]\\>\x80\x83&\a\xab\x19`\x82Ɛf\xb6\xf6\xbf\xa7\x1a\x0fr^$%U\xb4\x9e\x8b-\xa0\xd1\xf8\xf0\xa1_hN\xa6\xd3\xe9D\xd4\xea\x01-)\xa3\xe7 j\x85_\x1dj\xfe\x8b\xb2ǿS\xa6\xccl\xf3\xe3\xe4Qi9\x87\xab\x86\x9c\xa9>\"\x99\xc6\xe6x\x8d+\xa5\x95SFO*tB\n'\xe6\x13\x00\xa1\xb5q\x82\x87\x89\xff\x04ȍv֔%\xda\xe9\x1au\xf6\xd8,q٨R\xa2\xf5\xca\xd3֛\x1f\xb2\xbfe?L\x00r\x8b~\xf9\xbd\xaa\x90\x9c\xa8\xea9\xe8\xa6,'\x00ZT8\x87\xdaȍ)\x9b\n-\x923\x16)\xdb`\x89\xd6d\xcaL\xa8Ɯw][\xd3\xd4s8L\x84\xc5\x11Q8͝\x91\x0f^\xcfǠ\xc7O\x95\x8aܿG\xa7\u007fV\xe4\xbcH]6V\x94#8\xfc,)\xbdnJa\x87\xf3\x13\x00\xcaM\x8ds\xb8e(\xb5\xc8QN\x00\"\x01\x1e\xda\x14\x84\x94\x9eRQ\xdeY\xa5\x1d\xda+V\x91\xa8\x9c\x82Dʭ\xaa\x9d\xa7l\xaf\a\xcc\n\\\x81\xbc\xa5\xa7[(\xad\xf4\xda\x0f\x05\b\xe0\f,\x11\"\x12\xe9\x95\x01\xfcJF\xdf\tW\xcc!c\xe2\xb2\xda\xc8L'\x9dQ&p~\xdb\x1bu;>\a9\xab\xf4\xfa\x18\xb2\xff3\xa8\x0e\x9e;#\x9f\x88\xe4\xbe@/\x93\xd04ui\x84D˛\x17B\xcb\x12\x81-\x17\x9c\x15\x9aVh\x8f\xa0H\xcb\xeewu\x17ɧ\xa4\xaf5\xf3\x1cv\x9eCE\x90\xedl\xff\xd0\x1e:\xb7\uf751q\x01D\xa3\x06r\xc25\x04\xd4\xe4\x05\b\x82[\xdc\xcen\xf4\x9d5k\x8bD#0\xbcxV\x17\x82\xba8\x16~\xe2uq\xac\x8c\xad\x84\x9b\x83\xd2\xee\xaf\u007f9\x8e-.ʜq\xa2|\xbfsH\x1d\xa4\xf7\xfdဖ\x9dm\x1d\xaf\xff\x9b\xc0]2\xa4k\xa3\xbb\xbc\xbe\uf34e\x81m)M\x818\x1b\x04ю\xd6w\xeb\xae>)\\\x18\bӛ\x1fC(\xcb\v\xac\xc4ם\xb8\x01Nv\xa0\bD\\\x1aNq :\x85\xec\x8f\xffX\xdcC\xda\xda_F\x9f}\xcf\xfba!\x1d\xae\x80\tSz\xc5A\x97/qeM\xe5u\xa2\x96\xb5Q\xda\xf9?\xf2R\xa1\xee\xd3OͲR\x8e\xef\xfd?\r\x92\xe3\xbb\xca\xe0ʗ\x18\x1c/\x9b\x9a-Wfp\xa3\xe1JTX^\t\xc2W\xbf\x00f\x9a\xa6L\xecӮ\xa0]\x1d\xf5\x85\x03k\xad\x89T\xc1\x1c\xb9\xaf~U\xb2\xa81\xe7\xebc\x06y\xa9Z\xa9\xdc\xfb\x06\x87\x1f\x10\x03\xf9\xac\xa3z\xdcu\xf9[\x8a\xfc\xb1\xa9\x17\xceX\xb1ƟM\xd0\xd9\x17\xeaa{?\xb6&\x81ӭ\x9c\x17\x94\x03\x05ɁR\x802-\xde\x16h\xb1\xbd\xc6bmH9cw\xac8d\xcbl\xa0\xe1\xc8E\xf8#\x1by\xe6\x18\x1c\xee\xbdCX\\\xa1E\x9dc\x8a\x10\xa7*\x99\x91S\xb4\x12\xfa\x10\xe2q\xea\xe1D\xf4\x1c\x05\xfc\xee\xee&E\xcc\xc4p\x84\xee\x86\xfb\x9e\xa1\x87\xbf\x95\xc2R\xfa\x84r~\xef˛U\xd8\xcc\xc7\x0eg@@\xad0T\xa4\xfb`\fJ\x93C!\xc1\xacF5\xf2\xa3\x01\xd8\xc1,\xc6\x15oB\xa4\x88!\xe9\x10\u0099z\x10\x1c\xa3\x94\x84\u007f->\xdc\xce\xfe9\xc6\xfc\xfe\x14 \xf2\x1c\x89|\xbe\xc6\n\xb5{\xb3\xcf\xd9\x12IY\x94\\\xb8`V\t\xadVH.\x8b{\xa0\xa5\xcfo\xbf\x8c\xb3\a\U00013c40_EU\x97\xf8\x06T`|\x1f\xfe\x92\xcd(\nt\xec5\xc2V\xb9B\xf5\x93֞\x01\xb6\xaex\xec\xad?\xae\x13\x8f\b&\x1e\xb7A(\xd5#\xce\xe1\xc2W\x82\a\x98\xbf\xb1c\xfd~qD럂\x03]\xb0\xd0E\x00\xb7\xcfwm\x8f<\x80t\x85p\xe0\xacZ\xaf\xf1P\x88\xf6?\x1f\xbc9$~\x0f\xc62\x03ڴTx\xc5|{!\x1e\xa1\x1c\x80\xfe\xfc\xf6\xcbQ\xc4]\xbe@i\x89_\xe1-(\x1d\xb8\xa9\x8d\xfc>\x83{o\x1d;\xed\xc4W\xde)/\f\xe11f\x8d.w\xa1\xda\xdf \x90\xa9\x10\xb6X\x96\xd3PoH؊\x1d\xb3\x90.\x8e\xedM@-\xac;i\xad\xa9ʸ\xffp\xfda\x1e\x90\xb1A\xad}\xbc\xe3\xec\xb4R\\5p\xb9\x10r\x9e\xb7\xc6A\xd2L\x1f5\xc1|\x9c\x81\xbc\x10z\x8d\xe1\xbc\b\xab\x86\xb3Pv\xf9\x12?\x1e\xa6\xfe\xf4\x8d\x94\x00\xfd\xc0\xf1͒\xe8\x13\x0f\xe7+\xd5'\x1c\xae\xfd\xd6:y\xb8\xc7f\x89V\xa3C\u007f>ir\xe2\xa3\xe5X;\x9a\x99\rڍ\xc2\xedlk\xec\xa3\xd2\xeb)\x9b\xe64\xd8\x00\xcd\xfc\x93y\xf6\x9d\xff\xe7\xc5g\xf1\xaf\xeb\xa7\x1e\xa8\xf3\xe8\u007f\xcdS\xf1>4{ѡR\xad\xf8\xf4\x87\xdf~\x9f\x1cҝȹ\xd6Ey\xdb\xff!8V#\xe9w]\xffgnt\xf8!\x96\xe6\xf0\xf9\xcb\x04b\xab\xf1!\xfdX˃\xff\v\x00\x00\xff\xff\xd7w>\xba>\x1f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xdc\x18\x93Z\x98\x92\x9a\x01fAW֬#D{zSI\xa4\xbd\x16ĺv\xacV\x95\x1f{h\xe0:\x14\x8fS\x04V\xcc`\x0e*\x88}%Є\xb5r\xc7\xfeV\xb1\\\x8d\x82n\x90\xf7\xae\x86`+\x14`P`f\x95\x1eR2\x85\x9e\xfeIQ\x96#t\x8c\xa8;\xfc\xb7\x88M\x80\x04\x12\xf3\xe7-϶\xde\v \xd9tp Wh\x9c\xe6 Ou?\x86$\xcc\xf1>,2\xa5;\xdag\xe6L\x1d\u008b\xe9\x93\xf6Iз\xed3\xa3y\a\x9a%\xbc\x8f\x9a\xce\xf6\xf9\xf7$lmJN\x10ڻ\xc1\xd4\xd7\x15Z\x17U\x91\xb7\u007f\xb7\x06,J\xbb\xbf\x02n\xeb\xb7s\x10\x99\x10\x9d\xf5\xffƌ9^\xe2\xef\x0eg\xbe\xaa\xc4Ore\x0e\"q\xa5Y\xfeo\xc8\x14g,\x1e\x83\xadHfȯ\xddYW\xc0\xd7\rC\xf2+XsaQ\x1fp\xe6E\xe7\xe55\x88\x91b\xef\xe8)\x98Ͷ\uffd0gc\xda\fT\"]\x0e'{\x9f\xb8\x0e\x12\xfa\x86y\x06.\xb8\xf8\x95k,|\\\xfc\xc9Q\xb3}\xe3\x02\x8aw\x1f~\xc4|\x8a<\x90&y\x03D\xde\x1dl\xb6\xbbtp\xf4S\xd1\b\xaeO\x134\xf9\x8c\xc7\x150x½\xf7X\x98\x04b\x0e\xb3\xceߍ\x86OC\xe2\xb8ԋw\x8fq\xef\xc0\x84\\\xca\xec\xecTQ\xf0\xcf\x13F\xfc\xfd\xd8\xd3# \xed)D\xb8\x9e\x92\xf4\xc2\x11\xc2E\xde\xe9\xc4\x03\x97\x17\xabu\xd1\xb7]o\x87@\x9c\xd5ۡ\x05\x1aCwJj\xe6\xae\a\x80Le\xed8\xbb\xbd7Rs\x84\xe7\xb3B\nP1\xf7I/2\x9f\xc1\x8f\xf6\xbdK#e\xf0(vǻ.I\x9c\xedNH\xcf\x12}Y\xb4\xed\n\v\x97\x14\xd4;\\T\xf2I\xaag\xb9p1\xa5\x99\xcd\xd67\x8b\x9f\xac8\xbe\xa6\xd2\xe8\x8bW\xba\b\xd4\xf6\xf7\fJ!\x99\xcdG\x05\xc6SR0\xa7\x86|\x1b\xebȟ\xb3\xbb\x98Z\u007fbr\xa89\xde\xfa\xfe\xd3\xd4ަ\xbb\xf8\xac\x8e\xff\xf0\xbcE\xbbE]7\xb6.\\\x0foL\xad\xb6\xa5\xc9\xd6\x15n\x9a\x9dH~jo\xca7\xe1\x1d\xb4?\xc5}eY\tqE\x82\xcd*a}+\xaa\xae\"B\x94\xd4\x03\xb4RJ ;l\x8bM)\xa2ϕ\xce\xfb\xfd`M\xe9\xban\bS\xf5\"\x11\f=/}\xd7g\xb7.ۯ\x81\xbb\xecO\xbdӿ\xbcU,\xa1\xbc=SԞn\xa0\x9b\xa2\xd7Pl\xba\x14ke0\x8c\v\x9d\x95\xdf\x14\xf9f\n\xd1\xe3\xe5\xe7\x90lE\xcbvo\x97\xfd\u007f\xac\n\xc5h\x97Y\x88\xa0\xf2\xbcm\xf2\x04\xce\xf2ʜ\xefx^1ѓ\xc0\x0e\xcdZ҂\xd2 \xb9\x88ա\x88\xe6\xf5\xfc\x1e\x8d\xe1c\xe9\xf3\xd1G\x9f\xd5iw'\xadf}r\xa5\xba_\x89\x1e\xd1\xe0Ǧf\xd3[\xf2\xd2k\xd1\xd3\xc5\xe3c*Ї\xf5\xe5Q\xa0\xf3u\xe7\x14Ou\xa6\xc6|Be9\xb1\xab\xe8\xc5\t\xe8\x94\xda\xf1I\x15\xe3\xd9ƛ\xc4:q\xbf\x02<\r\xf2\x88\xeap\x12q\xe6+\xc1G\xd7\u007fC\xbdu\x12\x8f\xe4\xaao\xa4\x9e;\tx\xb4\xd6;Uŝ&y\xa4\u009b^\xbb\x9d\x04\xed\xea\xba\xf3\x15\xdb\xd7\xeb\xcbz\r\x17y\\\xd5\xccV]_\xe4B'\xd4U\x8f\xa9\xa6\xceR\xec\xc4\xcaiS\x19\x1dY\xf7\xd8zi\xbf\x1e:\x024\xa5J:R\x05\x1d\x818Y\x1bM\xad}\x8e\xc0\x9e1\xbb\x93R2\xf1g\xe3u\xff\xc6ʒ\xcb͐\xf3\xa9\xf21)\x1b\x83\xd2iw͞pt\x9d\xe3^X\x11[\xd2\u007f\x90\x18\tA\xea\xa4\x13\x97V-\xe1\x9d\xdc\x0f\xe0\xbaf\xe8\xa8\xcb\xdd\xffb\x85\xb6\xf5̅\xe8~\x95\xe1\xc0vA\x85\x0f\x9cL<\x10\xa6\x81c\x1f3E\x99\xa2t\xcfߝ\x8b8>\x1e\f鈴\xa6\xfd\xe7\x98\xeb\xcc\xed\xf6D\xff\xb9\xa8\x84\xe5e\xf4\x10\x97Z\xed\xb8K\x8amq\xdf\xd0\xf3\x0f徇X\xed\x1d\xa4\x8f\x0f\xcd\xf9Z\x1e\x84\x02,v*\x9eQ\b`f\x88~\xe6\xbf\t\xcc\xd4\xc2}\xe6C\x9c\xac\xe5!|;x\xe5\xce`,@\x95\xf5\xd7j\x05\x81q\xdf\x15\x9aH\x02`ԺL{\xb8\xde\x19w\xef\xfe\xacP\xefA\xed\\\x154\xb8<3\r\xc7^S\x98J\xb4\x1d\x1eA\x01\xfa/Q\x0f<\xffVc\xc0;\xe9mp\x14\xec\xc1\x1e\x1d\x1cRZm\xb4C\xfa\x99\x02\x99\x91\xa1Q\xa8R5\xb3\xe3\xf20ijR\xbbu\xcf\x1b\xfb\x1c\x1f\xfd\xcc\xfa\x1dg\x89\x80N\x8f\x81&@\xa6vߦe\xecg\xbbm\xcf\x15\v\xcdEC\xc9n`Z7\xed9\xbah\x8f\xe8\x9e=\"*:..J&SJ\x97\xecY\xa2\xa33\xc6G爐N\x8b\x91f@\x1et\xbf\xa6\xf4\xb5&\x15\x99\x92K\x14)E\xa5\xf9\xba\xe6t\xbfjB\x9fjB\xf1cn\xa7\t\xfd\xa8\xc7\xf5\xa1&\xd0\xf0L\xd1ә\xe2\xa7sDP獡f\xa3\xa8Yə\xfc\xfb\xe4\x1cy]M\xfd\xa0r\xbcW\xda\xce9\xfc\xf7\x87\xe3#\x15\xacN\x10\xa4D\x0e\xb2\x1e\x1aA\xca\xf9\xf2\xc1\x8f?\r\xa9x\xb1)\xac\u007f\xffy\x0e\x9f\x87f\xe04\"\xe4\x92\xd6\xf1Y\x04\x0f\x9a\xefp1\x92\x95f\xab,|\xb7\xe3,\\(\xa2\xaa<\x04!\xfa\xfbs`\xf9h\x99\xad\x12\x11\xf5c{\xb8\xf2l۩\xe7G\x10\xb2\x91\xb8N\x1e\x137\x8b;\xdf\xdfV\xe5\\\x9b\x1f;\xd5\x1f\xb86\xf6S\x99W\x8a\xe5\xad\xf1l\xad\xe6bW\xe5L5\xf5g\x00:\x91%^\xc1G\x1a\xaad\t\xa6g\x00~bv\xe8\x15\xb04\xb5\xa4b\xf9gŅAu-\xf3\xaa\b$ZA\x8a:Q\xbc4\x96\x14w\x86\x99J\x83܂ɰ=\x0e\x95\x9f\xb5\x14\x9f\x99ɮ`\xadm\xbbu\x991\x1d\xbe:\x129\x00\xbe\xca\x1c\b7m\x14\x17\xbb\xa1\xd1\xde\xc1\xb5\x92\x02\xf0k\xa9P\x13ʐZΊ\x1dCș6`x\x81\xc0\xfc\x80\xf0Ĵ\xc5a+\x15\x98\x8c\xeby\x9a\x10\x90\x0e\xb6\x0e\x9d\x0f\xfdj\x87P\xca\fztZ\xa0\x82T\xaf\x8f$\xb2\x03\xf3\xdd\x0e\x87\x81\xb9\xcf\xfb\xb7Nn\x92\f\vv\xe5[\xca\x12ŻϷ\x0f\u007f\xbd\xebTCO\x0e\xfc,\x81k`\xf0`\x85\x1a\x94_~`2f@!q\r\x85\xa1\x16\xa5\xc2U\xa0LZ\x83\x04\x90\nJT\\\xa6<\t\x14\xb5\x9du&\xab<\x85\r\x12q\xd7u\x87R\xc9\x12\x95\xe1aٸ\xd2R\x13\xad\xda\x1e\xc6ohR\xae\x95\x93\"\xd4Vp\xfcb\xc0\xd4\xd3\xc1\xc96\xd7\r\xfe\x96\xc0\x1d\xc0@\x8d\x98\x00\xb9\xf9\x19\x13\xb3\x86;T\x04&`\x9dH\xb1GE\x14H\xe4N\xf0\xff\u05305I\xac\xb1\x82dЯ\xe5\xa6\xd8\xc5'X\x0e{\x96Wx\x01L\xa4P\xb0\x03(\xa4Q\xa0\x12-x\xb6\x89^\xc3?\xa5B\xe0b+\xaf 3\xa6\xd4W\x97\x97;n\x82zLdQT\x82\x9bå\xd5t|S\x19\xa9\xf4e\x8a{\xcc/5߭\x98J2n01\x95\xc2KV\xf2\x95E]X\x15\xb9.\xd2?\x05\x8e\xea7\x1d\\\x8f֊+V\x89Mp\x80\xb4\x99\x13\x18\xd7\xd5͢!4U\x11u\xbe\xdc\xdcݷ\x85\x89\xeb>\xf5-\xdd[\x12ְ\x80\b\xc6\xc5\x16\xfdj\xdc*YX\x98(\xd2Rra\xec\x8f$\xe7(\xfa\xe4\xd7զ\xe0\x86\xf8\xfeK\x85\xda\x10\xaf\xd6pmm\x06\xc9aU\xd2\xeaI\xd7p+\xe0\x9a\x15\x98_3\x8d/\xce\x00\xa2\xb4^\x11a\xe3X\xd06w\xfdƎj\xad\x0f\xc14\x8d\xf0+\xac\xf1\xbb\x12\x93Β\xa1~|\xcb\x13\xbb0\xac\xe6\xabU@O\xfb\xb92\xbcj\xc1\xab\x1ejޯ\x9f\xd46\xb16\xe1\b&x\x15\xb3>\xfa2BM\xfb\t\x8b\x92\x96\xeb\f\x8a\xf7\xbe\x19\xa1H4Jk\x17$\x18ˠޤ\xd7jp\xa4T\xecp\x19\x12\xbd\xf6<\xf5Z㈚\xd3\x14\xa5\x92h~'X\xa93i\xc8.\xc8\xca\f\xb5\xeaM\xe0\xfa\xee\xb6\xd7)\xf0\xd9s\xddڽJcJSxb\xbc\xbf~B!y\xb8\xbe\xbb\x85\ar#0\xc0\x04g\xfd\xc0TJX5\xf8\x05Yz\xb8\x97?i\x84\xb4\xb2\xda ز\x8b\x11\xc0\x1b\xdc\xd2bSH0\xa8\x03*E\xb2\xa7-j\xb22kk\xa4Sܲ*7^\xb9p\ro\xbf\x83\x82\x8b\xca\xe01\xdfa\x9a\xf7\x8eH\x16\x9c\x9b\x8d\xbe\x97_P\x1b\x9eD\x10\xf4\xfd`\xc7\x16Q\x9f24\x19*\xd2t\xf6\x835\x1e\xa3s\xafIo\xd8#\xf9\x1f\x1b'Nd\x88\xf2\x1cJ\x99\xc2ލ\x04\x9bC@zj\xc2\x1b)sdC\"\x88_\x93\xbcJ1\xad}\xc6A!\xeb\xcd\xf6樓\xf5\xae\x19\x17\xb4dɗ%TE\xfdud\x9e\xd6\xf83\x85@Z\x97\v\a\x13\xb8\xf3\xf16#\xab\x97\n7X\x8c\xe09\xcbb\xb0^<\xdb\xe4x\x05FUC\x8a#\xc0`J\xb1\xc3\x04\xcdB\x04\xb2\x84du\x1fo\x1bs\x9e \x11\xab\xb6\x80\x96j\x964#\xf3\xfb?$X&\xe5c\f\x91\xfeA\xed\x1aK\x0f\x89\r\xf4`\x83\x19\xdbs\xa9t\xdf]į\x98T\x06\xc7\xd6\x113\x90\xf2\xed\x16\x15\xc1\xb2\xd1I\x1d\xccL\x11kZ\xdfRQӌ?\x9aW\xc3tb\x9e\xa5\xc6\xd8T\xac]\x1b\x85\n\x16qR\x87U\t\\\xa4|\xcfӊ\xe5\xc0\x856L$n~\xac\xc6ox~0'\x10G\xf8;k\x16fA\\\xea\xb8\tR \xf9\xf6\x85T\xc3\xc2\x11\xca1\x98q2l\x18i@9fۛ\xa2($\xf6\xa8\xa4\xd6\x1e5z\xe7\xa2\xe1\x94\xf3\xb0s\xb6\xc1\x1c4\xe6\x98\x18\xa9\xc6\xc9\x13#\x04\xae\xc4\xea\xcf\x11\xca\x0ehҮ!\x9eU\xa2M!K\x9d\xf1$s\xce0I\x99\x85\x05\xa9Dm5\x06+\xcb\xfc05i\x88\x91\f?\u061c\xd2hJ\x84\xfa\xe8\xc3\x1dS$M\x89\xd4\xc1M\x99\xd1\xc6]\xaa\xd7b\xf3J\xf4\x0e\x9a⛄\xfd\xf6\xa8\xfb\xf3\v;\x91\x9bS\xb0|\xbb\x05,Js\xb8\x00nBm\fTr\xb0\x1a<\xfe`\x8c;m\xb5\xdc\xf6{?\xfbjy\x16\xae\xd5h\xfcA\x98f\x8d՝\xb7U\x8b\x18\xf6\xa1\xdd\xf3\x02\xf8\xb6fXz\x01[\x9e\x1b\xb4\xbe\xd4\x1c\xa2-Gg\x96s\xcfI\xa0X\xdbK\xa5`&\xc9n\xea\xfd\x81\x88\x1e=Z\xf5\x018\xbf<\xc40\x96\a\x11 \xa1v*\xec\x96\x12WX\xb8\xad\xaa{\xbb>\x9a\x1a\xeb\x01\xbe\xfb\xf8\x1e\xd39\x92A\xbc\xa4\x1eM\xea]\xcf\xd3i\xa3`'\x18\x05\xb25)\xeb\xa6\xd51\x9eې\xbc\x00\x06\x8fxp\x9e\xd5`p9T\x88\xb5\xac\x06\xa9\xd0\xee\x8eZ5\xf2\x88\a\v\xcaowF\xc1[\"*\xae<\xe2!\xb6i\x8f\xa8\x84\x9f\xdf\xf0qԥ\n;\x8b\x98\xa5Ԕ\x9a\xa8~퀑q\x93\x85eJ)\x94@\xf1\x13\xa7]3\xac\xb3\xc7\xff\x88\x877ڱ\x8fVM\xc6\xcb\x05\x14 \x85\r\x1a\xed\n\v\x9b\xdb\x0f,\xe7i=\x98]'\v ފ\v\xf8(\r\xfds\xf3\x95kBQ\xa4\xf0^\xa2\xfe(\x8d\xadyQ\x12\xbbI\x9cH`\xd7\xd9.K\xe1\xcc\x02\xd1e\xd1\xf8\r\x0eք\x92\x88\xd6l\xe3\x1an\x05\xc5g\x8e>Kؔa@ΡUT\xdan\x8f\v)V\xd6L\x87\xd1\x16\x00m\xe3\xe5Y%U\x87S\x17\v!\x0e\xa2\xe8ѻ'k\xe5\xbe\x1c\x1d,L\x15\x85e\xce\x12L\xc3v\xa5=\xc5`\x06w<\x81\x02\xd5\x0e\xa1$\xbb\x11/T\v4\xb9+'Ha\xbck\x11\x8a7\vi\x1cb+Z\xf5\x91-\x03\x9b\xa3\x9a\x8f\x1cYL7\x8f\x9b\xa55\xef\xd6\x1f\x8a\xa2~\xfb||\x99eYȯc\x1f\xc4!\xe9\u070f\x82\xd9\xcd\xde_ɼZ\xf1\xfe-\xce\x1a2\xae\xf4\x1a\xde\xd9\xec\x80\x1c\xdb\xfd\xc3.ak\xa8(\x90\x84\t\xd7@r\xb2g9\xb9\x0f\xa4\xbc\x05`\xee\x9c\t\xb9=\xf2\xa0\xe2T\xccS&\xb5\xb3\xf9[\x8e\xb9=*<\u007f\xc4\xc3\xf9ő\xf6:\xbf\x15\xe7q0I\xe7\x1f)\xad\xdak\x91\"?\xc0\xb9\xfdvn\x1d\xb3%K\xe4\x04\xe7m\x81TG7\xb5g\xf9KB\x01\x8a\xb5\x83\xd7B\x9d\xeb\x13or\xe1\xe7f\x11-ӥ\xd4#\xc7N#h}\x96ڸ\r\xc0\x8e\xbb=\xb0C\x18\x13\xfd\xf9]C`[\x83\n\xb4\x91*\x9c.\x93\xda\xedm\x90\x13\xe7\xf5<\xef\x89\xd5\xf5n\xa4\x03LA\xe6y\xa3!\x9cN?w\xc7\xce\xf4\xf7<\xcc\xc4:K\x16v\xa9d\x82ZϋR\xa4\xe5\x98ٰ\xad7k\x99\v\u07b6Q\xaa9f+9\x94e\xae8\x91\xf6\x84\xc0\xe6\xe6kkߙ\xd4\x10\xfd\x8e\x11\xe5Sp\x04\x9b5V\x14\xac\x9f\xe9\x10\x8d\xee\xb5\xeb\x1d\x16\xa0\a\xe6\x02&\xb5\xab\xacRY\xe67{\x91\xfc\xbd9\x1e\x05\x17\xb7v x\xfbb\xce\n\x04U\x8e\xa7\x862ס\u007fÐ\xba\"6~\x85pn/\xedY\x8d\xc2\x0eg\x8fO2\xe29\x05\xe4L\viڛ5~\xa47\x1a\xb6\\i\xd3 \xbc\x00*\xd7\xf68\xf9ecLq\xa3\xd4\xc9!\xe6'\u05fb\xb5\xad\x98\xc9'\x9fe\xb2$\xb0\x0e\xc4\xcf\xd8\x1e\x81o\x81\x1b@\x91\xc8J\xd8\r/R\x174\xcc\x02\x88\x8e\x89ΘD\xda\xccVgQ\x15\xf1\x04YY\xe9\xe4bvw\xac\xdd\xe5\a\xc6\xe3v\xa7\xe04\xb6\x9a\xa9\x8c\x92\xa1\xd2M\x93\xf1\xa9%\xedt\xa2\x82}\xe5EU\x00+\x88-K\xe2ƭKJ\t\xb9G\x8e\xd7O\x8c\x1b\x9f\x8e\xe9\x0eV\x97i\xd3D\x16e\x8e\x06C\xbaI\"\x85\xe6)\xd6\xee\x83\xe7\xff`\xf2\xceXa\xb0e<\xaf\xd4\x02\x1d\xbd\x983K\xe36\xaf\x9e\x9e?\x18\x8bGde\x89\x19\xb9\xe9\xbe\xc0i\x9e\xb7\x1f\xa5Z\xe62\u007fV\xf8\xfc\xaei\xa98I\xa9\x9c\xf3NgaZ\xef\xb5\xeb\x9dz\xe1e\xe20\xe6\x9e\xceB\xb5\x98\xbc\xba\xa7uyuO_\xdd\xd3W\xf7\xb4W^\xdd\xd3W\xf7\xf4\xd5=\x1d.\xaf\xeei\xab\xbc\xba\xa7\xd1\xf6#\x06Õݹ\x9dh\x10\x85Ud\n\xc6\x1c\xda3c\xf9L\xa3\xeb\xbc\xd2\x06Ւ\f\xe9\xdb\xe1\x9e\x039\xf4\x89k\xb2\xb2\xd7\x1dǤ\xa6I]i\x8c^\x9d2MK2,&w\xb1%\xc2\v\x8fN\x83\x1e϶\x8fM\xa0\x9bK\x9b\xeb\xe6\x8e\xd7\xe9j\xee\xaf\x11\x82\x18\x19\x86\xf7\xdcs\x17\xa6\xda9W\xdd\xdc7\x1b\a\x04\x8c\u007f\x97y\xe5\x91im3\xc9lӉ\xf8c\x16>вw\xb8\xd0%\xa6\xea$~\xff\xaei\x19\x91m6\x9ec\xe6O-Ѱ\xfd\xdbu\xf7\x8b\x91>\xe3ldfO\xdcd\xee2\x17\x85\xaeb\xd7Nk\x0fr\xea/R\xf6i<\x02Q*\x10\x11\x90*E5{\xb8\xb2D4g\x85\xb2\x17\x13u\xc7\xef\xbd:\x10\xdeĢV탛1\xee\xc8\xfa\x16|\x02?r\x91:ސ\x10\xb6\xfc\v\xfbj\xa1\xbd'S;>\xe3b\xd4x\x9b\xbdC#\x8d%#5j\x83\"{\xb8\xad\xd7pÒ\xacn8\x02ю\x9c1\r[\xa9\nf\xe0\xbc>\x8d\xbb\f=\xa9\xe6|\r\xf0\x83\xac\x0fB[\xaf܌\xc0ռ(\xf3\x03E?p\xde\x05\xf4m\xa23*~ڿ\x04\xe7\x1fD\x8b\x88\x80\xef\xba=\x06\x8e}\xc3shI.\xab\xb4\x1ea\x82\xddL\x1c\xe0\xf3\x83\xf5\xa6\xec#PI\xf3X\x96\xf7\x95B$\xdc{Kk\x04\xe4\u0603\x82\x8bH6~8\xac\x8dTl\x87\x1f\xa4{k1\x86f\xdd\x1e\x9d\xe76\xbd\xae\n\xa9\"\xfe\xeeר$\xbb\xb9\xf5\x016\x19d~\xb55g\xe9\x84\xed\x98\x12\x9bY\xe7\xc6\xe4\x11\x93\xbb\xbf\xff\xe0&dx\x81\xeb\xf7\x95;\xa8_\x95Li$J\x87\x89\xbaN\x9bq;\x97\xc9'ȥ\xa7\xc3\xf7\xfdy(\xb4\x19k6'\xe0\xa4\xd9\xec;O\x1f\x06\xd2ň\xfc\xc3p\xcfV \xdbb\xe2\xd4Ѿ\u070e\xc2bZ˄[]d\xb7\x84l\xa2\xd8\xcb=\x157eQ&TF\xa5\xf1ӓ@\xf5%,T}+\x1c\xa7f^\xe2\xfc\xe9\xa8c`\xf0\x90\xfa \xfd\xd7k>d\xf7\x84'\x90v\xafT\x86\xbd-\xae\xeb\xb7I\x8fI7\xb3\xfe\xc7\xd7\xfe\xb0\xef\xba\x1a~\x0etU\xbfPz\x16AY\xf7\ng̣\xaf\xee\xb9΄\x95\xa6R\u07bc&\x95\xb2\xef\xe6\x11\x10t\xcfʝ\xf6\xeck\xf3\x04\xf6\f/\x9bG\xb1\x9bh\u007f\xf6\t\xee\x01\xfe\xd5\x0fȎ\xbe\xa8ꬫ{\"{E\xf0Oc\xe7\xe0:\xb0\xef\f\xce\xcc\xf43\xb5\xa9\x93|=\xa1m\xc7\xf0>\xe1\xdd\x18\xea\xc3Y\x9b+\xf8\x88O\x03\xb57\x82&q|\xa6\xe6R31\xb5{\x04CO^ONq_\xf7\xb2y\xb1\x03ڢ\xab\xe6z\xcd{\t7,\xcf[\x10]\x0e\xec\x10[\xff̷n\x03'\xa19\xfd\xe5\xa8Ũ\xe2\x9aTZc\nkpI\x1dUjT{L[B\xe2mx\xbb\xa6\xda4\xcfE¯\xbf\x9d5\xab\x92%\t\x96\xc6'v\xb5\xffk\x80\xf3s\xfb#\xbc\xfco\u007f&R8G[_\xc1\xbf\xfe}\x06\xde\x00?\x84\xe7\xfd\xa9\xf2\xbf\x01\x00\x00\xff\xff\x9dJq\x1dHa\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ko\x1c9sw\xfd\x8a\x82rp\x02hFk\xe4\x90@7\u007f\xb2\x16\x11ֱ\rK\xabK\x90\x03\xa7\xbbF\xc3U7\xd9K\xb2G\x9e,\xf6\xbf\aUl\xf6k\xfa\xc1\x1eK\xc8f!^\xeca\x93\xc5bU\xb1\x1ed\x91:[\xadVg\xa2\x90\x0fh\xac\xd4\xea\nD!\xf1\xbbCE\xbf\xec\xfa\xe9\xdf\xedZ\xea\xcb\xfd\xfb\xb3'\xa9\xd2+\xb8.\xad\xd3\xf97\xb4\xba4\t~ĭT\xd2I\xad\xcert\"\x15N\\\x9d\x01\b\xa5\xb4\x13Tm\xe9'@\xa2\x953:\xcbЬ\x1eQ\xad\x9f\xca\rnJ\x99\xa5h\x18x\x18z\xff\xd3\xfa\xdf\xd6?\x9d\x01$\x06\xb9\xfb\xbd\xcc\xd1:\x91\x17W\xa0\xca,;\x03P\"\xc7+\xb0\xc9\x0e\xd32C\xbb\xdec\x86F\xaf\xa5>\xb3\x05&4ڣ\xd1eq\x05\xcd\aߩ\xc2\xc4\xcf\xe2\xae\xea\xcfU\x99\xb4\xee\x97N\xf5'i\x1d\u007f*\xb2҈\xac5\x1e\xd7Z\xa9\x1e\xcbL\x98\xa6\xfe\f\xc0&\xba\xc0+\xf8LC\x15\"\xc1\xf4\f\xa0\x9a\x18\x0f\xbd\x02\x91\xa6L*\x91}5R94\xd7:+\xf3@\xa2\x15\xa4h\x13#\vǤ\xb8s\u0095\x16\xf4\x16\xdc\x0e\xdb\xe3P\xf9\xcdj\xf5U\xb8\xdd\x15\xac-\xb7[\x17;a\xc3WO\"\x0f\xa0\xaar\a\xc2\xcd:#\xd5\xe3\xd0h\x1f\xe0\xdah\x05\xf8\xbd0h\teH\x99\xb3\xea\x11\x9ew\xa8\xc0i0\xa5bT\xfe!\x92\xa7\xb2\x18@\xa4\xc0d\xddó¤[9\x87\xcb\xfd\x0e!\x13ց\x939\x82\xa8\x06\x84ga\x19\x87\xad6\xe0v\xd2\xceӄ\x80t\xb0\xf5\xe8|\xeaW{\x84R\xe1\xb0B\xa7\x05*H\xf5\xfaH\";0?\x9a\x1a\xf6\x00?|\xfe\x88\xe9\x1c\xc9 ^R\x8f&\xf5\xa1\xe7\xe9\xb4Q\xe0\tF\x81lM\x8aݴ:\xc6\xf3\x1b\xa3\x17 \xe0\t\x0f\u07b3\x1a\f.\x87\n\xb1V\xd4 \r\xf2.-\xab\x91'<0\xa8j\xdb5\n\xde\x12Q\xf1\xe5\t\x0f\xb1M{D%\xfc\xaa\r\x1fO]\xaa\xe0Y\xc4,\xa5\xa6\xd4D\xad\xd6\x0e8\x1d7YX\xa6\x94B\t\x14?q\xda5\xc3:g\rOxxg=\xfbh\xd5\xecd\xb1\x80\x02\xa4\xb0\xc1\"\xaf\xb0\xb0\xc9\xfe 2\x99փ\xf1:Y\x00\xf1V]\xc0g\xed蟛\xef\xd2\x12\x8a*\x85\x8f\x1a\xedg\xed\xb8\xe6UI\xec'q\"\x81}g^\x96ʛ\x05\xa2ˢ\xf1\x1b\x1c\u0604\x92\x88\xd6l\x93\x16n\x15\xc5g\x9e>KشÀ\x9cG+/-o\xd3+\xadVl\xa6\xc3h\v\x80\xb6\xf1\xaaX\xa5M\x87S\x17\v!\x0e\xa2X\xa1wO\xd6\xca\u007f9:\xe0\x98*\x06\x8bL$\x98\x86\xedJ>M\x11\x0e\x1fe\x029\x9aG\x84\x82\xecF\xbcP-\xd0侜 \x85\xf1\xaeE(\x95Y\x188\x1c\x18*+Z\xf5\x91-\x03\x9b\xa3\x9a\x8f\x1c\x9dL7\x8f\x9b%\x9bw\xf6\x87\xa2\xa8\xdf>\xeb_fY\x16\xf2\xeb\xd8\a\xf1Hz\xf7#\x17\xbc\xd9\xfb\a\x99W\x16\xef?㬡\x90Ʈ\xe1\x03g:d\xd8\xee\x1fv\t[CE\x81$L\xa4\x05\x92\x93\xbd\xc8\xc8} \xe5\xad\x003\xefL\xe8\xed\x91\a\x15\xa7b\x9ew\xdaz\x9b\xbf\x95\x98\xf1\x91\xe5\xf9\x13\x1e\xce/\x8e\xb4\xd7\xf9\xad:\x8f\x83I:\xffHi\xd5^\x8bV\xd9\x01\xce\xf9\xdb9;fK\x96\xc8\t\xce\xdb\x02\xa9\x8en\xca9\x05KB\x01\x8a\xb5\x83\xd7B\x9d\xeb\x93wr\xe1\xe7f\x11-Ӆ\xb6#\xc7N#h}\xd5\xd6\xf9\r\xc0\x8e\xbb=\xb0C\x18\x13\xfdU\xbb\x86 \xb6\x0e\rX\xa7M8\xe5&\xb5\xdb\xdb '\xce\xdby\xde\x13\xab\xeb\xddH\x0f\x98\x82\xcc\xf3FCx\x9d~\ue3ff\xe9\xff\xf30\x13v\x96\x18vat\x82\xd6\u038bR\xa4\xe5\x98ٰ\xad7k\x85\x0f\u07b6Q\xaa9f+9\x94e\xae8\x91\xf6\x84\xc0\xe6\xe6{kߙ\xd4\x10\xfd\x8e\x11\xe5Sp\x04\u0380\xcbs\xd1ϸ\x88F\xf7\xda\xf7\x0e\v\xb0\x02\xe6\x03&\xf3X\xb2RY\xe67W\"\xf9Ws/mi6Z\x9d?4oU~ \am\xd9\x05\x81\xd9|\xb3\x18\xa4!&\xcbl8\u007fl\x06\xea\x92ܲ\xd8\x18<\"\x8f,>{,\x8e<\xc0װcsƢ\xbd\xb6\xd8\xfc\xb0\xd7\xc9\n\x8b\xcc\x05kex͂<1\x03,\x9a`q\xd9^\xd19^\xaḓyjMdv\r\xe7k͂\x1c\xca\xe7\x8a\xc9Ҋ\xc25:7\xabθ\x9a\xdfI\xfc\xa1\x8c\xac\x97\xcf\xfd~I?\u007f:\xbf**\xab**\x16\x98\xc79*oji\xb6T\x14U\x97fF\xd5YO\x13\x03G\xe5C\x1d\xe7:MMe6\vj<\xc3i\n\xecP\xeeSD^\xd3\x04\xc8v\xc6\xd3b7`V\x9af\x1a\f?\x95\x10ʼ\xad\xcd\xfe/$\xf0G'\xadM\xc7\x05\x8e\x89J\xbe\xf4\xba\x10\xef\x83\xd77\xe4V\x8f\xc7x\xde\xd9>\xc1\xad\x1e\x01y\xbb\x85\xbc̜,\xb2\xd6[\x05n\x87\ax\x96YF\xea\xfc7\xcdW/7\a\x86\xf6\xe5[-\xc0c \xbb\x01\x82\xb0\xf0\x8cYF\xff\x1eQ!\xf1/\x83$z\x85dsƷ\xc1\xab\v\xf2ճ\"\x17>\u05cf沈=\xcb\tR\xb8W\u007fB\xf85\xed\xecz\x1f\x9d\xeb~/\xd1\x1c@\xef\xd1\xd4^ͨ\x985\xf7\x95\xaa\xa5iˬQ%\x95N\xf2\xef\xd3tU\xcb\xf8j\xa8\x174|P\xde\xcc\xf6qeX\xa4C\x9a\xe0hJuR,4\x06B\xe9\x1a\xc2H\xff\x18_z\xc9\x05\x9e\xd7\b\x95^\"X\x8ar+^#`z\xad\x90iiд\xe4\xe82\xea\x02\xcek\x84NK\x82\xa7E\x1e`\xfc\x05\x9b\u05faX\xf3\nA\xd4\xc9a\xd4\"\xd2\xc5^\x9cY\x1cLE\xcco\xe6\xa2̑\xc7\x15\x01r\xf4\x82\xccp@\x15\x01\xf1\xe8b\xcclH\x15\xb3\x0e\xfaA\xd7\x0f_s\x89>\xc6_t\x96\x14{\x04\x1fw\xcc3\u007f}%\xf2\xdaJ\xe4!P\f\xf6\x91\xd7S\x96_K\x89\xa4\xf3\x89\xc1\xd6\xe4Б\xd7O\x16\x85['\x06\\\x93\x10\xa7\xae\x9bL\x87\\\xd3\xdbi\xfdk&'\xb8\x13\x11\x126\xdb\xe4\x87O\x04\xb4I\xd1\xcc\x1e\xae,\x11\xcdY\xa1\xec\xc5D\xdd\xf1{\xaf\x0e\x847\xb1\xa8U\xfb\xe0f\x8c;\xba\xbe\x05\x9f\xc0/R\xa5\x9e7$\x84-\xff\x82_O\xe4{2\xb5\xe33.F\x8d\xb7\xd9;4\xb2X\bR\xa3\x1c\x14\xf1\xe1\xb6]ÍHvu\xc3\x11\x88<\xf2NX\xd8j\x93\v\a\xe7\xf5i\xdce\xe8I5\xe7k\x80\x9fu}\x10\xdaz\xe5f\x04\xae\x95y\x91\x1d(\xfa\x81\xf3.\xa0\x1f\x13\x9dQ\xf1\xb3\xd5KpՃh\x11\x11\xf0]\xb7\xc7\xd0K\x84\xd5shI\xa6˴\x1ea\x82\xddB\x1d\xe0\xeb\x03{S\xfc\bT\xd2<\x96U\xf9J!\x12\uef655\x02r\xecA\xc1E$\x1b?\x1c\xb6N\x1b\U000487f4\u007f\xf31\x86f\xdd\x1e\x9dg?+]\x15RE\xaa\xbb_\xa3\x92\xec\xe7\xd6\a\xd8d\x90U\xab\xad9K'lǔ\xd8\xcc:w.\x8b\x98\xdc\xfd\xfd'?!'s\\\u007f,\xfdA\xfd\xaa\x10\xc6\"Q:L\xd4wڌ۹\x9d~\x86LWt\xf8G\u007f\x1e\x069c\x8ds\x02N\x9a;\xf3\xf4a ]\x8c\xc8?\f\xf7l\x05\xb2-&N\x1d\xed\xeb\xed(,a\xadN$\xeb\"\xde\x12\xe2D\xb1\xd7{*nʢL\xa8\x8c\xd2\xe2\x97g\x85\xe6[X\xa8\xf6VyNͼ\xc4\xf9\xebQ\xc7\xc0\xe0!\xf5A\xfa\xaf\xd7|\xc8\uea4a@ֿR\x19\xf6\xb6\xa4\xad\xdf&=&\xdd\xcc\xfa\x1f_\xfbþ\xebj\xf89\xd0U\xfdB\xe9Y\x04e\xfd+\x9c1\x8f\xcf\xfa\xe7:\x13Q\xb8\xd2T\xe65)\r\xbf\x9bG@\xd0?+w\xda\xf3\xb3\xcds\xde3\xbcl\x1e\xf8n\xa2\xfd\xd9\xe7\xc4\a\xf8W? ;\xfa\xa2\xaa\xb7\xae\xfe\xb9\xef\x15\xc1?\x8d\x9d\x83\xeb\x80\xdf\x19\x9c{l\x97\xda\xd4I\xbe\x15\xa1\xb9cx\x9f\xf0n\f\xf5\xe1\xac\xcd\x15|\xc6\xe7\x81\xda\x1bE\x938>S\U000e9658\xf2\x1e\xc1\xd0\xd3ۓS\xdc\u05fd8/v@[t\xd5\\\xafy/\xe1FdY\v\xa2ρ\x1db\xeb?˭\xdf\xc0IhN\xffr\xd4bTqM*\xad1\x855\xb8\xa4\x8e*-\x9a=\xa6-!\xa9lx\xbb\xa6\xdc4\xcfE\xc2\x1f\u007f\x9e5\xabR$\t\x16\xaeJ\xecj\xff\x99\x83\xf3s\xfe\x11\xfe\x8a\x01\xffL\xb4\U0008edbd\x82\xff\xfa\xef3\xa8\f\xf0C\xf8S\x05T\xf9\xbf\x01\x00\x00\xff\xff\x16'-~\x14b\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4VO\x8f\xeb4\x10\xbf\xe7S\x8c\x1e\x87w!\xe9{\xe2\x00\xca\r\x15\x0e+`\xb5\xda>\xed\x05qp\x9di;\xacc\x9b\xf1\xb8K\xf9\xf4\xc8v\xb2m\x93\x94]\x90\xf0-\xf6\xfc\xf9\xcdo\xfed\xaa\xba\xae+\xe5\xe9\t9\x90\xb3-(O\xf8\xa7\xa0M_\xa1y\xfe.4\xe4V\xc7\xcf\xd53ٮ\x85u\f\xe2\xfaG\f.\xb2\xc6\x1fpG\x96\x84\x9c\xadz\x14\xd5)Qm\x05\xa0\xacu\xa2\xd2uH\x9f\x00\xdaYag\fr\xbdG\xdb<\xc7-n#\x99\x0e9\x1b\x1f]\x1f?5\xdf6\x9f*\x00͘տP\x8fAT\xef[\xb0ј\n\xc0\xaa\x1e[\b\xc8II\x94\xc4\xc0\xf8G\xc4 \xa19\xa2Av\r\xb9*x\xd4\xc9\xf1\x9e]\xf4-\x9c\x1f\x8a\xfe\x00\xaa\x04\xb4ɦ6\xd9\xd4c1\x95_\r\x05\xf9\xe9\x96\xc4\xcf4Hy\x13Y\x99e@Y \x1c\x1c\xcb\xfd\xd9i\r!py!\xbb\x8fF\xf1\xa2r\x05\x10\xb4\xf3\xd8B\xd6\xf5JcW\x01\fLe[\xf5\xc0\xc5\xf1s1\xa7\x0fث\xe2\x04\xc0y\xb4\xdf?\xdc=}\xb3\xb9\xba\x06\xe80h&/\x99\xef\x85Ȁ\x02(\x18P\x808PZc\b\xa0#3Z\x81\x82\x12\xc8\xee\x1c\xf79G\xaf\xa6\x01\xd4\xd6E\x019 d\x05\xd9*\x03Ge\"~\r\xcavЫ\x130&/\x10텽,\x12\x1a\xf8\xc51f2[8\x88\xf8ЮV{\x92\xb1\xeb\xb4\xeb\xfbhIN\xab\xdc@\xb4\x8d\xe28\xac:<\xa2Y\x05\xda\u05ca\xf5\x81\x04\xb5Dƕ\xf2Tg\xe86w^\xd3w_\xf1Ч\xe1\xe3\x15V9\xa5\xca\n\xc2d\xf7\x17\x0f\xb9!\xfe!\x03\xa9\x1dJ}\x14\xd5\x12ř\xe8t\x95\xd8y\xfcq\xf3\x05F\xd79\x19S\xf63\xefg\xc5pNA\"\x8c\xec\x0e\xb9$qǮ\xcf6\xd1vޑ-ե\r\xa1\x9d\xd2\x1f\xe2\xb6'\tc\xed\xa6\\5\xb0Σ\b\xb6\b\xd1wJ\xb0k\xe0\xce\xc2Z\xf5h\xd6*\xe0\xff\x9e\x80\xc4t\xa8\x13\xb1\xefK\xc1\xe5\x14\x9d\n\x17\xd6.\x1e\xc61w#_\vݽ\xf1\xa8S\x06\x13\x89I\x9bv\xa4s{\xc0\xce1\xa8%\x95\xe6]H\xb2ƿ\xc42L\x92\x82f2_R\u007f\xbe\x8dfy\x9c䗃\n8\xbd\x9c`zH2S\xff\x86v\xa8O\xda`1Q\xa6\t\xbe\r%\x1d\xb4\xb1\x9f\xfb\xac\xe1\x1e_\x16n\x1fإɚ\xe7\xfa\xf5\xb9Q\x1bP\xfe7{\xb2\xb3p\xa7\x91\x15\xa9\xfc\x0f\xbb\x1c\xd5\x17\x03z0\x04\x1c\xadM};\x9b\x90\x19\xc8t\x92\xcfdH\xb0_@\xb3\x88\xe7\xce\xee\\\xde\x04Tr\xac\xa4\xf4\x13\x0e\xc9\x1e\xfc\x14\\\v\x06o纜\xf9\xf0z\x17\xa1\xe5\xe4?\xe9\u007fSN\xe3\x86\x18\x17}\xd7\x19\xd5\xe2C\xf2\xb8\xc4\xf8r\u007f\r(\xa31jk\xb0\x05\xe18\xd7.\xba\x8aY\x9d\xa6U3\x96\xday\x9fz\xa3\x80f\n\xa9O^\x0ehou\x03\xbc\xa8锿\xf2\f\xdb\xd3-\xd5\xf5\xebr8o\xa9R\xba-\xa4\xd9]\v-p\xf6.R\x16\xb3WJzq\xf3\x98\x11\xb2\xb9\x94\x1dg\xc6Uk\x8c\x8b\xc8<\x86\x9b\x10\x16\x93=\xbb\xcc滋\xf0\x828V\xfb1\xe0\xf3\xe8M\x9b\x9a\x17\xec\xee\xa7+\xee\x87\x0fW\xbbj\xfe\xd4\xcevT6t\xf8\xf5\xb7\xaaX\xc5\xeei\\0\xd3\xe5\xdf\x01\x00\x00\xff\xff\xfb\xb1p\x12\x1b\f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x8f۶\x13\xbd\xfbS\f\x92\xc3^\"9\xc1\xef\xf0+t)\x82M\x0fA\xf3g\x11o\xf7R\xf4@\x93#\x8b]\x8aTgHm\xddO_\f)\xad\xbd\xb6\x9cl\x8aV\x17C\x149|\xf3\u07bc!\xbd\xaa\xaaj\xa5\x06{\x87\xc46\xf8\x06\xd4`\xf1ψ^\u07b8\xbe\xff\x81k\x1b\xd6\xe3\x9bս\xf5\xa6\x81\xeb\xc41\xf4_\x90C\"\x8dﰵ\xdeF\x1b\xfc\xaaǨ\x8c\x8a\xaaY\x01(\xefCT2\xcc\xf2\n\xa0\x83\x8f\x14\x9cC\xaav\xe8\xeb\xfb\xb4\xc5m\xb2\xce \xe5\xe0\xf3\xd6\xe3\xeb\xfa\xff\xf5\xeb\x15\x80&\xcc\xcbom\x8f\x1cU?4\xe0\x93s+\x00\xafzl`\f.\xf5\xc8^\r܅\xe8\x82.\x9b\xd5#:\xa4P۰\xe2\x01\xb5콣\x90\x86\x06\x0e\x1fJ\x88\tW\xc9\xe9.G\xdbL\xd1>L\xd1\xf2\x04g9\xfe\xfc\x95I\x1f,\xc7\xe8\xcdK\x9a,\xcaWO\xb0ƽT\x11G\xb2~w\xf4!\x1b\xe1+\n\x88\aJ!\x94\xa5%\x8b\x03\xd12$\xec|\xf9is\v\xf3\xd6Y\x8cS\xf63\uf1c5|\x90@\b\xb3\xbeE*\"\xb6\x14\xfa\x1c\x13\xbd\x19\x82\xf51\xbfhgџ\xd2\xcfi\xdb\xdb(\xba\xff\x91\x90\xa3hU\xc3u\xeeB\xb0EH\x83Q\x11M\r\xef=\\\xab\x1eݵb\xfc\xcf\x05\x10\xa6\xb9\x12b\x9f'\xc1q\x03=\x9d\\X;6\xd8\xd4\xde.\xe8\xb5\xec\xe4̀\xfa\x89\x81$\x8am\xed\xe4\xec6\xd0\t\xafj\xf6\xf9r\xbc\xfa\xc9\xf4e\x83C\xe9\xfe\xadݝ\x8e\x02(c\xf2١\xdc\xcdŵ_!l!\xef뼓\x14j\x1bH\x10\x8d\xd6 Us\x9e\x13\x92DS\xc2\x16\x9d\xe1\xfa,\xe4\x05\xces*\x84F4V\xee\x1c\xe8S$\x8f\x13\xf3᧬/\x94\x1f\x02\xe4ң~\xea\xb1>\xa27\xb9\xa9\x9f\xa1\t\xb9\x86\x19\r<\xd8\xd8\x15s\xb8\xe3C\xeay*\xc8s\x8f\xfb\xa5\xe1\x13\xec\xb7\x1d\xca\xcc\xd2N\x11\x185a\x14\x1c\x8cN\xcc+ά\x01>&\xce\xf6R\x8b\x11AZ\x845\xf3\xea{ܟ\x13\r\xdf\x12w:\xef\xbf\r\xf9J\xce\xc5\x190a\x8b\x84>.Z\\\xee\x1e\xe41bv\xb9\t\x9a\xc5\xe0\x1a\x87\xc8\xeb0\"\x8d\x16\x1f\xd6\x0f\x81\xee\xad\xdfUBxU\n\x81\xd7\xf9ް~\x99\u007f.\xa4|\xfb\xf9\xdd\xe7\x06\xde\x1a\x03!vH\xa2Z\x9b\xdc\\hG\xa7ݫ\xdcq_A\xb2\xe6ǫ\u007f\xc2K\x18\x8as\x9e\xc1\xcd&W\xff^N\xee\fJ(\xda\x14U\x02\x81\xf4M\x11\xbb\x9f\xd4,\xfda\xa9\x10gL\xdb\x10\x1c\xaa\xf3ғ\xeek\t\xcd9\xa4Jv\xf8\x1e\x9b\xcd\xce\xfd\x86\xc9n\xa6ibx\xc9j^6\x17B\xb9\x97\xe4[\x8a\xda\xe1%\xa3/p\xbc\x9cJ\xf5\xb8\xc1\xb3ZtT1\xf1\xf77\xe9\xbcl\x9a\xb9\x9d\x1a\xb5N$\x05=\xc5\\\xb8\xd0\xfc;\x8dz\xe8\x14/\xb8\xed\x19\xa8od\xe5,\x83\xb3-\xea\xbdvX\x02Bh\x17\xaa\xe9\xbb ˃>\xf5K\xa5\xf5vT֩\xadÅo\xbfxu\xf1\xebE\xf1\x17\xf5<\x1bd\xb9\xb5\x98\x06\"\xa5\x12{\xaa\xb2i䠾\xd2\xd2\\\xd0|:\xfd\xdb\xf1\xe2œ\u007f\x0e\xf9U\a_\xceDn\xe0\xd7\xdfV%*\x9a\xbb\xf9\xa2/\x83\u007f\a\x00\x00\xff\xff\xe4\xf3S\x85\xb2\r\x00\x00"), } diff --git a/pkg/apis/velero/v1/schedule_types.go b/pkg/apis/velero/v1/schedule_types.go index 077a375ecf..6cb553b9a8 100644 --- a/pkg/apis/velero/v1/schedule_types.go +++ b/pkg/apis/velero/v1/schedule_types.go @@ -38,6 +38,10 @@ type ScheduleSpec struct { // +optional // +nullable UseOwnerReferencesInBackup *bool `json:"useOwnerReferencesInBackup,omitempty"` + + // Paused specifies whether the schedule is paused or not + // +optional + Paused bool `json:"paused,omitempty"` } // SchedulePhase is a string representation of the lifecycle phase @@ -87,6 +91,7 @@ type ScheduleStatus struct { // +kubebuilder:printcolumn:name="Schedule",type="string",JSONPath=".spec.schedule",description="A Cron expression defining when to run the Backup" // +kubebuilder:printcolumn:name="LastBackup",type="date",JSONPath=".status.lastBackup",description="The last time a Backup was run for this schedule" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Paused",type="boolean",JSONPath=".spec.paused" // Schedule is a Velero resource that represents a pre-scheduled or // periodic Backup that should be run. diff --git a/pkg/cmd/cli/delete_options.go b/pkg/cmd/cli/delete_options.go index fbdcb0d0a9..80fd292228 100644 --- a/pkg/cmd/cli/delete_options.go +++ b/pkg/cmd/cli/delete_options.go @@ -27,24 +27,20 @@ import ( "github.com/spf13/pflag" "github.com/vmware-tanzu/velero/pkg/client" - "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" ) // DeleteOptions contains parameters used for deleting a restore. type DeleteOptions struct { - Names []string - all bool - Selector flag.LabelSelector - Confirm bool - Client clientset.Interface - Namespace string - singularTypeName string + *SelectOptions + Confirm bool + Client clientset.Interface + Namespace string } func NewDeleteOptions(singularTypeName string) *DeleteOptions { o := &DeleteOptions{} - o.singularTypeName = singularTypeName + o.SelectOptions = NewSelectOptions("delete", singularTypeName) return o } @@ -56,8 +52,7 @@ func (o *DeleteOptions) Complete(f client.Factory, args []string) error { return err } o.Client = client - o.Names = args - return nil + return o.SelectOptions.Complete(args) } // Validate validates the fields of the DeleteOptions struct. @@ -65,23 +60,14 @@ func (o *DeleteOptions) Validate(c *cobra.Command, f client.Factory, args []stri if o.Client == nil { return errors.New("Velero client is not set; unable to proceed") } - var ( - hasNames = len(o.Names) > 0 - hasAll = o.all - hasSelector = o.Selector.LabelSelector != nil - ) - if !xor(hasNames, hasAll, hasSelector) { - return errors.New("you must specify exactly one of: specific " + o.singularTypeName + " name(s), the --all flag, or the --selector flag") - } - return nil + return o.SelectOptions.Validate() } // BindFlags binds options for this command to flags. func (o *DeleteOptions) BindFlags(flags *pflag.FlagSet) { flags.BoolVar(&o.Confirm, "confirm", o.Confirm, "Confirm deletion") - flags.BoolVar(&o.all, "all", o.all, "Delete all "+o.singularTypeName+"s") - flags.VarP(&o.Selector, "selector", "l", "Delete all "+o.singularTypeName+"s matching this label selector.") + o.SelectOptions.BindFlags(flags) } // GetConfirmation ensures that the user confirms the action before proceeding. diff --git a/pkg/cmd/cli/schedule/create.go b/pkg/cmd/cli/schedule/create.go index e32a71c0cd..00d84721c4 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -82,6 +82,7 @@ type CreateOptions struct { BackupOptions *backup.CreateOptions Schedule string UseOwnerReferencesInBackup bool + Paused bool labelSelector *metav1.LabelSelector } @@ -96,6 +97,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { o.BackupOptions.BindFlags(flags) flags.StringVar(&o.Schedule, "schedule", o.Schedule, "A cron expression specifying a recurring schedule for this backup to run") flags.BoolVar(&o.UseOwnerReferencesInBackup, "use-owner-references-in-backup", o.UseOwnerReferencesInBackup, "Specifies whether to use OwnerReferences on backups created by this Schedule. Notice: if set to true, when schedule is deleted, backups will be deleted too.") + flags.BoolVar(&o.Paused, "paused", o.Paused, "Specifies whether the newly created schedule is paused or not.") } func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { @@ -149,6 +151,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { }, Schedule: o.Schedule, UseOwnerReferencesInBackup: &o.UseOwnerReferencesInBackup, + Paused: o.Paused, }, } diff --git a/pkg/cmd/cli/schedule/pause.go b/pkg/cmd/cli/schedule/pause.go new file mode 100644 index 0000000000..e8aa3fe82e --- /dev/null +++ b/pkg/cmd/cli/schedule/pause.go @@ -0,0 +1,122 @@ +/* +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 schedule + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + kubeerrs "k8s.io/apimachinery/pkg/util/errors" + + 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/cli" +) + +// NewPauseCommand creates the command for pause +func NewPauseCommand(f client.Factory, use string) *cobra.Command { + o := cli.NewSelectOptions("pause", "schedule") + + c := &cobra.Command{ + Use: use, + Short: "Pause schedules", + Example: ` # Pause a schedule named "schedule-1". + velero schedule pause schedule-1 + + # Pause schedules named "schedule-1" and "schedule-2". + velero schedule pause schedule-1 schedule-2 + + # Pause all schedules labelled with "foo=bar". + velero schedule pause --selector foo=bar + + # Pause all schedules. + velero schedule pause --all`, + Run: func(c *cobra.Command, args []string) { + cmd.CheckError(o.Complete(args)) + cmd.CheckError(o.Validate()) + cmd.CheckError(runPause(f, o, true)) + }, + } + + o.BindFlags(c.Flags()) + + return c +} + +func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error { + client, err := f.Client() + if err != nil { + return err + } + + var ( + schedules []*velerov1api.Schedule + errs []error + ) + switch { + case len(o.Names) > 0: + for _, name := range o.Names { + schedule, err := client.VeleroV1().Schedules(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + errs = append(errs, errors.WithStack(err)) + continue + } + schedules = append(schedules, schedule) + } + default: + selector := labels.Everything().String() + if o.Selector.LabelSelector != nil { + selector = o.Selector.String() + } + res, err := client.VeleroV1().Schedules(f.Namespace()).List(context.TODO(), metav1.ListOptions{ + LabelSelector: selector, + }) + if err != nil { + errs = append(errs, errors.WithStack(err)) + } + + for i := range res.Items { + schedules = append(schedules, &res.Items[i]) + } + } + if len(schedules) == 0 { + fmt.Println("No schedules found") + return nil + } + + msg := "paused" + if !paused { + msg = "unpaused" + } + for _, schedule := range schedules { + if schedule.Spec.Paused == paused { + fmt.Printf("Schedule %s is already %s, skip\n", schedule.Name, msg) + continue + } + schedule.Spec.Paused = paused + if _, err := client.VeleroV1().Schedules(schedule.Namespace).Update(context.TODO(), schedule, metav1.UpdateOptions{}); err != nil { + return errors.Wrapf(err, "failed to update schedule %s", schedule.Name) + } + fmt.Printf("Schedule %s %s successfully\n", schedule.Name, msg) + } + return kubeerrs.NewAggregate(errs) +} diff --git a/pkg/cmd/cli/schedule/schedule.go b/pkg/cmd/cli/schedule/schedule.go index 274a2ad799..85c51c3999 100644 --- a/pkg/cmd/cli/schedule/schedule.go +++ b/pkg/cmd/cli/schedule/schedule.go @@ -34,6 +34,8 @@ func NewCommand(f client.Factory) *cobra.Command { NewGetCommand(f, "get"), NewDescribeCommand(f, "describe"), NewDeleteCommand(f, "delete"), + NewPauseCommand(f, "pause"), + NewUnpauseCommand(f, "unpause"), ) return c diff --git a/pkg/cmd/cli/schedule/unpause.go b/pkg/cmd/cli/schedule/unpause.go new file mode 100644 index 0000000000..8a9e467e3d --- /dev/null +++ b/pkg/cmd/cli/schedule/unpause.go @@ -0,0 +1,55 @@ +/* +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 schedule + +import ( + "github.com/spf13/cobra" + + "github.com/vmware-tanzu/velero/pkg/client" + "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" +) + +// NewUnpauseCommand creates the command for unpause +func NewUnpauseCommand(f client.Factory, use string) *cobra.Command { + o := cli.NewSelectOptions("pause", "schedule") + + c := &cobra.Command{ + Use: use, + Short: "Unpause schedules", + Example: ` # Unpause a schedule named "schedule-1". + velero schedule unpause schedule-1 + + # Unpause schedules named "schedule-1" and "schedule-2". + velero schedule unpause schedule-1 schedule-2 + + # Unpause all schedules labelled with "foo=bar". + velero schedule unpause --selector foo=bar + + # Unpause all schedules. + velero schedule unpause --all`, + Run: func(c *cobra.Command, args []string) { + cmd.CheckError(o.Complete(args)) + cmd.CheckError(o.Validate()) + cmd.CheckError(runPause(f, o, false)) + }, + } + + o.BindFlags(c.Flags()) + + return c +} diff --git a/pkg/cmd/cli/select_option.go b/pkg/cmd/cli/select_option.go new file mode 100644 index 0000000000..45ed547168 --- /dev/null +++ b/pkg/cmd/cli/select_option.go @@ -0,0 +1,69 @@ +/* +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 cli + +import ( + "errors" + "strings" + + "github.com/spf13/pflag" + + "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" +) + +// SelectOptions defines the options for selecting resources +type SelectOptions struct { + Names []string + All bool + Selector flag.LabelSelector + CMD string + SingularTypeName string +} + +// NewSelectOptions creates a new option for selector +func NewSelectOptions(cmd, singularTypeName string) *SelectOptions { + return &SelectOptions{ + CMD: cmd, + SingularTypeName: singularTypeName, + } +} + +// Complete fills in the correct values for all the options. +func (o *SelectOptions) Complete(args []string) error { + o.Names = args + return nil +} + +// Validate validates the fields of the SelectOptions struct. +func (o *SelectOptions) Validate() error { + var ( + hasNames = len(o.Names) > 0 + hasAll = o.All + hasSelector = o.Selector.LabelSelector != nil + ) + if !xor(hasNames, hasAll, hasSelector) { + return errors.New("you must specify exactly one of: specific " + o.SingularTypeName + " name(s), the --all flag, or the --selector flag") + } + + return nil +} + +// BindFlags binds options for this command to flags. +func (o *SelectOptions) BindFlags(flags *pflag.FlagSet) { + flags.BoolVar(&o.All, "all", o.All, strings.Title(o.CMD)+" all "+o.SingularTypeName+"s") + flags.VarP(&o.Selector, "selector", "l", strings.Title(o.CMD)+" all "+o.SingularTypeName+"s matching this label selector.") +} diff --git a/pkg/cmd/cli/select_option_test.go b/pkg/cmd/cli/select_option_test.go new file mode 100644 index 0000000000..b555f2bebb --- /dev/null +++ b/pkg/cmd/cli/select_option_test.go @@ -0,0 +1,45 @@ +/* +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 cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" +) + +func TestCompleteOfSelectOption(t *testing.T) { + option := &SelectOptions{} + args := []string{"arg1", "arg2"} + require.Nil(t, option.Complete(args)) + assert.Equal(t, args, option.Names) +} + +func TestValidateOfSelectOption(t *testing.T) { + option := &SelectOptions{ + Names: nil, + Selector: flag.LabelSelector{}, + All: false, + } + assert.NotNil(t, option.Validate()) + + option.All = true + assert.Nil(t, option.Validate()) +} diff --git a/pkg/cmd/util/output/schedule_describer.go b/pkg/cmd/util/output/schedule_describer.go index 46a65ba182..48f3669dbd 100644 --- a/pkg/cmd/util/output/schedule_describer.go +++ b/pkg/cmd/util/output/schedule_describer.go @@ -52,6 +52,9 @@ func DescribeSchedule(schedule *v1.Schedule) string { } } + d.Println() + d.Printf("Paused:\t%t\n", schedule.Spec.Paused) + d.Println() DescribeScheduleSpec(d, schedule.Spec) diff --git a/pkg/cmd/util/output/schedule_printer.go b/pkg/cmd/util/output/schedule_printer.go index 4c7f7821e4..e39ee90692 100644 --- a/pkg/cmd/util/output/schedule_printer.go +++ b/pkg/cmd/util/output/schedule_printer.go @@ -36,6 +36,7 @@ var ( {Name: "Backup TTL"}, {Name: "Last Backup"}, {Name: "Selector"}, + {Name: "Paused"}, } ) @@ -71,6 +72,7 @@ func printSchedule(schedule *v1.Schedule) []metav1.TableRow { schedule.Spec.Template.TTL.Duration, humanReadableTimeFromNow(lastBackupTime), metav1.FormatLabelSelector(schedule.Spec.Template.LabelSelector), + schedule.Spec.Paused, ) return []metav1.TableRow{row} diff --git a/pkg/controller/backup_storage_location_controller.go b/pkg/controller/backup_storage_location_controller.go index 3791e01e68..793347dec4 100644 --- a/pkg/controller/backup_storage_location_controller.go +++ b/pkg/controller/backup_storage_location_controller.go @@ -207,18 +207,15 @@ func (r *backupStorageLocationReconciler) SetupWithManager(mgr ctrl.Manager) err mgr.GetClient(), &velerov1api.BackupStorageLocationList{}, bslValidationEnqueuePeriod, - kube.PeriodicalEnqueueSourceOption{ - FilterFuncs: []func(object client.Object) bool{ - func(object client.Object) bool { - location := object.(*velerov1api.BackupStorageLocation) - return storage.IsReadyToValidate(location.Spec.ValidationFrequency, location.Status.LastValidationTime, r.defaultBackupLocationInfo.ServerValidationFrequency, r.log.WithField("controller", BackupStorageLocation)) - }, - }, - }, + kube.PeriodicalEnqueueSourceOption{}, ) + gp := kube.NewGenericEventPredicate(func(object client.Object) bool { + location := object.(*velerov1api.BackupStorageLocation) + return storage.IsReadyToValidate(location.Spec.ValidationFrequency, location.Status.LastValidationTime, r.defaultBackupLocationInfo.ServerValidationFrequency, r.log.WithField("controller", BackupStorageLocation)) + }) return ctrl.NewControllerManagedBy(mgr). // As the "status.LastValidationTime" field is always updated, this triggers new reconciling process, skip the update event that include no spec change to avoid the reconcile loop For(&velerov1api.BackupStorageLocation{}, builder.WithPredicates(kube.SpecChangePredicate{})). - Watches(g, nil). + Watches(g, nil, builder.WithPredicates(gp)). Complete(r) } diff --git a/pkg/controller/backup_sync_controller.go b/pkg/controller/backup_sync_controller.go index cc79bbfe81..196ddeb266 100644 --- a/pkg/controller/backup_sync_controller.go +++ b/pkg/controller/backup_sync_controller.go @@ -31,19 +31,17 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - - "github.com/vmware-tanzu/velero/pkg/util/kube" + "sigs.k8s.io/controller-runtime/pkg/builder" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/features" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" + "github.com/vmware-tanzu/velero/pkg/util/kube" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" ) const ( @@ -286,33 +284,18 @@ func (b *backupSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { backupSyncReconcilePeriod, kube.PeriodicalEnqueueSourceOption{ OrderFunc: backupSyncSourceOrderFunc, - FilterFuncs: []func(object client.Object) bool{ - func(object client.Object) bool { - location := object.(*velerov1api.BackupStorageLocation) - return b.locationFilterFunc(location) - }, - }, }, ) + gp := kube.NewGenericEventPredicate(func(object client.Object) bool { + location := object.(*velerov1api.BackupStorageLocation) + return b.locationFilterFunc(location) + }) + return ctrl.NewControllerManagedBy(mgr). - For(&velerov1api.BackupStorageLocation{}). // Filter all BSL events, because this controller is supposed to run periodically, not by event. - WithEventFilter(predicate.Funcs{ - CreateFunc: func(ce event.CreateEvent) bool { - return false - }, - UpdateFunc: func(ue event.UpdateEvent) bool { - return false - }, - DeleteFunc: func(de event.DeleteEvent) bool { - return false - }, - GenericFunc: func(ge event.GenericEvent) bool { - return false - }, - }). - Watches(backupSyncSource, nil). + For(&velerov1api.BackupStorageLocation{}, builder.WithPredicates(kube.FalsePredicate{})). + Watches(backupSyncSource, nil, builder.WithPredicates(gp)). Complete(b) } diff --git a/pkg/controller/gc_controller.go b/pkg/controller/gc_controller.go index 4e61cd25d3..61b22996a6 100644 --- a/pkg/controller/gc_controller.go +++ b/pkg/controller/gc_controller.go @@ -25,6 +25,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/clock" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -75,8 +76,7 @@ func NewGCReconciler( func (c *gcReconciler) SetupWithManager(mgr ctrl.Manager) error { s := kube.NewPeriodicalEnqueueSource(c.logger, mgr.GetClient(), &velerov1api.BackupList{}, c.frequency, kube.PeriodicalEnqueueSourceOption{}) return ctrl.NewControllerManagedBy(mgr). - For(&velerov1api.Backup{}). - WithEventFilter(predicate.Funcs{ + For(&velerov1api.Backup{}, builder.WithPredicates(predicate.Funcs{ UpdateFunc: func(ue event.UpdateEvent) bool { return false }, @@ -86,7 +86,7 @@ func (c *gcReconciler) SetupWithManager(mgr ctrl.Manager) error { GenericFunc: func(ge event.GenericEvent) bool { return false }, - }). + })). Watches(s, nil). Complete(c) } diff --git a/pkg/controller/schedule_controller.go b/pkg/controller/schedule_controller.go index 9ca9de1edb..3f3df4a6f7 100644 --- a/pkg/controller/schedule_controller.go +++ b/pkg/controller/schedule_controller.go @@ -28,6 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" ctrl "sigs.k8s.io/controller-runtime" + bld "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -67,7 +68,16 @@ func NewScheduleReconciler( func (c *scheduleReconciler) SetupWithManager(mgr ctrl.Manager) error { s := kube.NewPeriodicalEnqueueSource(c.logger, mgr.GetClient(), &velerov1.ScheduleList{}, scheduleSyncPeriod, kube.PeriodicalEnqueueSourceOption{}) return ctrl.NewControllerManagedBy(mgr). - For(&velerov1.Schedule{}). + // global predicate, works for both For and Watch + WithEventFilter(kube.NewAllEventPredicate(func(obj client.Object) bool { + schedule := obj.(*velerov1.Schedule) + if pause := schedule.Spec.Paused; pause { + c.logger.Infof("schedule %s is paused, skip", schedule.Name) + return false + } + return true + })). + For(&velerov1.Schedule{}, bld.WithPredicates(kube.SpecChangePredicate{})). Watches(s, nil). Complete(c) } @@ -89,13 +99,6 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, errors.Wrapf(err, "error getting schedule %s", req.String()) } - if schedule.Status.Phase != "" && - schedule.Status.Phase != velerov1.SchedulePhaseNew && - schedule.Status.Phase != velerov1.SchedulePhaseEnabled { - log.Debugf("the schedule phase is %s, isn't %s or %s, skip", schedule.Status.Phase, velerov1.SchedulePhaseNew, velerov1.SchedulePhaseEnabled) - return ctrl.Result{}, nil - } - c.metrics.InitSchedule(schedule.Name) original := schedule.DeepCopy() @@ -124,7 +127,8 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, nil } - // check for the schedule being due to run, and submit a Backup if so + // check for the schedule being due to run, and submit a Backup if so. + // As the schedule must be validated before checking whether it's due, we cannot put the checking log in Predicate if err := c.submitBackupIfDue(ctx, schedule, cronSchedule); err != nil { return ctrl.Result{}, errors.Wrapf(err, "error running submitBackupIfDue for schedule %s", req.String()) } diff --git a/pkg/util/kube/periodical_enqueue_source.go b/pkg/util/kube/periodical_enqueue_source.go index 16c4db0ce0..1b0ec1a31f 100644 --- a/pkg/util/kube/periodical_enqueue_source.go +++ b/pkg/util/kube/periodical_enqueue_source.go @@ -23,13 +23,13 @@ import ( "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" ) @@ -61,11 +61,11 @@ type PeriodicalEnqueueSource struct { } type PeriodicalEnqueueSourceOption struct { - FilterFuncs []func(object client.Object) bool - OrderFunc func(objList client.ObjectList) client.ObjectList + OrderFunc func(objList client.ObjectList) client.ObjectList } -func (p *PeriodicalEnqueueSource) Start(ctx context.Context, h handler.EventHandler, q workqueue.RateLimitingInterface, pre ...predicate.Predicate) error { +// Start enqueue items periodically. The predicates only apply to the GenericEvent +func (p *PeriodicalEnqueueSource) Start(ctx context.Context, h handler.EventHandler, q workqueue.RateLimitingInterface, predicates ...predicate.Predicate) error { go wait.Until(func() { p.logger.Debug("enqueueing resources ...") if err := p.List(ctx, p.objList); err != nil { @@ -80,19 +80,19 @@ func (p *PeriodicalEnqueueSource) Start(ctx context.Context, h handler.EventHand p.objList = p.option.OrderFunc(p.objList) } if err := meta.EachListItem(p.objList, func(object runtime.Object) error { - obj, ok := object.(metav1.Object) + obj, ok := object.(client.Object) if !ok { p.logger.Error("%s's type isn't metav1.Object", object.GetObjectKind().GroupVersionKind().String()) return nil } - for _, filter := range p.option.FilterFuncs { - if filter != nil { - if enqueueObj := filter(object.(client.Object)); !enqueueObj { - p.logger.Debugf("skip enqueue object %s/%s due to filter function.", obj.GetNamespace(), obj.GetName()) - return nil - } + event := event.GenericEvent{Object: obj} + for _, predicate := range predicates { + if !predicate.Generic(event) { + p.logger.Debugf("skip enqueue object %s/%s due to the predicate.", obj.GetNamespace(), obj.GetName()) + return nil } } + q.Add(ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: obj.GetNamespace(), diff --git a/pkg/util/kube/periodical_enqueue_source_test.go b/pkg/util/kube/periodical_enqueue_source_test.go index e209d6d9eb..8d5e142dd3 100644 --- a/pkg/util/kube/periodical_enqueue_source_test.go +++ b/pkg/util/kube/periodical_enqueue_source_test.go @@ -68,7 +68,7 @@ func TestStart(t *testing.T) { require.Equal(t, 0, queue.Len()) } -func TestFilter(t *testing.T) { +func TestPredicate(t *testing.T) { require.Nil(t, velerov1.AddToScheme(scheme.Scheme)) ctx, cancelFunc := context.WithCancel(context.TODO()) @@ -79,15 +79,13 @@ func TestFilter(t *testing.T) { client, &velerov1.BackupStorageLocationList{}, 1*time.Second, - PeriodicalEnqueueSourceOption{ - FilterFuncs: []func(object crclient.Object) bool{func(object crclient.Object) bool { - location := object.(*velerov1.BackupStorageLocation) - return storage.IsReadyToValidate(location.Spec.ValidationFrequency, location.Status.LastValidationTime, 1*time.Minute, logrus.WithContext(ctx).WithField("BackupStorageLocation", location.Name)) - }}, - }, + PeriodicalEnqueueSourceOption{}, ) - require.Nil(t, source.Start(ctx, nil, queue)) + require.Nil(t, source.Start(ctx, nil, queue, NewGenericEventPredicate(func(object crclient.Object) bool { + location := object.(*velerov1.BackupStorageLocation) + return storage.IsReadyToValidate(location.Spec.ValidationFrequency, location.Status.LastValidationTime, 1*time.Minute, logrus.WithContext(ctx).WithField("BackupStorageLocation", location.Name)) + }))) // Should not patch a backup storage location object status phase // if the location's validation frequency is specifically set to zero diff --git a/pkg/util/kube/predicate.go b/pkg/util/kube/predicate.go index 3073ef8816..a9660b34e7 100644 --- a/pkg/util/kube/predicate.go +++ b/pkg/util/kube/predicate.go @@ -19,6 +19,7 @@ package kube import ( "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" ) @@ -45,3 +46,53 @@ func (SpecChangePredicate) Update(e event.UpdateEvent) bool { newSpec := reflect.ValueOf(e.ObjectNew).Elem().FieldByName("Spec") return !reflect.DeepEqual(oldSpec.Interface(), newSpec.Interface()) } + +// NewGenericEventPredicate creates a new Predicate that checks the Generic event with the provided func +func NewGenericEventPredicate(f func(object client.Object) bool) predicate.Predicate { + return predicate.Funcs{ + GenericFunc: func(event event.GenericEvent) bool { + return f(event.Object) + }, + } +} + +// NewAllEventPredicate creates a new Predicate that checks all the events with the provided func +func NewAllEventPredicate(f func(object client.Object) bool) predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(event event.CreateEvent) bool { + return f(event.Object) + }, + DeleteFunc: func(event event.DeleteEvent) bool { + return f(event.Object) + }, + UpdateFunc: func(event event.UpdateEvent) bool { + return f(event.ObjectNew) + }, + GenericFunc: func(event event.GenericEvent) bool { + return f(event.Object) + }, + } +} + +// FalsePredicate always returns false for all kinds of events +type FalsePredicate struct{} + +// Create always returns false +func (f FalsePredicate) Create(event.CreateEvent) bool { + return false +} + +// Delete always returns false +func (f FalsePredicate) Delete(event.DeleteEvent) bool { + return false +} + +// Update always returns false +func (f FalsePredicate) Update(event.UpdateEvent) bool { + return false +} + +// Generic always returns false +func (f FalsePredicate) Generic(event.GenericEvent) bool { + return false +} diff --git a/pkg/util/kube/predicate_test.go b/pkg/util/kube/predicate_test.go index d1c3be8df5..40d1b8c9c0 100644 --- a/pkg/util/kube/predicate_test.go +++ b/pkg/util/kube/predicate_test.go @@ -178,3 +178,22 @@ func TestSpecChangePredicate(t *testing.T) { }) } } + +func TestNewGenericEventPredicate(t *testing.T) { + predicate := NewGenericEventPredicate(func(object client.Object) bool { + return false + }) + + assert.False(t, predicate.Generic(event.GenericEvent{})) +} + +func TestNewAllEventPredicate(t *testing.T) { + predicate := NewAllEventPredicate(func(object client.Object) bool { + return false + }) + + assert.False(t, predicate.Create(event.CreateEvent{})) + assert.False(t, predicate.Update(event.UpdateEvent{})) + assert.False(t, predicate.Delete(event.DeleteEvent{})) + assert.False(t, predicate.Generic(event.GenericEvent{})) +}