diff --git a/changelogs/unreleased/4855-reasonerjt b/changelogs/unreleased/4855-reasonerjt new file mode 100644 index 000000000..3f05d3355 --- /dev/null +++ b/changelogs/unreleased/4855-reasonerjt @@ -0,0 +1 @@ +Refactor backup deletion controller based on kubebuilder \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_deletebackuprequests.yaml b/config/crd/v1/bases/velero.io_deletebackuprequests.yaml index 761036ddb..076e0af46 100644 --- a/config/crd/v1/bases/velero.io_deletebackuprequests.yaml +++ b/config/crd/v1/bases/velero.io_deletebackuprequests.yaml @@ -16,7 +16,16 @@ spec: singular: deletebackuprequest scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - description: The name of the backup to be deleted + jsonPath: .spec.backupName + name: BackupName + type: string + - description: The status of the deletion request + jsonPath: .status.phase + name: Status + type: string + name: v1 schema: openAPIV3Schema: description: DeleteBackupRequest is a request to delete one or more backups. @@ -63,6 +72,8 @@ spec: type: object served: true storage: true + subresources: + status: {} status: acceptedNames: kind: "" diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 54838c355..12ee414f2 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -31,7 +31,7 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec<]o#9r\xef\xfe\x15\x05\xe7a\xee\x00K\xbeE\x1e\x12\xf8m\xd63\x8b\b\xbb\x993֎\xf3\x10\xe4\x81\xea.Ib\x81F\xaf\xa5\xbe\xb2\x15f4\xd7\xde躺\x83\xf6\a?$\xe0\xe1\xd7\xf0#\x8f\xe6\x0f\x85\xb4\xee\xe7\xce\xc7_\xa4u\xfcCU\xd4F\x14\xcdL\xfc\xcdJ\xb5\xaf\va\xe2\xd7+\x00\x9b\xe9\n\xef\xe0\vMQ\x89\f\xf3+\x80\xb0\x1c\x9er\x15\x10>\xfe\xe0!d\a,\x85\xc7\x05@W\xa8>>l\x9e\xff\xf1\xb1\xf7\x19 G\x9b\x19Y9&\x8aG\f\xa4\x05\x01ϼ,0\x81\xfc\xe0\x0e\u0081\xc1ʠE\xe5,\xb8\x03B&*W\x1b\x04\xbd\x83\x9f\xeb-\x1a\x85\x0em\x03\x1a +j\xebЀu\xc2!\b\a\x02*-\x95\x03\xa9\xc0\xc9\x12\xe1\x0f\x1f\x1f6\xa0\xb7\u007f\xc1\xccY\x10*\aa\xadΤp\x98\xc3Q\x17u\x89~\xec\x1f\xd7\r\xd4\xca\xe8\n\x8d\x93\x91ξu\xa4\xaa\xf3u\xb0\xbc\x0fD\x01\xdf\vr\x12'\xf4\xcb\bT\xc4<\x10\x8d\xd6\xe3\x0eҶ\xcbe\t\xe9\x01\x06\xea$T@~\r\x8fh\b\f\u0603\xae\x8b\x9c\xa4\xf0\x88\x86\b\x96齒\xff\xdd\xc0\xb6\xe04OZ\b\x87A\x00\xda&\x95C\xa3D\x01GQ\xd4x\xc3$)\xc5\t\f\xd2,P\xab\x0e<\xeeb\xd7\xf0\xaf\xda H\xb5\xd3wpp\xae\xb2w\xb7\xb7{\xe9\xe2n\xcatY\xd6J\xba\xd3-o\f\xb9\xad\x9d6\xf66\xc7#\x16\xb7V\xeeW\xc2d\a\xe90#FފJ\xae\x18u\xc5;j]\xe6\xff\x10\x05\xc0~\xe8\xe1\xeaN$\x8c\xd6\x19\xa9\xf6\x9d\x1fX\xeag8@\x1b\xc0˗\x1f\xeaW\xd1\x12\x9a>\x11u~\xfd\xfc\xf8ԕ=i\x87\xd4g\xbaw\x04\xb2e\x01\x11L\xaa\x1d\x1a\xcfĝ\xd1%\xc3D\x95{\xe9c\xd1-$\xaa!\xf9m\xbd-\xa5#\xbe\xffW\x8d\x96\x84\\\xaf\xe1\x9eU\fl\x11\xea*'\xc9\\\xc3F\xc1\xbd(\xb1\xb8\x17\x16\xbf9\x03\x88\xd2vE\x84McAW;\x0e;{\xaau~\x88\xbal\x82_^!\xc3٫È9q\xa2\xa7\x1a\xb5B\xd0\x06J\xb2\a\xe7]\x87\x16\xacmS\xcb\xde\n\xd2Nڋ\xa8\xa9\v\xb4a\xaa\x9cun\xab\x03n&A7\x1c\xf1\xbeD!\xb6X\x80\xc5\x023\xa7\xcd89\x96\x98\xec[\x8a^\x9b\xa0∆ku7-\xb5]\xd8\fH \xb5\xfdz\x90\xd9\xc1\x9by\x92 \x86\x03\xb9F˻\\TUq\x9aZ$,q>L2\xb7\xd1۶\xb0\xe5\x87\xf0\xc66\u007f\xdb\x12tc\xdb\x16\xb4d\x9f\xb2\x8d8\x80ӳ\xcb\xfe\xffIب\xf6\xdf \xb4\x9b\xb3\xa1\xef+\xb4DRI\xee\xfcf\aXV\xeet\x03\xd2ůK\x10\xc9Yi\xe7\xff;f\xcc\xe5\x12\xbf\x19\x8e|W\x89\x9f\xe5\xca\x12D\xe2J3\xfd\xdf!S\xd8X<\x06[\x91̐_\xba\xa3n@\xee\x1a\x86\xe47\xb0\x93\x85C3\xe0\xcco\xda/\xefA\x8c\x14{G\xad\x14.;|\xfeJ\x9e\x97m\x13N\x89t\x19\x0e\xf6\xfek\xf4\xe7\xfb\x86y\x01.p\x80*\r\x96>\xf0}bj\xb6_أ\xfa\xf8\xe5\x13\xe6s\xe4\x814\xc9;[\xc8\xc7\x01\xb2ݩ\x83S\x9e\xba\x8c\xe0\xfa4\xf1\x8dOi܀\x80\x170!8\xb6N'\x1ep\xe2+\xea\xa2\xe5\xc5A\xba\"\x89-\xd2\xfe\r\xcbl\xd8\xd6I\x1a2c?X\xcf\"\xda\x05\aY%.\x94\xcc\x1cX\xe4\xdd\x12S_Ϣ\x90y3\x91\x97\xfb\x8d\x9a\xf6\x86\xfb\xed\x8bv\x1bu\x03\x9f\xbfJ\x1b\xb2\x8f\x9f4\xda/\xda\xf1\x97oBN\x8f\xf8\x1b\x88\xe9\a\xf2\xf6R^m\x13\x1d\xba9\xb4\x04\xe1\xf6m\xe3#\xbc\x86=\xd2\xc2FQ\xdc\x12\xe8\xc1\x19Q?ݼ}跲\xb6\x9c$SZ\xad\xd8T\xae\xc7f\xf2\xc4N\x04\xa9M\x8f#\xe7\xa85\x93\xfa\t\x13\xc1>\x91%\xf1\xe3}\x8e\xb7\x10\x19\xe6\x90\xd7LL\xceL\n\x87{\x99A\x89f?g8\xba\xad\"\xfd\x9e\x86B\xa2\xd6\xf5\xedB\tK3\xed\xb1\x05՝/#\xb3\xa2\x9d\x9b\xd0+2{\xb1\xebDBr\xba\xeb\xf2\x8a\xd8IJ\xff\xb1H]\x91\xe7|\x96$\x8a\x87\v4\xfe\x05\xbc8\xb7\xfd\x1e1o!K\xc1I\xc6\xff!3\xc7\x02\xfd\xbfP\ti\x12\xf6\xf0G>\x1a*\xb076d\xb1\xba\xd3\xd0\f\xd2\x02\xf1\xf7(\x8a\xf3T\xf7\xc8\xe24\xe9\x16,\xbc!\u05fb3\x8f\xe5\x06^\x0f\xdaz\x9b\xba\x938\x9aR\xed7i\xe1\xfa\x05O\xd77gz\xe0z\xa3\xae\xbd\x81\xbfX\xdd4ނV\xc5\t\xaey\xec\xf5oq\x82\x12%1\xa9\x1b\x1f\xc1\xa5\xba\xca\x14KFO\x80\x066\xe7N\xe4\xe6\xcea\x9d$\x87\x95\xb6.\x19\x95\am\x9d\xcf,\xf6\xdc\xd2K\xb2X\xe0e(d\xaf@\xec\xfcɟ6\xf1L\x87\xd4\xde \xe1J\\\xb3\xf3\x1a\x96\xd8\xd8d\xc4)\x19[\xbaCJD\xbaЕ\xff\xfc\xb5\x93\xbd\xa4\xcdO\u007f/\tߥx\x01\xefٲ\x14Ó\xc1$\x14\xef\xfdȸM\x02 \x1f\x1a\x98}\xcd[=݃\f\x82\xf4{0ӥT\x1b\x9e\x00~xw\xb3\xde(I|\x8b\xe3~\x1fǶDo>\xf0\xeeM\xf5\x884g\xee\r\xf68w\x9e\xe7&G1\x11\xa4Ү\x9bN \xb8\x95\xce?X\xd8Ic]\x17\xd1T\xa1\xa8\x17v\u007f\xdb.\x8d\x9c\xd4gc\xde\x148\xfdُ\xec$\xb2\x0e\xfa5\x9e\xafN\x1ef\x8e5>\x14B\x90;\x90\x0ePe\xbaV\x9c~\xa1\xad\xceSx\x16x\x05\x9dL\xb24\x05A\rU]\xa6\x11`\xc5R'\xd5l\x9e\xa6\xdb\xfd'!\x8bo\xc16'K\xd4\xf5\xac\xe1l[\x8fmO~d\uf83c\x14_eY\x97 J\"}jس\xf3\xc51=\x8eë\x90\x8e-\a\xc1e3\xe24m\xaa\xaa@\x97\xba#\xb7\xb8ӆ\xf7\xb3\x9596\x869H\x81V `'dQ\x9bD\ry\x11m/\x895\x82\xb2x\xbf \"m\xf2\x15\x93\"!\x11\x9b\xe8,\xcek\xebʤ\xbb\x8a\x0f\x06\xd3ܳ\xa5\xa4tt\xcf*#I\x96\xf4{{hAĄ:}w\xd1\xce\xdaw\x17m\xa1}w\xd1&\xdbw\x17m\xb9}w\xd1B\xfb\xee\xa2\xc5\xf6\xddE\xfb\xee\xa2\xcdu\x9b\xd3\xd6K\x18\xf9\x8a\xfb\x89\x1f\x17\xb1H8\x9e\x9eCq\x06~\xa8\xa6\xb8\xf7\xd5\xf7\xa9\x15\x96\x9b\xf1Q#u\xb5\xa1\xac\u007f\xc57\x12\xc6$\xa0-\xbahMISrI\x1b$\x8a\xb7/ ^(\xc2L*\xa7\x1c\xaf\xbeM)\xf8Y*\xf3\xe9י6e6\xb1\xd0T\xc7IF\xe8\x10o6\x90\xdbۭ!\xe9\xd7밟\x1b1\xfd\x9bנ&\x94\xe2,\x14\xe0\xcc\x17\xe6\xce\xd1k\x10z\xf4\tfz\x05\xa3\xbf\x1bz-T\xc9L\xd7Ƅ\x93 t\xe2\xf8ú\xff\x8bӡR\x06^\xa5;\x8c,\xe5\xf5\x80\x8aϰԾ[\xf6\x1a\xe5-\\1\x19\xd2\x11\xb4\x01%\v&猴\xf6\xc8\v\u007f\xae|\bw\xf1\xbe\x9c\x0f?\xd2ji\xde\\Aӯ\x90\x99Pї\x1e\x19\xa5\x17\n\xa7\xd7\xc8\xcc\x17\xb5\\R\x193\xac{\x99\x04\xba\\\x0f\x93\x129.Ծ\xbc\xa1\xe2%\xb1\xda\xf17\x1f\x8c\xa5Դ\xbc\xa9\x92e\xb1 0\xb1~\xa5_\x992\x0f\U00082a95$\xe2,W\xa8\\\\\x97\x12\xea@fב\\\x8d2Rg2\vx\xb2\x06e\xae\xbad!+u^y\x92^S2\v\x9a\xebM\x96+Iޯ^\xf4=|\xe0iU\xb3X\r\xb2\xe8#\xcf\xe3\xb7X\xefqI\x95\xc7\"\xc5\xdeX\xd1\xd1TlL\xcc{i\x1dG\xbfNc\x02hJ\xf5\xc6Du\xc6\x04\xc4ٚ\x8dԚ\x8c\t\xd8\vfwVJf~\x1c\xbf\b\t\x8b\xf6\xad\xf8kI\xd4[\x17\xa6M\x8ef\xd6COEs\x16\xc5~\xc6k0\xe7\xa0\xcc>^\x9c\xa4^]\xaf\u007f\x8c\xe5\xba)\t\xcf\xe0g\xa9r/'$\xe8\x1d?\x81/\nsQL㮴\xfe\xde8\xd0A\xa4a\xb1\x12\x86o\x92oO>[a\xd7\xf0Yd\x87~G8\bK1i9\xea\x86]7a\xdam\x1cE_\xae\xd7\x00?\xe9&\x12\xee^\xb3\xb2\xb2\xac\x8a\x13\xd4\x16\xe1\xba?\xe4m1Ǩ\x04X%*{\xd0\xf1\x0e\xecB\xd8\xf1\xd8\xef=\x12\xd1\xc7\x1b\xb0Y\xa1뼁>\xc1<\xa1N\xf0\xf0̾\x0f\xdf\x1d\xcc\xda{\x94\xc1\xbf\x89\x91\xc4\xf0\x9a\xe5\x8f\xef\x1f\xe1[\xa7\x8d\xd8\xe3/\xda_F^\xa2D\xbfw\xef&z\xd0a1\xe3\x16\v\xb2\xc4\b\x11µ\xe8\x01\xb06\x91\x1evC\x9b\xfc ,\xc7\xd4\xdb\xcc\xfes\xaeXX\xcc\xd3\xd3/~\x01N\x96\xb8\xfeT\xfblʪ\x12\xc6\"Q3.\xcc\x0f\xda\xd2\u007f\x0f\xfauL\xe1\xe9\xb0\xe6\x1f\x87x\x1b\xe4d='m.\xc2\xde_\x9b\x8e\x82\x17I\xb4$\xa8\xcf\xe3\xa3:\x81^\x87I~\x97\xeb\xb1s\x89)8\x9d\xd7%(\xb0\xf6\xc5v\xef{\xf7w\xcac\x99\xba\u007f\uf12b\xed\xf2\r|\xee\x16\xdf\xdb\bG>\xb5ዻ\x1e\x84\xbf\xe8\xfa\xa6K\xf8!C\xdd{\x03e\x9eO\xf7\xe7#\xf8\xa5\v\x93{\xd487\xdeܦ\u007f\x15\xb6ɂ\x8f\x9a\xf8\x16\x9c\x1f\xc9\x1e4A\xc3\x1c\xf0\x88\n\xb4\xe2\xa47_\x89\xf5\xaf\xb1\fnj%\x93:PBV\xbd\xae\n-\xf2\xb8ã\xcd\n/x<\xb1\xfe2G4\x1f\xec\fL~1`\xa7\xcd\x18\x11\xce\x15\xa67,w\x90\v\x87\xabQ\xa0I\xbaoT\xd82+\xfb\x82n?:G\xf1Ș\xb7\xde\xe7\xdf\xe3fjd\xb4\xbfN;Q\x80\xaa˭7\xe8\"v\x18\xe3\xdf\xe3f\xb0\xe5l8\x06\x99\xd9^~aR9\xdc\xe3\xd0\xe9<_\xd9}\x94\x9f\x8bW\u058c\x9cZ\x99\xad\xb3\f\xad\xdd\xd5E1\x16d4\x92\xfb\xfe\xcb\xe4\x03\xbeŇ\x0f\xb8\x93W\x81|:\x18_\xa7\xf0ǃ%Z+\xf6\xf1ŃW\xb2@{TȎ\xcf\xc8jB@\xda\x1e'\xf5\xef\xfb\xfb\x9c\x98\xc8\\-\xc2\x041\x1d\xd8\xe9\xf5a\xcc/(\xf4\x1ev\xb2\xe0\xae\xe1m\x96`\x9a/\xa4\xc9\xd7J\x9a\x14S\xfe\xb9\xe9H\xb4\xe1\x84(3\xa2}\xc3\b\v\xb9\x97d\a\x89I{a\xb6b\x8f\xabL\x17\x05r\xed\xc99^\xdfr\xb3zأo\x14\x9d-\xed\xa7n\xdf(\xb6A[y8\xf1ɢ\x9b\xe0b\x8dG\x18\xa5\xf8\x8b67PJE\xff\x90\x1fͩ\x858\xf8\"\x83\xce\xcfC,\xe0\xfd@}\x9a\x8a\x84\x8ey\xc2(fS\x0e\xe0\xf8)\xf4\n\xbe\u0e7f\xe2\x0f\x961\xe7\xe4\xd9\xd8\xc3L\xd4e\xa3\x1e\x8c\xdeS\xec;\xf2c\xa3\x12F~{\x10\xc6IQ\x14'?\xc9\xe4\xec#?|B2\a\x93>\xc18Y\x03\x96K\x94\r\xdd\xda@[*/\t|$\xbcյ\xebm\xd0v\x83\x8f\x88E\x9cs\r_\xb4Ø\x87\x95}\x98\xa4\xd2к\x15\xeev\xda8\x1fׯV w\xc1\xc7\x18\x81K&\x9a\xcf\x1e\xfc;I ]\x9b\xffj\xa5\x97\xc3\a\x83²\xf4:~\xad\x89\x8f\aE\x96\x91\v\x8b\xb7։bDk\xfc\xa6\xe3\x06v\xe6H\xfa0\xff\xb7\x11\xef\xe6\x8c\xe0\x9bn\xff\xe6\x8eFc3\x18\x9c\xa7\x1c\x97ox\x8d9j?\x80\x0f\xf5Q\xc1\xab\x91Α\x96\xea\x1e\u0380#\xbdT\x14`5\xec\xc4ċ\x1cs\xfa\x92\u007f'\x8b\xb6\x99N\n\xf6\xa3\x86\xa6\xf3\x94A\f\x8b\xd3Ė-\x93`bY\xbe\x86t\x8a\xaf\xe1\xbc\x1f\xbe\x9ax\x03V\xaa\xf8L\xa0O\xd7xQ\xb0\xe4\xbd\x1b\xe4\bx\xf4\xc0\xe9\xcc\xef\xeey\xd9}\xf4\xff\xba\x0e\xf6\xb1\xb10\x9fS<\xb5\xe7A\xf7\xc1A8\xed\xf2\x16b\xf0\xaeF\xe8\xf1\a\xb9\xf3'a\x19a\xfdǿ\xf9\x01\xf71\xc9g\xf90뮰'\xd2\xf8\x1d\xf0\t+\x83\x99\x18u\xe7\x01\x1e\n$?\xc2\"\xf6=\xa1\x0f\x179\x92Ƿ\x85F\xef\x19\x17\xc5\x17,\xdf'Z8\xbe-\"\xfaf\xe1\xd0\xfb\xae\xeeU\x18%\xd5~i\x8f\xfd{\xe86\x12\x0f\x05\b#\x11\xd1\xc82\x9a\x18i1\"\xea\x04D\x11lj\x87\xe5\x06A\xd2;\x85D\xa3v\xe0\xec#+м\xb3\xb7\xc3L\xe1K\x9bf\x12Y\x86$\xae_\x86/\xd5^_\xf3\x1f\xf11Z\xfe3\xd3ʛ[{\a\xff\xf1\x9fW\x10\xf2\x98\xcf\xf1\xd5Y\xfa\xf8\u007f\x01\x00\x00\xff\xff\x01\xf3\x19\x8b\xd5W\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKo#\xb9\x11\xbe\xebW\x14f\x0f\xbe\x8cZ\xb3\xc9!\x81.\x81F\x93\x00\x83x\xd6\xc6\xc8q\x0eI\x80\xa5Ȓ\xc45\x9b\xec\xf0!\xad\x12\xe4\xbf\aŇ\xba\xd5ݲ\xe4A\xb2ˋ->\x8aU_\xbdٓ\xe9t:a\x8d|F\xeb\xa4\xd1s`\x8dğ=j\xfa媗\u07fbJ\x9a\xd9\xfe\xfbɋ\xd4b\x0e\xcb༩\xbf\xa23\xc1r\xfc\x84\x1b\xa9\xa5\x97FOj\xf4L0\xcf\xe6\x13\x00\xa6\xb5\xf1\x8c\xa6\x1d\xfd\x04\xe0F{k\x94B;ݢ\xae^\xc2\x1a\xd7A*\x816\x12/W\xef?T\xbf\xab>L\x00\xb8\xc5x\xfcI\xd6\xe8<\xab\x9b9\xe8\xa0\xd4\x04@\xb3\x1a\xe7\xb0f\xfc%4\xce\x1b˶\xa8\fOwU{ThM%\xcd\xc45\xc8\xe9\xea\xad5\xa1\x99C\xbb\x90(d\xb6\x92H\x1f#\xb1U\"v\x9f\x89\xc5u%\x9d\xff\xf3\xe5=\xf7\xd2\xf9\xb8\xafQ\xc12u\x89\xad\xb8\xc5\xed\x8c\xf5?\xb4WOa\xedTZ\x91z\x1b\x14\xb3\x17\x8eO\x00\x1c7\r\xce!\x9en\x18G1\x01ȘEjS`BD-0\xf5h\xa5\xf6h\x97F\x85Z\x9f\xee\x12踕\x8d\x8f('Y \v\x03E\x1ap\x9e\xf9\xe0\xc0\x05\xbe\x03\xe6`\xb1gR\xb1\xb5\xc2\xd9_4+\xffGz\x00?9\xa3\x1f\x99\xdf͡J\xa7\xaaf\xc7\\YM:z\xec\xcc\xf8#\t༕z;\xc6\xd2=s\xfe\x99))NZ\a\xe9\xc0\xef\x10\x14s\x1e\x8c\xb6e,\x1e?\x97ț\x1c(\xfb[ƪ\x82E\xf6\\\xb3\x81\x0f \xa4\xa3\x02\xc0E\xa2C\xb0\xa8<\xa3\xf59x\x1b\xde$>7z#\xb7C\xa1\xbb5\xcd%\x8b\xb9B\xba\x87\xdc2\xdeD\xa1\x89\xac\xa3\xb1f/\x05\xda)\xf9\x87\xdcH\x9e9\t6e\xae\x8dD%\xdcP\xd2\v^\x16E\xb1(ȫ\x99\xba\xa2\xc3\xe5ic,\x8d\x99\xd4ɂ[\x021\xd8\xd8:\xa7T\xedQ\x8bS5rƍ\x89Qˡ\x80\x83\xf4\xbb\x14\x0e\u0558\xdf\xc1\xab\xbeG\xe3\x05\x8fc\xd3=ޟvH;S\x02Ep\xc8-\xfahm\xa8\xc8|Ȕ*\x80/\xc1ŀڏ\x13e\xc4B\xad\x9c~\xc1\xe3\x10h\xb8\xa6\xdc\\\xc2\\g\xf9\x8eJ\xe7°\xc5\rZ\xd4~4\xa8Sgb5z\x8cq]\x18\xee(\xa4sl\xbc\x9b\x99=ڽ\xc4\xc3\xec`\xec\x8b\xd4\xdb)\x01>\xcd\x1e4\x8bm\xc5\xec\xbb\xf8\xe7\x82\xc8O\x0f\x9f\x1e\xe6\xb0\x10\x02\x8cߡ%\xadm\x82*\x86֩o\xde\xc7\x1c\xfb\x1e\x82\x14\u007f\xb8\xfb\x16\\L\x93<\xe7\x06lV\xd1\xfa\x8fT\xa8E\xa6\b\xa2UҊ\xb1@\x99\x92\x94]gm\xa6X3f\x88c\x15fwP`\xa2\f2\x16Q_p\x18L_q\xb3\\\xec^\xf1\xb1RHK-$\xa7B\xec\xdc7J\x83!\xce\xea\xed\x11\xc1\xfa\x15\xf8\xa5\x880.x\x12 \xe7\xc3+\x1c?t\xf7\xb6mY\nO9\xc79\xf4T@9\xd0H9\x90\xd9!r1(p\xa35y\xa37\xc0N\xa1\xee\xce\xf5c\xfc\x1b#\xc4:\xf0\x17\x1c\x01~ \xcaǸ\xb1`\x9c\x8e\x11/\xc1a\f\xbe\xd7\u0600\xeb6\xce\xd9\x12\xed-\xbc,\x17\xb4\xf1\x94&\x19,\x17\xb0\x0eZ(,\x1c\x1dv\xa8\xa9C\x90\x9b\xe3\xf8]4\x9e\xeeW\x05\xd5Xa\xe4\x1a\xbf`;.C\x8a\xe1sX\x1fGj\x82\x1b\x84l,n\xe4\xcf7\b\xf9\x187\x16\xc0\x1b\xe6w \xb5\x93\x02\x81\x8d\xc0\x9f\x8a\xb5\v\x82\x9e\xf2\xffC\x8e\"ߠ\x9e\u05fc=\xb1\xf3\x16\x87/\x18_\xf1\x9fǼ\xed\x84B\xf9\x9d#\xffy-xɏG%ڟ\x1e\f\xfe\x94*,>\x92*Ϙy\x1e\x9ex\xa5R+\xcf\x16c\xceLu\x81\xb1\x16]c\xb4\xa0\xe6\xe9\xb6:\xade\xf9\u007fW\xad\x8d\xabuz\x1e\xe5zkE\v7\xb5*\xf1\x89\xe6\xcd\xcdJz\xb8\xea\xb6\x02f\xed\xa8Sl\xfb\x95\x9e\x8c\xbfH\x9b\xf2\xaeӧP?\xac!\xe8X\xa9Ō_\xc1\xdf5|\xa2ޖ\xb2\x93\x98\x13\xdfv\xcc\x00\xa4\x03m\x0et\xbcC/\x92\x00\xa3S\xbe\xa6n\x8di\x91\x9b\xe1\xb8t\x90JQƶX\x9b\xfdhƦBӢ:\x02sd:\xfb\xdfT\x1f\xaaw\xbfZ\x17\xa4\x98\xf3\xd4Ԡ\xf8\x8a{9|\xe5\x19\xa2{?8Q\x1c\xff\xe4\x0e\xf4\xe3\xc7\xd2,\xcfl\xde\xf6\xe3\b\x18\x1b\xa9\xa8\x16\x1c\x89\x13m\xc50|\x8f\xfc\xb8\xba\xbfs\xb1\x84G\xed\xc7ʾ\x03Z\x8c\x1d\x13\n\xaa\xe2M~\x97\bΣ\x1d1\x80\x93\xf6\xa2\xceA\x19\xbd\xed9N\x1a\xf9\x95\x82*\xb4dPƂ@O\xa9Io\x81\xef\x98\xdeb\xfb\n\x95\xf9\u007f\x9dS2\x9f\x9eʹ\x16\"\xf5%\xf3\xb8I\xa3Or\xacL\x1f\xbc\x00\xb7\x9b\xc7_\u007f\v\xf7E\xb3\x17ۜ+\xb8\x0f\xf6\x97,M\xa0N}\xfb\"\u070eooo\x87\xcf\xcd7 \xf1ַ\xf0W\xde5\xe0\xc0\\\xfb*\xfe\xeb\xe1PS\xb5z\xb5\x04\xfe\x92v\xa5\xe7\xc3|\x04\xd8\xda\x04\xff\x9agލ\x19t~\xee\u007f\v\x8f\xf1#Ƶ\"\x83\xf6\x14\x8d\xf0`\xa9\x95l_\xc5bP\x18\xcb-\xb7?/-z\xdfZ\xbak\xc3/17\xc85\x9ak\a\x93)_v\xf4\x9aA\xee΄\xf5饸p\x9e36\xfc\xfb?\x936yS\x86l<\x8a\x1f\xfa\x9f\xdaޥ\x00R\xbe\x97ş\x9c\xaa\x9a\xf4\xad\x10\xfe\xf6\x8fI\xba\x18\xc5s\xf9\xc0E\x93\xff\r\x00\x00\xff\xff\x04\x0e\x95\xf5\xa5\x1c\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4V=s\xe36\x10\xed\xf9+v.\xc55\x11u7)\x92Q\x97\xf8\xae\xf0$\xf1x\xec\x1b7\x99\x14\x10\xb0\x127&\x01dw!\xc7\xf9\xf5\x19\x00\xa4%Q\xf4\xc5)\u008e\xfb\x85\x87\xf7v\x97lV\xabUc\"= \v\x05\xbf\x01\x13\t\xffR\xf4\xf9M\xda\xc7\x1f\xa4\xa5\xb0>|l\x1eɻ\r\\%\xd10ܡ\x84\xc4\x16?\xe1\x8e<)\x05\xdf\f\xa8\xc6\x195\x9b\x06\xc0x\x1f\xd4d\xb3\xe4W\x00\x1b\xbcr\xe8{\xe4\xd5\x1e}\xfb\x98\xb6\xb8M\xd4;\xe4R|:\xfa\xf0\xa1\xfd\xbe\xfd\xd0\x00Xƒ\xfe\x85\x06\x145C܀O}\xdf\x00x3\xe0\x06\x1c\xf6\xa8\xb85\xf61E\xc6?\x13\x8aJ{\xc0\x1e9\xb4\x14\x1a\x89h\xf3\xc1{\x0e)n\xe0\xe8\xa8\xf9#\xa8z\xa1O\xa5\xd4O\xa5\xd4]-U\xbc=\x89\xfe\xfcZ\xc4/4F\xc5>\xb1\xe9\x97\x01\x95\x00!\xbfO\xbd\xe1Ő\x06@l\x88\xb8\x81\x9b\f+\x1a\x8b\xae\x01\x18\xf9(0W\xe3\x8d\x0f\x1fk9\xdb\xe1`*~\x80\x10\xd1\xffx{\xfd\xf0\xdd\xfd\x99\x19\xc0\xa1X\xa6\xa8\x85\xd5\x05\xfc@\x02\x06F\x14\xa0a\x04\a\xc1#\x04\x86!0BE*\xedK\xd1\xc8!\"+M\xfc\xd5\xe7\xa4uN\xac3\b\xef3\xca\x1a\x05.\xf7\f\nh\x87\xd3Mэ\x17\x83\xb0\x03\xedH\x8012\n\xfa\xdaEg\x85!\a\x19\x0fa\xfb\aZm\xe1\x1e9\x97\x01\xe9B\xea]n\xb5\x03\xb2\x02\xa3\r{O\u007f\xbfԖ|\xcf|hot\x12\xf9\xf8\x90Wdoz8\x98>\xe1\xb7`\xbc\x83\xc1<\x03c>\x05\x92?\xa9WB\xa4\x85_3M\xe4wa\x03\x9dj\x94\xcdz\xbd'\x9dFƆaH\x9e\xf4y]\xba\x9f\xb6I\x03\xcb\xda\xe1\x01\xfb\xb5\xd0~e\xd8v\xa4h51\xaeM\xa4U\x81\xee\xcbش\x83\xfb\x86\xc7!\x93\xf7gX\xf597\x8c(\x93ߟ8J7\u007fE\x81\xdc\xcbU\xf6\x9aZoq$:\x9b2;w\x9f\xef\xbf\xc0tt\x11c\xce~\xe1\xfd\x98(G\t2a\xe4w\xc8U\xc4\x1d\x87\xa1\xd4D\xefb \xaf\xe5\xc5\xf6\x84~N\xbf\xa4\xed@*SKf\xadZ\xb8*{\x04\xb6\b):\xa3\xe8Z\xb8\xf6pe\x06쯌\xe0\xff.@fZV\x99طIp\xba\x02\xe7\xc1\x95\xb5\x13Ǵ\xa3^\xd1kah\xef#ڬ`&1gӎl\x19\x0f\xd8\x05\x86\xa7\x8el7\r\xed\x8cݗ\x01o\xcf\x1c\xcb\x03\x9d\x9fZ&/\xa5\xb9\xe7\xd5\xcbCю\x18g]\xb8:)\xf6&^\xd4h\x92\xff\xc8Lə\xb8\xb1\x89\x19\xbd\x8e\x95ʶXJz+\x17\xc8\x1c\xf8\xc2:\x03\xf5\xb9\x04\x95\xef\x9c!/`\xfc\xf3\x98\b\xda\x19\x85'\xe4<\x066\xa4\xbcgЁK\x17\xfc\x8d\xb4tX\xc5\xca\xc2F\x0e\x16Eڋ8R\x1c\x160}E\x9d\xfc\xe4o\xa8\xd9\xf6\xb8\x01儯(k\x98\xcd\xf3\xcc\x17;#\v\xadpF\xc1m\x8eY\xd2\x00\xebV\xc7\u007f\x17\xa1\xd0\xed\xd3py\xd2\nn\xf0i\xc1z\xedo9\xec\x19e\xde\xf2\xd9y[\xd9+\xdf\xd47\xb2\xb4ؔ\x17F\xc9\xfbΝ\xb0(\x1a\xd8\xec'^\x8f-l\xacŨ\xe8n\xe6\u007f\x1d\xefޝ\xfd>\x94W\x1b\xbc\xa3\xfa\xd3\x04\xbf\xfd\xdeԪ\xe8\x1e\xa6\xbf\x81l\xfc'\x00\x00\xff\xff\x8c\xdb\x1fܮ\t\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\x80\xef\xfa\x15\x98}\x0f{y%\xefN\x0f\xed\xe8\xd6\xcd\xee!\xd36\xe3I2\xb9tz\xa0I\xd8\xe2F\"Y\x00t\xeav\xfa\xdf;$%\u007f\xc8v6=\x947\x81 \x00>\xf8\x10\xab\xba\xae+\x15\xec\x13\x12[\xefZP\xc1\xe2\x1f\x82.}q\xf3\xfc\x037\xd6/\xb6\x1f\xabg\xebL\v7\x91\xc5\x0f\xf7\xc8>\x92\xc6ϸ\xb6Ί\xf5\xae\x1aP\x94Q\xa2\xda\n@9\xe7E%1\xa7O\x00흐\xef{\xa4z\x83\xaey\x8e+\\E\xdb\x1b\xa4l|r\xbd\xfd\xd0|\xdf|\xa8\x004a>\xfeh\adQCh\xc1ž\xaf\x00\x9c\x1a\xb0\x05\x83=\n\xae\x94~\x8e\x81\xf0\xf7\x88,\xdcl\xb1G\xf2\x8d\xf5\x15\a\xd4\xc9\xf1\x86|\f-\x1c6\xca\xf91\xa8r\xa1\xcf\xd9ԧl꾘ʻ\xbde\xf9\xe9\x9a\xc6\xcfv\xd4\n}$\xd5_\x0e(+\xb0u\x9b\xd8+\xba\xa8R\x01\xb0\xf6\x01[\xb8Ka\x05\xa5\xd1T\x00#\x8f\x1cf\rʘLX\xf5K\xb2N\x90n|\x1f\x87\x89l\r\x06Y\x93\r\x92\t>v\x98\xaf\b~\r\xd2!\x14w \x1eV8F`\xf29\x80\xaf\xec\xddRI\xd7B\x93x5E5\x052*\x14ԟ\xe6b٥\x80YȺ͵\x10X\x94D\x9e\x82\xc8~\xadw@G|O\x03\xc8\xfaM\xe8\x14\x9fz\u007f\xc8\x1b\xd7<\x17\x9d\xed\xc7BZw8\xa8v\xd4\xf5\x01ݏ\xcbۧ\xef\x1eN\xc4p\x1a\xeb\x85ԂePS\xa4\t\\\xa1\x06\xde!x\x82\xc1\xd3D\x95\x9b\xbd\xd1@> \x89\x9dJ\xab\xac\xa3\xae:\x92\xceBx\x9f\xa2,Z`R;!ghc\x11\xa0\x19/V`Z\x06\xc2@\xc8\xe8J\x83\x9d\x18\x86\xa4\xa4\x1c\xf8\xd5W\xd4\xd2\xc0\x03R2\x03\xdc\xf9؛ԅ[$\x01B\xed7\xce\xfe\xb9\xb7\xcd\xe9\x9e\xc9i\xaf䐟i\xe5\xa2s\xaa\x87\xad\xea#\xfe\x1f\x9430\xa8\x1d\x10&/\x10ݑ\xbd\xac\xc2\r\xfc\x920Y\xb7\xf6-t\"\x81\xdb\xc5bce\x9a&\xda\x0fCtVv\x8b<\x18\xec*\x8a'^\x18\xdcb\xbf`\xbb\xa9\x15\xe9\xce\nj\x89\x84\v\x15l\x9dCwy\xa24\x83\xf9\x1f\x8d\xf3\x87ߟ\xc4zV e\xe5F\u007f%\x03\xa9\xcdK\xda\xcb\xd1r\x8b\x03\xe8$Jt\xee\xbf<<\xc2\xe4:'cN?s?\x1c\xe4C\n\x120\xeb\xd6H%\x89k\xf2C\xb6\x89\xce\x04o\x9d\xe4\x0f\xdd[ts\xfc\x1cW\x83\x15\x9eJ2媁\x9b>\xd1U]ו\n\xe6\x16#\x19\xef֠\x82\xc1/\x8cN\xbe\xa8\xb9\xfb\x91\x1a\xe3W\xbb7՝q\xed\x1a\xae\x12\xb1\x1f\xae\x91|\x8a\x1a\xdf\xe2\xd68\xc3ƻj@V\xadb\xb5\xae\x00\x94s\x9e\x95L\x93|\x02h\xef8zk1\xd6\x1d\xba\xe6.mp\x93\x8cm1f\xe7\xd3ѻ\xd7\xcd\x0f\xcd\xeb\n@G\xcc\xdb?\x9b\x01\x89\xd5\x10\xd6\xe0\x92\xb5\x15\x80S\x03\xae\xa1\xf5\xf7\xcez\xd5F\xfc+!15;\xb4\x18}c|E\x01\xb5\x1c\xdaE\x9f\xc2\x1a\x0e\ve\xef\b\xa8\\\xe6\xed\xe8溸\xc9+\xd6\x10\xff\xb2\xb4\xfa\xc1\x8c\x16\xc1\xa6\xa8\xec9\x88\xbcH\xc6uɪx\xb6\\\x01\x90\xf6\x01\xd7\xf0Q`\x04\xa5\xb1\xad\x00ƻgX\xf5x\xbbݛ\xe2J\xf78\xa8\x82\x17\xc0\at?}z\u007f\xfb\xfd\xcd\xc94@\x8b\xa4\xa3\t\x9c#8\xc3\f\x86@\xc1\x88\x00\xd8\xefA\x81r\xa0\"\x9b\xad\xd2\f\xdb\xe8\a\xd8(}\x97\xc2\xde+\x80\xdf\xfc\x89\x9a\x81\xd8G\xd5\xe1+\xa0\xa4{P⯘\x82\xf5\x1dl\x8d\xc5f\xbf)D\x1f0\xb2\x99\xa2\\\xc6\x11\xb9\x8efg\xc0_\xca݊\x15\xb4\xc2*$\xe0\x1e\xa7\xf8`;\x86\x03\xfc\x16\xb87\x04\x11CDBWxv\xe2\x18\xc4H\xb9\xf1\x06\r\xdc`\x147@\xbdO\xb6\x152\xee02DԾs\xe6\xef\xbdo\x92\bɡV\xf1D\x87\xc30\x8e1:ea\xa7l\xc2W\xa0\\\v\x83z\x80\x889N\xc9\x1d\xf9\xcb&\xd4\xc0\xaf>\"\x18\xb7\xf5k\xe8\x99\x03\xadW\xab\xce\xf0TT\xda\x0fCr\x86\x1fV\xb9>\xcc&\xb1\x8f\xb4jq\x87vE\xa6\xabUԽaԜ\"\xaeT0u\x86\xeera5C\xfbM\x1cː^\x9e`\xe5\a\xa1\x19q4\xae;ZȜ\u007f$\x03\xc2\xfaB\x98\xb2\xb5\xdc\xe2\x10h\x99\x92\xe8\\\xbf\xbb\xf9\f\xd3\xd19\x19\xf3\xe8\x17\xe6\xec7\xd2!\x05\x120\xe3\xb6\x18K\x123\xf3\xc4'\xba6x\xe38\u007fhk\xd0\xcd\xc3Oi3\x18\xa6\x89̒\xab\x06\xae\xb2\xd2\xc0\x06!\x85V1\xb6\r\xbcwp\xa5\x06\xb4W\x8a\xf0\u007fO\x80D\x9aj\t\xec\xf3Rp,\x92s\xe3\x12\xb5\xa3\x85I\xc9.\xe4kV\xea7\x01\xb5dO\x02(;\xcd\xd6\xe8\\\x1a\xb0\xf5\x11ԡ\xf2\xc7\x006'\x9e\x97+7\x83S\xb1C\x9e\xcfΰ|\xceFr\xfc}\xafN\x85\xe6[l\xbaF\xb4\x82F E=\xbek\xce<^\xc6\x00\x8b\xec]D2\x91X\xc2 q\x15)\x10\x91:\xc6t~\xb4\ftiX>\xa0\x86\x9f3\xe6\x0f\xbe{t\xfd\xca;\x16\xba?jt\xebm\x1a\xf0Ʃ@\xbd\u007f\xc2\xf6=\xe3\xf0<\xcb\xe9A\xde?R\xe7\x86\xd7(R\x8e\x97/1\x1a\\#%{\xe1\xb8\v\xb4\x9eF~\xbe\x9eΑ<\x80S\x8edK\xd1t\x04i\v\xa2CF:\xc8˽\xe1~\xd1#\xc0}ot\x9f7\xe6\x04\x8br\x11ym\xb2\x0e|=|\xa9\v\x13q\x81du&\xdf´\x80?\x9b\xbeP͗\x0e\xa8\xc7\n{\x96\"\xb0\xe2D_\xa1\t\xd9~\n\xb5N1\xa2\xe3\xd1K~#\xe7\x1b\x9e+\nS%\xfdv\xfd\xe1\tex{\xb0\xcc]\xa02\xae\xa0\t\x11k2\x9d\xbc\xec\xb2&ڐk\xf6<\x18e\x9cv\x1a\xa7\x81Z\xcc(~\t&f\x05|\x02\u2efda\x110t\xe5q\x9a\xf7R\xd9!R~\xf8\xb5\x9a\xb7\x1c26\b-Zdla\xf3P\x94\xf8\x81\x18\x87s\xdc[\x1f\a\xc5k\x90G\xabf\xb3@#\xe9w\xd5\xc6\xe2\x1a8\xa6K,[\xbcx\xe8\x15-\x94\xe1ɝ?\x89\xcd\x121\xf6\xc5\xf8(3\xe0\xa2^\xd6\xf0\x11\xef\x17f?E\xaf\x91\b\xcf\xcb\xe8\xe2M\x16\x8b\xe0l\x92\xa4\xb3h\x8f\xa246\xac\xc73i\xb3\xef\x94&\xc4c)\xc1?\xffV\x87\xaaRZc`l?\xce\u007f\x14^\xbc8\xe9\xfc\xf3\xa7\xf6\xae5\xe5\x1f\a~\xff\xa3*\ac{;5\xf42\xf9_\x00\x00\x00\xff\xff\xcbT\xc3P]\r\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs#\xb7\x11\xbe\xf3Wt\xad\x0f\x8a\xab\x96C\xdbI%)\xdev\xa58\xa5\xc4֪č.[{\x00\a\xcd!\xac\x19\x00\x010\xe42.\xff\xf7T\xe31\x9c\aHJ\xaa\xac3\x17\x89x4>4\xfa\xf150\x9b\xcf\xe73\xa6\xc5#\x1a+\x94\\\x02\xd3\x02\xbf8\x94\xf4\xcb\x16O\u007f\xb5\x85P\x8b\xdd\xf7\xb3'!\xf9\x12\xae[\xebT\xf3\x80V\xb5\xa6\xc4\x1b\xdc\b)\x9cPr֠c\x9c9\xb6\x9c\x010)\x95c\xd4l\xe9'@\xa9\xa43\xaa\xae\xd1\xcc+\x94\xc5S\xbb\xc6u+j\x8e\xc6\vOK\xef\xbe+\xfeR|7\x03(\r\xfa\xe9\x1fE\x83ֱF/A\xb6u=\x03\x90\xac\xc1%h\xc5w\xaan\x1b\\\xb3\xf2\xa9ն\xd8a\x8dF\x15Bͬƒ\x16\xad\x8cj\xf5\x12\x8e\x1dan\x04\x146s\xaf\xf8\xa3\x17\xf3ދ\xf1=\xb5\xb0\ue7f9ޟ\x84u~\x84\xae[\xc3\xea)\b\xdfi\x85\xacښ\x99I\xf7\f\xc0\x96J\xe3\x12\xee\b\x86f%\xf2\x19@ܻ\x875\aƹ\xd7&\xab\uf350\x0e\xcd5IHZ\x9c\x03G[\x1a\xa1\x9d\xd7ֽ\xe2\x10\x00B@\b\xd61\xd7Z\xb0m\xb9\x05f\xe1\x0e\xf7\x8b[yoTe\xd0\x06x\x00\xbfX%\xef\x99\xdb.\xa1\b\xc3\v\xbde\x16coP\xef\xcaw\xc4&w \xd0\xd6\x19!\xab\x1c\f:#\xd8oQ\x82\xdb\n\va\xb7\xb0g\x96\xe0\x18\xe7w\x99_\xd8\xf7wG<@pM\x06\xd0M\r\x108s\x98\x03\xd0\xe9\x13\xd4\x06\xdc\x16I\xf3\xde☐BV\xbe)\x9c\x048\x05k\xf4\x10\x91C\xab3\xc84\x96\x85V\xbc\x90I\xe8\x00\xd6ݨ\xf5\x92nh\xfc\xff\x1a\xd5\x00н⯀\xf2\xa2u\xc3\xe0\xc1\xaa\x8f\xfd\xa6K\v?\xa0u\xa2\x04\x83ZY\xe1\x949\x80\xe0(\x9d\xd8\b4\xb0Q\xa6o6' \xd0\xdc\xdbn\xd2\x00J\x94\xfe\x80Z\xbdB\x11\xd1oVN\x19V!\xfc\xa4J\x1fvȜ\r\x0e\xec\xd9nU[sX\xa7]\x03X\xa7Lָ\tq\x98\x15\xe5&\xb1#\x1f\x1b\xaey\x1a}Ov\n\xb2\xc5$@\x0ed\xbf\xab0\xef9\xa1{\xf7}\bU\xe5\x16\x1b\xb6\x8c#\x95F\xf9\xee\xfe\xf6\xf1\x8f\xabA3\x806J\xa3q\"\x85\xce\xf0\xf52F\xaf\x15\x86\xaa\xbe\"\x81a\x14pJ\x15h\x83\xfd\x856\xe4\x11C8\x0ea\xc9H\fZ\x94\xae\xaf\x92\xf4\xa9\r0\tj\xfd\v\x96\xae\x80\x15\x1a\x12\x93\x0e\xa6Tr\x87Ɓ\xc1RUR\xfc\xa7\x93m\xc9\xcciњ9\x8c\x11\xfc\xf8\xf9 +Y\r;V\xb7\xf8\x16\x98\xe4а\x03\x18\xa4U\xa0\x95=y~\x88-\xe0ge\x10\x84ܨ%l\x9d\xd3v\xb9XT¥LY\xaa\xa6i\xa5p\x87\x85Ozb\xdd:e\xec\x82\xe3\x0e\xeb\x85\x15՜\x99r+\x1c\x96\xae5\xb8`Z\xcc=t\xe9\xb3e\xd1\xf0oḼ\xf6j\x80ub\x18\xe1\xf3\x89\xec\xcc\tP*\x03a\x81ũa\x17GE\xa7P\xf4\xf0\xb7\xd5GHK\xfb\xc3\x18k\xdf\xeb\xfd8\xd1\x1e\x8f\x80\x14&\xe4\x06\xa3+o\x8cj\xbcL\x94\\+!\x9d\xffQ\xd6\x02\xe5X\xfd\xb6]7\xc2ѹ\xff\xbbE\xeb\xe8\xac\n\xb8\xf6\xf4\x81BS\xab\xc9ry\x01\xb7\x12\xaeY\x83\xf55\xb3\xf8\xd5\x0f\x804m\xe7\xa4\xd8\xe7\x1dA\x9f\xf9\x8c\a\a\xad\xf5:\x12=9q^#α\xd2X\xd2\xe9\x91\x02i\xa6؈\x18\xa1(p\xb2\xf1\xf0b 8\xef\xb8\xf4e\xa3\xd3x\xd0\b\xd9\xfbܜ\x84M\xf6bj\n\x98a\xe4D(@=\x8e\xb2d\x8e\x93\x1cac\x80-&\x12N\x1c\x03}Rq\xbc\xb0\x8f;\xc51\a\x9b\xa6\x82۲`\xadĭ(\x1e\xb5RNW\xa1O\xc9\x17\x01ӊ_\xc0\x15Wd`p\x83\x06e\x89)p\x9d#\x0e\x19d\xfd\x94>\xc5x\xda(\xe0LT\xcf\"~w\u007f\x9b\"yRb\xc4\xee\xa6\xeb^\xd0\x0f}\x1b\x815\xf7\x89\xee\xf2\xdaW\xb7\x9b\xb0\x98\x8fiN\x01\x03-0P\xc0.I\x80\x90\xd6!\xe3\xa06Y\x89T\xa8\x009\xbe\xc18\xe3m\x88`1T\x1eS\v\xe9\x1e\x18\xc5N\xc1\xe1\x1f\xab\x0fw\x8b\xbf\xe7T\xdf\xed\x02XY\xa2\xf5\f\xd8a\x83ҽ\xedH9G+\fr\xa2\xd8X4L\x8a\rZW\xc45\xd0\xd8O?|\xcek\x0f\xe0Ge\x00\xbf\xb0F\xd7\xf8\x16D\xd0x\x17\x96\x93\xd1\b\x1b\xd4\xd1I\x84\xbdp[1N\xa6\x9d\x06ȼ\xe2\xb6\xf7~\xbb\x8e=!\xa8\xb8\xdd\x16\xa1\x16O\xb8\x847\x9e\xd6\x1ca\xfeJ\xbe\xf3ۛ\x13R\xff\x10\\\xfb\r\rz\x13\xc0uy\xb8\xeftG\x90\xc1\xf3\x8c\xa8*<\xb2\xaa\xf1\xe7\x93\n\x85\xeaoA\x19ҀT=\x11^0\x9d^\b\x94\xc8'\xa0?\xfd\xf0\xf9$⡾@H\x8e_\xe0\a\x10\xb1\xacъ\u007f[\xc0Go\x1d\a\xe9\xd8\x17Z\xa9\xdc*\x8b\xa74\xabd}\xa0=o\xd9\x0e\xc1**\x92\xb0\xae\xe7\x81\aqس\x03i!\x1d\x1c\xd9\x1b\x03͌;k\xad\x89\xfd|\xfcp\xf3a\x19\x90\x91AU>\x12S\xd6\xdc\bb3DcB.\xf6\xd68I\xe6\xe9\xb3m0\x1f\xa7\xa0\xdc2Ya\xd8/¦\xa5\xecX\\\xbdƏ\xa7\x94$}\x19j2\x0e\x1c\xff\xb7\xe4\xfe\xcc\xcdy\x06\xfd\x8c\xcd\xf5\xab\x8c\xb3\x9b{j\xd7h$:\xf4\xfb㪴\xb4\xb5\x12\xb5\xb3\v\xb5C\xb3\x13\xb8_\xec\x95y\x12\xb2\x9a\x93i\u0383\r\u0605/Q\x17\xdf\xf8?\xafދ\xaff\x9f\xbb\xa1A\x95\xfd5wE\xeb\xd8ū6\x958\xec\xf3\xf3\xd8\xd5*2\xab\xf1\\r\x8b\xfdV\x94\xdbT\x9c\xc4\x18{\u0099\x041a\x1eB3\x93\x87\xafnʤ\xd0\xd6\x10\xa2\xc3<^\xb0͙\xe4\xf4\xbf\x15\xd6Q\xfb\xab4؊g\xb9\xef\xbfno~\x1f\x03oū|\xf5\x04\x01\x0f6ҿO\xb8@\xcc\x1e\x06\x83\x13u\xcc0\xd6n̋\x98\xa1cU\x86\x8a\xf5/\x02\xcf\x11\xb6\xb3\x1a\x18^ӱ\xca\x023\b\f\x1a\xa6\xe9\xe4\x9e\xf00\x0f)^3A\xf9\x99Rpw\xcf\x01L\xebZdSqL䑄F\xbeO\x856\xab쩽g\xcf!H\xb8\xa0\xffxř\xa1\xec\x11@\xe07\x1dm\xf7\xb7Z9^|\x9a\x14\x9f\xd4\"ե\xc4ֆ\x10\xe7\xf9\x02j4\x86\n\x8aQ\x93V|Ԓ\xbd\xd9J\x9d\x83\x9b\xb7\xb3\xca\f\x17\xaa/\xa8+\xc3Eq\xd4i\x88\".]\x1f\x13\x85~meY*b\xa7ë\xfb\xf3\xc7{=\x9d\xe1/q\f\x0f\xe0\x9ch\xc8f{\xd7\xcaq\x8d\\i\b=qa\xa6\x8f\xdb$\r\xb9\xa7\x8e\xc4l7L\xd4\xc8!\xbd\x1d\x8c\xe7d\xa4\xf6\xa5\xacqCA\xaeյb<\x15d\x11^GϨ^\xf7\xb7#W\xf6\x8c\xcc\xd6\"\xf7\x95|F\tSʶQ\xa6a.\xdc\xe6ͳBe[\xd7l]\xe3\x12\x9ci\xa7\xddg\x82E\x83ֲ\xea\x92+\xfe\x1cF\x85:5N\x01\xb6V\xad\xeb\n\xd5AP\xb8\xb2Ѧ^V+gK\xc0\xa193\xa2\xe86Rպ\xf6s\xfa\x81\xe0\xf8\xe0\xe4Q\xad1\x9f\xea^\x13\x13\x00\xfc\x83\xc9%\x844&\xe7`]\xf4:\xeba\xf4\xa1l\x9b\xe9*s\xb8\xc3}\xa6u\xf2\xd0\xd3\xef\xbcN.\x93\xe9\xfb\xd1{Ë\xf6\x1f\x17\xba\xa4\x828\f\xb6\xaaNά\x1c\xabA\xb6\xcd\x1a\r\xe9a}ph\x87\xe1n\xe6r\xd0\n53\xe4\xe9\xfe&\xfcz\xfcT\xf3\x16\xac\xf0\xd7{ķ\x02\x01\vŷ\xa5\xe4D\xc4R\x19̄L\x98\xa6\x95A\x12\x19\xc2\xff=\xf3G\xd6N&\x8d\x1e9\xefɎW\xc4\xfd\x96v\xdd=\u007f\xa4Ê\xdc\x06~\xfdmv\xa49\xac\xa4\x02\x02\xf9\xdd\xf8I\xffM\xb8\xe0Io\xf4\xfeg\xa9d`\xd3v\t\x9f>\xcf\xd2\xd3\xddczz\xa7\xc6\xff\x06\x00\x00\xff\xff\xb6\xe8a\xa8\a!\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\xe3\xb8\x11\xbe\xebWty\x0f\xceV\r\xa9\xddI*I\xe9\xb6kgSJv=\xae\x913\x97\xa99@DS\xec\x98\x04\x18\xa0)YI忧\x1a \xf4\xa4\x1ev\xd5Lx\xb1\x85G\xe3\xeb\xaf\x1f\xe8&GY\x96\x8dTK\x9f\xd0y\xb2f\x02\xaa%|a4\xf2\xcb\xe7\xcf\u007f\xf69\xd9\xf1\xf2\xc7\xd13\x19=\x81\xbbγm>\xa2\xb7\x9d+\xf0\x1eK2\xc4dͨAVZ\xb1\x9a\x8c\x00\x941\x96\x95\f{\xf9\tPX\xc3\xce\xd65\xbal\x81&\u007f\xee\xe68\xef\xa8\xd6\xe8\x82\xf0t\xf4\xf2\x87\xfcO\xf9\x0f#\x80\xc2a\xd8\xfeD\rzVM;\x01\xd3\xd5\xf5\b\xc0\xa8\x06'\xd0Z\xbd\xb4uנC\xcf֡ϗX\xa3\xb39ّo\xb1\x90S\x17\xcev\xed\x04\xb6\x13qs\x8f(j\xf3h\xf5\xa7 \xe7c\x94\x13\xa6j\xf2\xfc\xf7\xc1\xe9_\xc9sX\xd2֝S\xf5\x00\x8e0\xeb\xc9,\xbaZ\xb9\xe3\xf9\x11\x80/l\x8b\x13x\x10(\xad*P\x8f\x00z\x02\x02\xb4\xacWq\xf9c\x94UTب\x88\x19\xc0\xb6h~z\x9c~\xfa\xfdlo\x18\xa0u\xb6EǔԋώYwF\x014\xfa\xc2Qˁ\xf4[\x11\x18W\x81\x16{\xa2\a\xae0\x81B\xddc\x00[\x02W\xe4\xc1a\xebУ\x89\x16\xde\x13\f\xb2H\x19\xb0\xf3\u007fb\xc19\xccЉ\x18\xf0\x95\xedj-n\xb0D\xc7ర\vC\xff\xde\xc8\xf6\xc06\x1cZ+ƞ\xe3\xedC\x86\xd1\x19U\xc3R\xd5\x1d\xbe\x03e44j\r\x0e\xe5\x14\xe8̎\xbc\xb0\xc4\xe7\xf0\x9bu\bdJ;\x81\x8a\xb9\xf5\x93\xf1xA\x9cܹ\xb0M\xd3\x19\xe2\xf58x&\xcd;\xb6Ώ5.\xb1\x1e{Zd\xca\x15\x151\x16\xdc9\x1c\xab\x96\xb2\x00\xdd\x04\x97\xce\x1b\xfd\x9d\xeb\x03\xc0\xdf\xeea\xe5\xb5\xd8ֳ#\xb3ؙ\b\xcev\xc6\x02\xe2m@\x1eT\xbf5j\xb1%Z\x86\x84\x9d\x8f\u007f\x99=A::\x18\xe3\x90\xfd\xc0\xfbv\xa3ߚ@\b#S\xa2\x8bF,\x9dm\x82L4\xba\xb5d8\xfc(jBsH\xbf\xef\xe6\r\xb1\xd8\xfd_\x1dz\x16[\xe5p\x17b\x1c\xe6\b]\xab\x15\xa3\xceaj\xe0N5X\xdf)\x8f_\xdd\x00´τ\xd8\xebL\xb0\x9b\x9e\x0e\x17G\xd6v&R\n9a\xafô0k\xb1\x10\xf3\t\x83\xb2\x95J*Bl@i\x1d\xa8\xa3\xf5\xf9\x9e\xe8\xe1Еg\xae\x8a箝\xb1uj\x81\xbf\xda(\xf3p\xd1\x01\xb6\x9f\x87\xf6$p\x92Yb\x18c/\x1c|\\y$\x14\xa0N\x9bW\x15:\f{$\x8bQ!\xeee=\xb1uk\x11\x1cT\xd2\xf9\x91\x84\x13\x86\b*[}A\x8dG\xdb\a\x84\xc3\x12\x1d\x1aq\xf7\x98!Z\x1b\xf2\b+2),b\x8a\x05\xb6\x03Z\xcc#\xeaa\x88\xa7\xa9\x873\xd9s\x10\xf0O\x8fӔ1\x13\xc3=t>>\xf7\x02=\U00094135~T\\]q\xf6\xed\xb4\x8c\x87\x85\xdc\xc1\x16\x14\xb4\x84\x05\xee%c \xe3\x19\x95\x06[\x0eJ\x94[\x1b$\xc0\x1c\xf6;\xde\xc5Lѧ\xa4m\n\x17\xeaAI\x8e\"\r\u007f\x9b}x\x18\xffu\x88\xf9\x8d\x16\xa0\x8a\x02\xbd\bR\x8c\r\x1a~\a\xbe+*P^\xd4 \x87z&3y\xa3\f\x95\xe89\xef\xcf@\xe7?\xbf\xff2\xcc\x1e\xc0/\xd6\x01\xbe\xa8\xa6\xad\xf1\x1dPd|\x93\xfe\x92ϐ\x8ftl$\u008a\xb8\xa2\xc3KkÀxW\xaf\xf6*\xa8\xcb\xea\x19\xc1\xf6\xeav\b5=\xe3\x04n$\xcaw`\xfeG\x02\xeb\xbf7'\xa4\xfe.\x06Ѝ,\xba\x89\xe06\xf7\xddnDnAr\xa5\x18\xd8\xd1b\x81.\x14\bCOHޒ\x12\xbf\a\xeb\x84\x01cwD\x04\xc1b\xbd\x98\x8fP\x1f\x81\xfe\xfc\xfe\xcbI\xc4\xfb|\x01\x19\x8d/\xf0\x1e\xc8DnZ\xab\xbf\xcf\xe1)x\xc7ڰz\x91\x93\x8a\xcaz<Ŭ5\xf5Zt\xae\xd4\x12\xc1\xdb\x06a\x85u\x9d\xc5zC\xc3J\xad\x85\x85d8\xf17\x05\xadr|\xd6[S\x95\xf1\xf4\xe1\xfe\xc3$\"\x13\x87Z\x84|'\xb7SIR5H\xb9\x10\xef\xbc\xe0\x8dG\x97fz|\x17݇-\x14\x952\v\x8c\xfa\"\x94\x9d\xdcB\xf9\xed[\xe2\xf8\xf8\xeaO\xcf@\tp\x988\xfeo\x97\xe8\x95ʅJ\xf5\n\xe5\x1ev\xbc\xfc\xacr\xd2\x188\x83\x8cA?m\v/\xaa\x15ز\x1f\xdb%\xba%\xe1j\xbc\xb2\xee\x99\xcc\"\x13\xd7̢\x0f\xf8q(\xed\xc7߅?o\xd6%\x14\xe4\xd7*\x14\x16\u007f\v\xad\xe4\x1c?~\x93R\xa9V\xbc\xfe\x1e\xbb\x9d\xf5\x05\xcc\xe1^\t\x8bUEE\x95\x9a\x80>Ǟ\b&\x92\x8aS\xc7Ԭ\xcc\xfa\xab\xbb\xb2\x10\xda9A\xb4\xce\xfan3SF\xcb\xff\x9e<\xcb\xf8\x9b\x18\xec\xe8\xaa\xf0\xfd\xc7\xf4\xfe\xdb8xGo\x8a\xd5\x13\x85n\xf4\x91\xd6N\xb5PY\x12\xba\vu\xd9ǽũ\xae\x1c\xa8\v7k^U\x18z\xa3Z_Y\x9e\xde_\xc01\xdb,L\x18\xb6\x06\xe8\xcb\xc1$K\x1c\xf7l\x15x\x06O\x14u\x01K\xac\xed\x87j\xec\x1eI\xac9\u0088Ե\x01\xcfp\xb0\xbe\x16\xa1\xb4dR@\xed#̆;\x87\x835\xad\xd5\a#\xfb\x9ep0\xb95\xcd\xc1DT\U000aad8a\x15w\xfe5\x8dUؐ\x98\x8d\xf1ͽ\x98Pܾ\xb9\xb5*\xac\x14\x8e\xfb\xaf\x98\xce[\xf9\xeexGx\x8f\xe1tD\xc7\xd4`\xe8W\x02\x0eX)\x9f\x0e\x19\xb2(\xecȋ[CN\x15q\xa8CY'Ug\xa9\xa8F\r\x9b\x97\\\xf0$\x1dfh\xe8o\x87\xaa\x98$\xa8\xf3\xa8C\xef9\x00\xfax_i]\xa3x\x02\xd2\xc6g\"\xe2h\x85\xe9\xeaZ\xcdk\x9c\x00\xbb\xeex\xfaL\x005\xe8\xbdZ\\\x8a\xa0\xdf\xe2\xaa\xd8\xf1\xf5[@\xcdmǛ\x96\xaf\x0f\xa5\x9e\x8a[\xdf{\xc1\xeb\xda\xceJ\xf9KP\x1ee͐\xc7m\x82\xfa\xbc\xcbɃ\xa6k\x8e\x8f\xc9\xe0\x01W\x03\xa3S\xf3\xe8\xec¡?\xb6L\x96\f8\xd0\x04d\xf0K\xf0\x8eW\x11\xd0\x1ft\x89\x83~\x19T\xb6N\xdemY\xd5`\xbaf\x8eN\x88\x98\xaf\x19}b$\xa5\x86\xa1\x1e:\xd4\xde[&\xb7\x12R\xb6\x8b\xa2\xfan\xa2P&\xbcR\x12\xffe\v\x9a|[\xab\xf5\x80ܤI\xb8^\xc5}%\x8e\xb6\x1e\x93\xa2P\xc2?̽\xb6\xf7\x0f\xa0\xee\xad9Q\r\xa6\x90!\xc3\u007f\xfcÙۘ\f\xe3\xe2 \x95\xf6\xf3B\xe8\xcfr\xca\xd79\xe1̅\xefY9\xbe6\xed\xcd\xf6\x16_\xcaxA\xf4p\xbe\xdbM]ljj\xff\x98o\x99\xa3\x06\x89:\x1a\f\xc8\xf5\x8e\xec\xfe\xbdY?\xb2\xbd\xd9T!\xc5\x1c\xea\x87\xc3O\r77{_\x0e\xc2\xcf\xc2\x1aM\xf13\t|\xfe2\x82\xfe]ڧ\xf49@\x06\xff\x17\x00\x00\xff\xffñ\x1b\xae\xa0\x19\x00\x00"), diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1f6a9f12a..fbb442f1e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -12,6 +12,7 @@ rules: - backups verbs: - create + - delete - apiGroups: - velero.io resources: @@ -32,6 +33,26 @@ rules: - get - patch - update +- apiGroups: + - velero.io + resources: + - deletebackuprequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - velero.io + resources: + - deletebackuprequests/status + verbs: + - get + - patch + - update - apiGroups: - velero.io resources: @@ -52,6 +73,26 @@ rules: - get - patch - update +- apiGroups: + - velero.io + resources: + - podvolumebackups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - velero.io + resources: + - podvolumebackups/status + verbs: + - get + - patch + - update - apiGroups: - velero.io resources: @@ -92,23 +133,3 @@ rules: - get - patch - update -- apiGroups: - - velero.io - resources: - - podvolumebackup - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - velero.io - resources: - - podvolumebackup/status - verbs: - - get - - patch - - update \ No newline at end of file diff --git a/pkg/apis/velero/v1/delete_backup_request.go b/pkg/apis/velero/v1/delete_backup_request_types.go similarity index 81% rename from pkg/apis/velero/v1/delete_backup_request.go rename to pkg/apis/velero/v1/delete_backup_request_types.go index 6e2eb5172..6e14d28d7 100644 --- a/pkg/apis/velero/v1/delete_backup_request.go +++ b/pkg/apis/velero/v1/delete_backup_request_types.go @@ -50,8 +50,15 @@ type DeleteBackupRequestStatus struct { Errors []string `json:"errors,omitempty"` } +// 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 +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="BackupName",type="string",JSONPath=".spec.backupName",description="The name of the backup to be deleted" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="The status of the deletion request" // DeleteBackupRequest is a request to delete one or more backups. type DeleteBackupRequest struct { @@ -68,6 +75,7 @@ type DeleteBackupRequest struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true // DeleteBackupRequestList is a list of DeleteBackupRequests. type DeleteBackupRequestList struct { diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 00b4ca0fa..7c31fa759 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -669,34 +669,6 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string } } - deletionControllerRunInfo := func() controllerRunInfo { - deletionController := controller.NewBackupDeletionController( - s.logger, - s.sharedInformerFactory.Velero().V1().DeleteBackupRequests(), - s.veleroClient.VeleroV1(), // deleteBackupRequestClient - s.veleroClient.VeleroV1(), // backupClient - s.sharedInformerFactory.Velero().V1().Restores().Lister(), - s.veleroClient.VeleroV1(), // restoreClient - backupTracker, - s.resticManager, - s.sharedInformerFactory.Velero().V1().PodVolumeBackups().Lister(), - s.mgr.GetClient(), - s.sharedInformerFactory.Velero().V1().VolumeSnapshotLocations().Lister(), - csiVSLister, - csiVSCLister, - s.csiSnapshotClient, - newPluginManager, - backupStoreGetter, - s.metrics, - s.discoveryHelper, - ) - - return controllerRunInfo{ - controller: deletionController, - numWorkers: defaultControllerWorkers, - } - } - restoreControllerRunInfo := func() controllerRunInfo { restorer, err := restore.NewKubernetesRestorer( s.veleroClient.VeleroV1(), @@ -756,7 +728,6 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string controller.BackupSync: backupSyncControllerRunInfo, controller.Backup: backupControllerRunInfo, controller.GarbageCollection: gcControllerRunInfo, - controller.BackupDeletion: deletionControllerRunInfo, controller.Restore: restoreControllerRunInfo, controller.ResticRepo: resticRepoControllerRunInfo, } @@ -830,6 +801,19 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.logger.Fatal(err, "unable to create controller", "controller", controller.Schedule) } + if err := controller.NewBackupDeletionReconciler( + s.logger, + s.mgr.GetClient(), + backupTracker, + s.resticManager, + s.metrics, + s.discoveryHelper, + newPluginManager, + backupStoreGetter, + ).SetupWithManager(s.mgr); err != nil { + s.logger.Fatal(err, "unable to create controller", "controller", controller.BackupDeletion) + } + if _, ok := enabledRuntimeControllers[controller.ServerStatusRequest]; ok { r := controller.ServerStatusRequestReconciler{ Scheme: s.mgr.GetScheme(), diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 2ca0fda47..67c53dd7b 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -23,25 +23,19 @@ import ( "time" jsonpatch "github.com/evanphx/json-patch" - snapshotterClientSet "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" - snapshotv1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "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/util/clock" kubeerrs "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/client-go/tools/cache" + "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" "github.com/vmware-tanzu/velero/internal/delete" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/discovery" - 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/label" "github.com/vmware-tanzu/velero/pkg/metrics" "github.com/vmware-tanzu/velero/pkg/persistence" @@ -54,250 +48,202 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -const resticTimeout = time.Minute +const ( + resticTimeout = time.Minute + deleteBackupRequestMaxAge = 24 * time.Hour +) -type backupDeletionController struct { - *genericController - - deleteBackupRequestClient velerov1client.DeleteBackupRequestsGetter - deleteBackupRequestLister velerov1listers.DeleteBackupRequestLister - backupClient velerov1client.BackupsGetter - restoreLister velerov1listers.RestoreLister - restoreClient velerov1client.RestoresGetter - backupTracker BackupTracker - resticMgr restic.RepositoryManager - podvolumeBackupLister velerov1listers.PodVolumeBackupLister - kbClient client.Client - snapshotLocationLister velerov1listers.VolumeSnapshotLocationLister - csiSnapshotLister snapshotv1listers.VolumeSnapshotLister - csiSnapshotContentLister snapshotv1listers.VolumeSnapshotContentLister - csiSnapshotClient *snapshotterClientSet.Clientset - processRequestFunc func(*velerov1api.DeleteBackupRequest) error - clock clock.Clock - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager - backupStoreGetter persistence.ObjectBackupStoreGetter - metrics *metrics.ServerMetrics - helper discovery.Helper +type backupDeletionReconciler struct { + client.Client + logger logrus.FieldLogger + backupTracker BackupTracker + resticMgr restic.RepositoryManager + metrics *metrics.ServerMetrics + clock clock.Clock + discoveryHelper discovery.Helper + newPluginManager func(logrus.FieldLogger) clientmgmt.Manager + backupStoreGetter persistence.ObjectBackupStoreGetter } -// NewBackupDeletionController creates a new backup deletion controller. -func NewBackupDeletionController( +// NewBackupDeletionReconciler creates a new backup deletion reconciler. +func NewBackupDeletionReconciler( logger logrus.FieldLogger, - deleteBackupRequestInformer velerov1informers.DeleteBackupRequestInformer, - deleteBackupRequestClient velerov1client.DeleteBackupRequestsGetter, - backupClient velerov1client.BackupsGetter, - restoreLister velerov1listers.RestoreLister, - restoreClient velerov1client.RestoresGetter, + client client.Client, backupTracker BackupTracker, resticMgr restic.RepositoryManager, - podvolumeBackupLister velerov1listers.PodVolumeBackupLister, - kbClient client.Client, - snapshotLocationLister velerov1listers.VolumeSnapshotLocationLister, - csiSnapshotLister snapshotv1listers.VolumeSnapshotLister, - csiSnapshotContentLister snapshotv1listers.VolumeSnapshotContentLister, - csiSnapshotClient *snapshotterClientSet.Clientset, - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, - backupStoreGetter persistence.ObjectBackupStoreGetter, metrics *metrics.ServerMetrics, helper discovery.Helper, -) Interface { - c := &backupDeletionController{ - genericController: newGenericController(BackupDeletion, logger), - deleteBackupRequestClient: deleteBackupRequestClient, - deleteBackupRequestLister: deleteBackupRequestInformer.Lister(), - backupClient: backupClient, - restoreLister: restoreLister, - restoreClient: restoreClient, - backupTracker: backupTracker, - resticMgr: resticMgr, - podvolumeBackupLister: podvolumeBackupLister, - kbClient: kbClient, - snapshotLocationLister: snapshotLocationLister, - csiSnapshotLister: csiSnapshotLister, - csiSnapshotContentLister: csiSnapshotContentLister, - csiSnapshotClient: csiSnapshotClient, - metrics: metrics, - helper: helper, - // 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, +) *backupDeletionReconciler { + return &backupDeletionReconciler{ + Client: client, + logger: logger, + backupTracker: backupTracker, + resticMgr: resticMgr, + metrics: metrics, + clock: clock.RealClock{}, + discoveryHelper: helper, newPluginManager: newPluginManager, backupStoreGetter: backupStoreGetter, - - clock: &clock.RealClock{}, } - - c.syncHandler = c.processQueueItem - c.processRequestFunc = c.processRequest - - deleteBackupRequestInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: c.enqueue, - }, - ) - - c.resyncPeriod = time.Hour - c.resyncFunc = c.deleteExpiredRequests - - return c } -func (c *backupDeletionController) processQueueItem(key string) error { - log := c.logger.WithField("key", key) - log.Debug("Running processItem") - - ns, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - return errors.Wrap(err, "error splitting queue key") - } - - req, err := c.deleteBackupRequestLister.DeleteBackupRequests(ns).Get(name) - if apierrors.IsNotFound(err) { - log.Debug("Unable to find DeleteBackupRequest") - return nil - } - if err != nil { - return errors.Wrap(err, "error getting DeleteBackupRequest") - } - - switch req.Status.Phase { - case velerov1api.DeleteBackupRequestPhaseProcessed: - // Don't do anything because it's already been processed - default: - // Don't mutate the shared cache - reqCopy := req.DeepCopy() - return c.processRequestFunc(reqCopy) - } - - return nil +func (r *backupDeletionReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Make sure the expired requests can be deleted eventually + s := kube.NewPeriodicalEnqueueSource(r.logger, mgr.GetClient(), &velerov1api.DeleteBackupRequestList{}, time.Hour) + return ctrl.NewControllerManagedBy(mgr). + For(&velerov1api.DeleteBackupRequest{}). + Watches(s, nil). + Complete(r) } -func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupRequest) error { - log := c.logger.WithFields(logrus.Fields{ - "namespace": req.Namespace, - "name": req.Name, - "backup": req.Spec.BackupName, +// +kubebuilder:rbac:groups=velero.io,resources=deletebackuprequests,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=velero.io,resources=deletebackuprequests/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=delete + +func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.logger.WithFields(logrus.Fields{ + "controller": BackupDeletion, + "deletebackuprequest": req.String(), }) + log.Debug("Getting deletebackuprequest") + dbr := &velerov1api.DeleteBackupRequest{} + if err := r.Get(ctx, req.NamespacedName, dbr); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Unable to find the deletebackuprequest") + return ctrl.Result{}, nil + } + log.WithError(err).Error("Error getting deletebackuprequest") + return ctrl.Result{}, err + } - var err error + // Since we use the reconciler along with the PeriodicalEnqueueSource, there may be reconciliation triggered by + // stale requests. + if dbr.Status.Phase == velerov1api.DeleteBackupRequestPhaseProcessed { + age := r.clock.Now().Sub(dbr.CreationTimestamp.Time) + if age >= deleteBackupRequestMaxAge { // delete the expired request + log.Debug("The request is expired, deleting it.") + if err := r.Delete(ctx, dbr); err != nil { + log.WithError(err).Error("Error deleting DeleteBackupRequest") + } + } else { + log.Info("The request has been processed, skip.") + } + return ctrl.Result{}, nil + } // Make sure we have the backup name - if req.Spec.BackupName == "" { - _, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { - r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed - r.Status.Errors = []string{"spec.backupName is required"} + if dbr.Spec.BackupName == "" { + _, err := r.patchDeleteBackupRequest(ctx, dbr, func(res *velerov1api.DeleteBackupRequest) { + res.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed + res.Status.Errors = []string{"spec.backupName is required"} }) - return err + return ctrl.Result{}, err } + log = log.WithField("backup", dbr.Spec.BackupName) + // Remove any existing deletion requests for this backup so we only have // one at a time - if errs := c.deleteExistingDeletionRequests(req, log); errs != nil { - return kubeerrs.NewAggregate(errs) + if errs := r.deleteExistingDeletionRequests(ctx, dbr, log); errs != nil { + return ctrl.Result{}, kubeerrs.NewAggregate(errs) } // Don't allow deleting an in-progress backup - if c.backupTracker.Contains(req.Namespace, req.Spec.BackupName) { - _, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + if r.backupTracker.Contains(dbr.Namespace, dbr.Spec.BackupName) { + _, err := r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed r.Status.Errors = []string{"backup is still in progress"} }) - - return err + return ctrl.Result{}, err } // Get the backup we're trying to delete - backup, err := c.backupClient.Backups(req.Namespace).Get(context.TODO(), req.Spec.BackupName, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { + backup := &velerov1api.Backup{} + if err := r.Get(ctx, types.NamespacedName{ + Namespace: dbr.Namespace, + Name: dbr.Spec.BackupName, + }, backup); apierrors.IsNotFound(err) { // Couldn't find backup - update status to Processed and record the not-found error - req, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + _, err = r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed r.Status.Errors = []string{"backup not found"} }) - - return err - } - if err != nil { - return errors.Wrap(err, "error getting backup") + return ctrl.Result{}, err + } else if err != nil { + return ctrl.Result{}, errors.Wrap(err, "error getting backup") } // Don't allow deleting backups in read-only storage locations location := &velerov1api.BackupStorageLocation{} - if err := c.kbClient.Get(context.Background(), client.ObjectKey{ + if err := r.Get(context.Background(), client.ObjectKey{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, location); err != nil { if apierrors.IsNotFound(err) { - _, err := c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + _, err := r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed r.Status.Errors = append(r.Status.Errors, fmt.Sprintf("backup storage location %s not found", backup.Spec.StorageLocation)) }) - return err + return ctrl.Result{}, err } - return errors.Wrap(err, "error getting backup storage location") + return ctrl.Result{}, errors.Wrap(err, "error getting backup storage location") } if location.Spec.AccessMode == velerov1api.BackupStorageLocationAccessModeReadOnly { - _, err := c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + _, err := r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed r.Status.Errors = append(r.Status.Errors, fmt.Sprintf("cannot delete backup because backup storage location %s is currently in read-only mode", location.Name)) }) - return err + return ctrl.Result{}, err } // if the request object has no labels defined, initialise an empty map since // we will be updating labels - if req.Labels == nil { - req.Labels = map[string]string{} + if dbr.Labels == nil { + dbr.Labels = map[string]string{} } - - // Update status to InProgress and set backup-name label if needed - req, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + // Update status to InProgress and set backup-name and backup-uid label if needed + dbr, err := r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseInProgress - if req.Labels[velerov1api.BackupNameLabel] == "" { - req.Labels[velerov1api.BackupNameLabel] = label.GetValidName(req.Spec.BackupName) + if r.Labels[velerov1api.BackupNameLabel] == "" { + r.Labels[velerov1api.BackupNameLabel] = label.GetValidName(dbr.Spec.BackupName) + } + + if r.Labels[velerov1api.BackupUIDLabel] == "" { + r.Labels[velerov1api.BackupUIDLabel] = string(backup.UID) } }) if err != nil { - return err - } - - // Set backup-uid label if needed - if req.Labels[velerov1api.BackupUIDLabel] == "" { - req, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { - req.Labels[velerov1api.BackupUIDLabel] = string(backup.UID) - }) - if err != nil { - return err - } + return ctrl.Result{}, err } // Set backup status to Deleting - backup, err = c.patchBackup(backup, func(b *velerov1api.Backup) { + backup, err = r.patchBackup(ctx, backup, func(b *velerov1api.Backup) { b.Status.Phase = velerov1api.BackupPhaseDeleting }) if err != nil { log.WithError(errors.WithStack(err)).Error("Error setting backup phase to deleting") - return err + return ctrl.Result{}, err } backupScheduleName := backup.GetLabels()[velerov1api.ScheduleNameLabel] - c.metrics.RegisterBackupDeletionAttempt(backupScheduleName) + r.metrics.RegisterBackupDeletionAttempt(backupScheduleName) - var errs []string - - pluginManager := c.newPluginManager(log) + pluginManager := r.newPluginManager(log) defer pluginManager.CleanupClients() - backupStore, err := c.backupStoreGetter.Get(location, pluginManager, log) + backupStore, err := r.backupStoreGetter.Get(location, pluginManager, log) if err != nil { - return errors.Wrap(err, "error getting the backup store") + return ctrl.Result{}, errors.Wrap(err, "error getting the backup store") } actions, err := pluginManager.GetDeleteItemActions() log.Debugf("%d actions before invoking actions", len(actions)) if err != nil { - return errors.Wrap(err, "error getting delete item actions") + return ctrl.Result{}, errors.Wrap(err, "error getting delete item actions") } // don't defer CleanupClients here, since it was already called above. @@ -308,13 +254,13 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR if err != nil { log.WithError(err).Errorf("Unable to download tarball for backup %s, skipping associated DeleteItemAction plugins", backup.Name) } else { - defer closeAndRemoveFile(backupFile, c.logger) + defer closeAndRemoveFile(backupFile, r.logger) ctx := &delete.Context{ Backup: backup, BackupReader: backupFile, Actions: actions, - Log: c.logger, - DiscoveryHelper: c.helper, + Log: r.logger, + DiscoveryHelper: r.discoveryHelper, Filesystem: filesystem.NewFileSystem(), } @@ -322,11 +268,13 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR // but what do we do with the error returned? We can't just swallow it as that may lead to dangling resources. err = delete.InvokeDeleteActions(ctx) if err != nil { - return errors.Wrap(err, "error invoking delete item actions") + return ctrl.Result{}, errors.Wrap(err, "error invoking delete item actions") } } } + var errs []string + if backupStore != nil { log.Info("Removing PV snapshots") @@ -340,7 +288,7 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR volumeSnapshotter, ok := volumeSnapshotters[snapshot.Spec.Location] if !ok { - if volumeSnapshotter, err = volumeSnapshotterForSnapshotLocation(backup.Namespace, snapshot.Spec.Location, c.snapshotLocationLister, pluginManager); err != nil { + if volumeSnapshotter, err = volumeSnapshottersForVSL(ctx, backup.Namespace, snapshot.Spec.Location, r.Client, pluginManager); err != nil { errs = append(errs, err.Error()) continue } @@ -353,9 +301,8 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR } } } - log.Info("Removing restic snapshots") - if deleteErrs := c.deleteResticSnapshots(backup); len(deleteErrs) > 0 { + if deleteErrs := r.deleteResticSnapshots(ctx, backup); len(deleteErrs) > 0 { for _, err := range deleteErrs { errs = append(errs, err.Error()) } @@ -369,15 +316,19 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR } log.Info("Removing restores") - if restores, err := c.restoreLister.Restores(backup.Namespace).List(labels.Everything()); err != nil { + restoreList := &velerov1api.RestoreList{} + selector := labels.Everything() + if err := r.List(ctx, restoreList, &client.ListOptions{ + Namespace: backup.Namespace, + LabelSelector: selector, + }); err != nil { log.WithError(errors.WithStack(err)).Error("Error listing restore API objects") } else { - for _, restore := range restores { + for _, restore := range restoreList.Items { if restore.Spec.BackupName != backup.Name { continue } - - restoreLog := log.WithField("restore", kube.NamespaceAndName(restore)) + restoreLog := log.WithField("restore", kube.NamespaceAndName(&restore)) restoreLog.Info("Deleting restore log/results from backup storage") if err := backupStore.DeleteRestore(restore.Name); err != nil { @@ -387,202 +338,160 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR } restoreLog.Info("Deleting restore referencing backup") - if err := c.restoreClient.Restores(restore.Namespace).Delete(context.TODO(), restore.Name, metav1.DeleteOptions{}); err != nil { - errs = append(errs, errors.Wrapf(err, "error deleting restore %s", kube.NamespaceAndName(restore)).Error()) + if err := r.Delete(ctx, &restore); err != nil { + errs = append(errs, errors.Wrapf(err, "error deleting restore %s", kube.NamespaceAndName(&restore)).Error()) } } } if len(errs) == 0 { // Only try to delete the backup object from kube if everything preceding went smoothly - err = c.backupClient.Backups(backup.Namespace).Delete(context.TODO(), backup.Name, metav1.DeleteOptions{}) - if err != nil { + if err := r.Delete(ctx, backup); err != nil { errs = append(errs, errors.Wrapf(err, "error deleting backup %s", kube.NamespaceAndName(backup)).Error()) } } if len(errs) == 0 { - c.metrics.RegisterBackupDeletionSuccess(backupScheduleName) + r.metrics.RegisterBackupDeletionSuccess(backupScheduleName) } else { - c.metrics.RegisterBackupDeletionFailed(backupScheduleName) + r.metrics.RegisterBackupDeletionFailed(backupScheduleName) } // Update status to processed and record errors - req, err = c.patchDeleteBackupRequest(req, func(r *velerov1api.DeleteBackupRequest) { + if _, err := r.patchDeleteBackupRequest(ctx, dbr, func(r *velerov1api.DeleteBackupRequest) { r.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed r.Status.Errors = errs - }) - if err != nil { - return err + }); err != nil { + return ctrl.Result{}, err } - // Everything deleted correctly, so we can delete all DeleteBackupRequests for this backup if len(errs) == 0 { - listOptions := pkgbackup.NewDeleteBackupRequestListOptions(backup.Name, string(backup.UID)) - err = c.deleteBackupRequestClient.DeleteBackupRequests(req.Namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, listOptions) + labelSelector, err := labels.Parse(fmt.Sprintf("%s=%s,%s=%s", velerov1api.BackupNameLabel, label.GetValidName(backup.Name), velerov1api.BackupUIDLabel, backup.UID)) + if err != nil { + // Should not be here + r.logger.WithError(err).WithField("backup", kube.NamespaceAndName(backup)).Error("error creating label selector for the backup for deleting DeleteBackupRequests") + return ctrl.Result{}, nil + } + alldbr := &velerov1api.DeleteBackupRequest{} + err = r.DeleteAllOf(ctx, alldbr, client.MatchingLabelsSelector{ + Selector: labelSelector, + }, client.InNamespace(dbr.Namespace)) if err != nil { // If this errors, all we can do is log it. - c.logger.WithField("backup", kube.NamespaceAndName(backup)).Error("error deleting all associated DeleteBackupRequests after successfully deleting the backup") + r.logger.WithError(err).WithField("backup", kube.NamespaceAndName(backup)).Error("error deleting all associated DeleteBackupRequests after successfully deleting the backup") } } + log.Infof("Reconciliation done") - return nil + return ctrl.Result{}, nil } -func volumeSnapshotterForSnapshotLocation( - namespace, snapshotLocationName string, - snapshotLocationLister velerov1listers.VolumeSnapshotLocationLister, +func volumeSnapshottersForVSL( + ctx context.Context, + namespace, vslName string, + client client.Client, pluginManager clientmgmt.Manager, ) (velero.VolumeSnapshotter, error) { - snapshotLocation, err := snapshotLocationLister.VolumeSnapshotLocations(namespace).Get(snapshotLocationName) + vsl := &velerov1api.VolumeSnapshotLocation{} + if err := client.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: vslName, + }, vsl); err != nil { + return nil, errors.Wrapf(err, "error getting volume snapshot location %s", vslName) + } + volumeSnapshotter, err := pluginManager.GetVolumeSnapshotter(vsl.Spec.Provider) if err != nil { - return nil, errors.Wrapf(err, "error getting volume snapshot location %s", snapshotLocationName) + return nil, errors.Wrapf(err, "error getting volume snapshotter for provider %s", vsl.Spec.Provider) } - volumeSnapshotter, err := pluginManager.GetVolumeSnapshotter(snapshotLocation.Spec.Provider) - if err != nil { - return nil, errors.Wrapf(err, "error getting volume snapshotter for provider %s", snapshotLocation.Spec.Provider) - } - - if err = volumeSnapshotter.Init(snapshotLocation.Spec.Config); err != nil { - return nil, errors.Wrapf(err, "error initializing volume snapshotter for volume snapshot location %s", snapshotLocationName) + if err = volumeSnapshotter.Init(vsl.Spec.Config); err != nil { + return nil, errors.Wrapf(err, "error initializing volume snapshotter for volume snapshot location %s", vslName) } return volumeSnapshotter, nil } -func (c *backupDeletionController) deleteExistingDeletionRequests(req *velerov1api.DeleteBackupRequest, log logrus.FieldLogger) []error { +func (r *backupDeletionReconciler) deleteExistingDeletionRequests(ctx context.Context, req *velerov1api.DeleteBackupRequest, log logrus.FieldLogger) []error { log.Info("Removing existing deletion requests for backup") + dbrList := &velerov1api.DeleteBackupRequestList{} selector := label.NewSelectorForBackup(req.Spec.BackupName) - dbrs, err := c.deleteBackupRequestLister.DeleteBackupRequests(req.Namespace).List(selector) - if err != nil { + if err := r.List(ctx, dbrList, &client.ListOptions{ + Namespace: req.Namespace, + LabelSelector: selector, + }); err != nil { return []error{errors.Wrap(err, "error listing existing DeleteBackupRequests for backup")} } - var errs []error - for _, dbr := range dbrs { + for _, dbr := range dbrList.Items { if dbr.Name == req.Name { continue } - - if err := c.deleteBackupRequestClient.DeleteBackupRequests(req.Namespace).Delete(context.TODO(), dbr.Name, metav1.DeleteOptions{}); err != nil { + if err := r.Delete(ctx, &dbr); err != nil { errs = append(errs, errors.WithStack(err)) + } else { + log.Infof("deletion request '%s' removed.", dbr.Name) } } - return errs } -func (c *backupDeletionController) deleteResticSnapshots(backup *velerov1api.Backup) []error { - if c.resticMgr == nil { +func (r *backupDeletionReconciler) deleteResticSnapshots(ctx context.Context, backup *velerov1api.Backup) []error { + if r.resticMgr == nil { return nil } - snapshots, err := restic.GetSnapshotsInBackup(backup, c.podvolumeBackupLister) + snapshots, err := restic.GetSnapshotsInBackup(ctx, backup, r.Client) if err != nil { return []error{err} } - ctx, cancelFunc := context.WithTimeout(context.Background(), resticTimeout) + ctx2, cancelFunc := context.WithTimeout(ctx, resticTimeout) defer cancelFunc() var errs []error for _, snapshot := range snapshots { - if err := c.resticMgr.Forget(ctx, snapshot); err != nil { + if err := r.resticMgr.Forget(ctx2, snapshot); err != nil { errs = append(errs, err) } } - return errs } -const deleteBackupRequestMaxAge = 24 * time.Hour - -func (c *backupDeletionController) deleteExpiredRequests() { - c.logger.Info("Checking for expired DeleteBackupRequests") - defer c.logger.Info("Done checking for expired DeleteBackupRequests") - - // Our shared informer factory filters on a single namespace, so asking for all is ok here. - requests, err := c.deleteBackupRequestLister.List(labels.Everything()) +func (r *backupDeletionReconciler) patchDeleteBackupRequest(ctx context.Context, req *velerov1api.DeleteBackupRequest, mutate func(*velerov1api.DeleteBackupRequest)) (*velerov1api.DeleteBackupRequest, error) { + patchHelper, err := patch.NewHelper(req, r.Client) if err != nil { - c.logger.WithError(err).Error("unable to check for expired DeleteBackupRequests") - return + return nil, errors.Wrap(err, "unable to get the patch helper") } - - now := c.clock.Now() - - for _, req := range requests { - if req.Status.Phase != velerov1api.DeleteBackupRequestPhaseProcessed { - continue - } - - age := now.Sub(req.CreationTimestamp.Time) - if age >= deleteBackupRequestMaxAge { - reqLog := c.logger.WithFields(logrus.Fields{"namespace": req.Namespace, "name": req.Name}) - reqLog.Info("Deleting expired DeleteBackupRequest") - - err = c.deleteBackupRequestClient.DeleteBackupRequests(req.Namespace).Delete(context.TODO(), req.Name, metav1.DeleteOptions{}) - if err != nil { - reqLog.WithError(err).Error("Error deleting DeleteBackupRequest") - } - } - } -} - -func (c *backupDeletionController) patchDeleteBackupRequest(req *velerov1api.DeleteBackupRequest, mutate func(*velerov1api.DeleteBackupRequest)) (*velerov1api.DeleteBackupRequest, error) { - // Record original json - oldData, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(err, "error marshalling original DeleteBackupRequest") - } - // Mutate mutate(req) - - // Record new json - newData, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(err, "error marshalling updated DeleteBackupRequest") + if err := patchHelper.Patch(ctx, req); err != nil { + return nil, errors.Wrap(err, "error patching the deletebackuprquest") } - - patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) - if err != nil { - return nil, errors.Wrap(err, "error creating json merge patch for DeleteBackupRequest") - } - - req, err = c.deleteBackupRequestClient.DeleteBackupRequests(req.Namespace).Patch(context.TODO(), req.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) - if err != nil { - return nil, errors.Wrap(err, "error patching DeleteBackupRequest") - } - return req, nil } -func (c *backupDeletionController) patchBackup(backup *velerov1api.Backup, mutate func(*velerov1api.Backup)) (*velerov1api.Backup, error) { +func (r *backupDeletionReconciler) patchBackup(ctx context.Context, backup *velerov1api.Backup, mutate func(*velerov1api.Backup)) (*velerov1api.Backup, error) { + //TODO: The patchHelper can't be used here because the `backup/xxx/status` does not exist, until the bakcup resource is refactored + // Record original json oldData, err := json.Marshal(backup) if err != nil { return nil, errors.Wrap(err, "error marshalling original Backup") } - // Mutate - mutate(backup) - - // Record new json - newData, err := json.Marshal(backup) + newBackup := backup.DeepCopy() + mutate(newBackup) + newData, err := json.Marshal(newBackup) if err != nil { return nil, errors.Wrap(err, "error marshalling updated Backup") } - patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) if err != nil { return nil, errors.Wrap(err, "error creating json merge patch for Backup") } - backup, err = c.backupClient.Backups(backup.Namespace).Patch(context.TODO(), backup.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) - if err != nil { + if err := r.Client.Patch(ctx, backup, client.RawPatch(types.MergePatchType, patchBytes)); err != nil { return nil, errors.Wrap(err, "error patching Backup") } - return backup, nil } diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index a03e98eb0..816f90627 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -18,33 +18,35 @@ package controller import ( "bytes" - "context" "fmt" - "io/ioutil" - "strings" - "testing" "time" - "github.com/pkg/errors" + "context" + "io/ioutil" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + + "strings" + "testing" + + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/sets" - core "k8s.io/client-go/testing" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks" "github.com/vmware-tanzu/velero/pkg/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" - "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake" + "github.com/vmware-tanzu/velero/pkg/builder" informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions" "github.com/vmware-tanzu/velero/pkg/metrics" persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks" @@ -53,134 +55,49 @@ import ( velerotest "github.com/vmware-tanzu/velero/pkg/test" ) -func TestBackupDeletionControllerProcessQueueItem(t *testing.T) { - client := fake.NewSimpleClientset() - sharedInformers := informers.NewSharedInformerFactory(client, 0) - - controller := NewBackupDeletionController( - velerotest.NewLogger(), - sharedInformers.Velero().V1().DeleteBackupRequests(), - client.VeleroV1(), // deleteBackupRequestClient - client.VeleroV1(), // backupClient - sharedInformers.Velero().V1().Restores().Lister(), - client.VeleroV1(), // restoreClient - NewBackupTracker(), - nil, // restic repository manager - sharedInformers.Velero().V1().PodVolumeBackups().Lister(), - nil, - sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister(), - nil, // csiSnapshotLister - nil, // csiSnapshotContentLister - nil, // csiSnapshotClient - nil, // new plugin manager func - nil, // backupStoreGetter - metrics.NewServerMetrics(), - nil, // discovery helper - ).(*backupDeletionController) - - // Error splitting key - err := controller.processQueueItem("foo/bar/baz") - assert.Error(t, err) - - // Can't find DeleteBackupRequest - err = controller.processQueueItem("foo/bar") - assert.NoError(t, err) - - // Already processed - req := pkgbackup.NewDeleteBackupRequest("foo", "uid") - req.Namespace = "foo" - req.Name = "foo-abcde" - req.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed - - err = controller.processQueueItem("foo/bar") - assert.NoError(t, err) - - // Invoke processRequestFunc - for _, phase := range []velerov1api.DeleteBackupRequestPhase{"", velerov1api.DeleteBackupRequestPhaseNew, velerov1api.DeleteBackupRequestPhaseInProgress} { - t.Run(fmt.Sprintf("phase=%s", phase), func(t *testing.T) { - req.Status.Phase = phase - sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(req) - - var errorToReturn error - var actual *velerov1api.DeleteBackupRequest - var called bool - controller.processRequestFunc = func(r *velerov1api.DeleteBackupRequest) error { - called = true - actual = r - return errorToReturn - } - - // No error - err = controller.processQueueItem("foo/foo-abcde") - require.True(t, called, "processRequestFunc wasn't called") - assert.Equal(t, err, errorToReturn) - assert.Equal(t, req, actual) - - // Error - errorToReturn = errors.New("bar") - err = controller.processQueueItem("foo/foo-abcde") - require.True(t, called, "processRequestFunc wasn't called") - assert.Equal(t, err, errorToReturn) - }) - } -} - type backupDeletionControllerTestData struct { - client *fake.Clientset fakeClient client.Client sharedInformers informers.SharedInformerFactory volumeSnapshotter *velerotest.FakeVolumeSnapshotter backupStore *persistencemocks.BackupStore - controller *backupDeletionController - req *velerov1api.DeleteBackupRequest + controller *backupDeletionReconciler + req ctrl.Request } -func setupBackupDeletionControllerTest(t *testing.T, objects ...runtime.Object) *backupDeletionControllerTestData { +func defaultTestDbr() *velerov1api.DeleteBackupRequest { req := pkgbackup.NewDeleteBackupRequest("foo", "uid") - req.Namespace = "velero" + req.Namespace = velerov1api.DefaultNamespace req.Name = "foo-abcde" + return req +} + +func setupBackupDeletionControllerTest(t *testing.T, req *velerov1api.DeleteBackupRequest, objects ...runtime.Object) *backupDeletionControllerTestData { var ( - client = fake.NewSimpleClientset(append(objects, req)...) - fakeClient = velerotest.NewFakeControllerRuntimeClient(t, objects...) - sharedInformers = informers.NewSharedInformerFactory(client, 0) + fakeClient = velerotest.NewFakeControllerRuntimeClient(t, append(objects, req)...) volumeSnapshotter = &velerotest.FakeVolumeSnapshotter{SnapshotsTaken: sets.NewString()} pluginManager = &pluginmocks.Manager{} backupStore = &persistencemocks.BackupStore{} ) data := &backupDeletionControllerTestData{ - client: client, fakeClient: fakeClient, - sharedInformers: sharedInformers, volumeSnapshotter: volumeSnapshotter, backupStore: backupStore, - controller: NewBackupDeletionController( + controller: NewBackupDeletionReconciler( velerotest.NewLogger(), - sharedInformers.Velero().V1().DeleteBackupRequests(), - client.VeleroV1(), // deleteBackupRequestClient - client.VeleroV1(), // backupClient - sharedInformers.Velero().V1().Restores().Lister(), - client.VeleroV1(), // restoreClient + fakeClient, NewBackupTracker(), nil, // restic repository manager - sharedInformers.Velero().V1().PodVolumeBackups().Lister(), - fakeClient, - sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister(), - nil, // csiSnapshotLister - nil, // csiSnapshotContentLister - nil, // csiSnapshotClient - func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, - NewFakeSingleObjectBackupStoreGetter(backupStore), metrics.NewServerMetrics(), nil, // discovery helper - ).(*backupDeletionController), - - req: req, + func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + NewFakeSingleObjectBackupStoreGetter(backupStore), + ), + req: ctrl.Request{NamespacedName: types.NamespacedName{Namespace: req.Namespace, Name: req.Name}}, } pluginManager.On("CleanupClients").Return(nil) - return data } @@ -201,59 +118,51 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { }, }, } - td := setupBackupDeletionControllerTest(t, location, backup) + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), location, backup) td.controller.backupStoreGetter = &fakeErrorBackupStoreGetter{} - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(ctx, td.req) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "error getting the backup store")) }) t.Run("missing spec.backupName", func(t *testing.T) { - td := setupBackupDeletionControllerTest(t) - td.req.Spec.BackupName = "" + dbr := defaultTestDbr() + dbr.Spec.BackupName = "" + td := setupBackupDeletionControllerTest(t, dbr) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(ctx, td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"errors":["spec.backupName is required"],"phase":"Processed"}}`), - ), - } - - assert.Equal(t, expectedActions, td.client.Actions()) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "spec.backupName is required", res.Status.Errors[0]) }) t.Run("existing deletion requests for the backup are deleted", func(t *testing.T) { - td := setupBackupDeletionControllerTest(t) + input := defaultTestDbr() + td := setupBackupDeletionControllerTest(t, input) - // add the backup to the tracker so the execution of processRequest doesn't progress + // add the backup to the tracker so the execution of reconcile doesn't progress // past checking for an in-progress backup. this makes validation easier. - td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName) - - require.NoError(t, td.sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(td.req)) - + td.controller.backupTracker.Add(td.req.Namespace, input.Spec.BackupName) existing := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: td.req.Namespace, Name: "bar", Labels: map[string]string{ - velerov1api.BackupNameLabel: td.req.Spec.BackupName, + velerov1api.BackupNameLabel: input.Spec.BackupName, }, }, Spec: velerov1api.DeleteBackupRequestSpec{ - BackupName: td.req.Spec.BackupName, + BackupName: input.Spec.BackupName, }, } - require.NoError(t, td.sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(existing)) - _, err := td.client.VeleroV1().DeleteBackupRequests(td.req.Namespace).Create(context.TODO(), existing, metav1.CreateOptions{}) + err := td.fakeClient.Create(context.TODO(), existing) require.NoError(t, err) - - require.NoError(t, td.sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add( + existing2 := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: td.req.Namespace, @@ -265,206 +174,103 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { Spec: velerov1api.DeleteBackupRequestSpec{ BackupName: "some-other-backup", }, - }, - )) + } + err = td.fakeClient.Create(context.TODO(), existing2) + require.NoError(t, err) + _, err = td.controller.Reconcile(context.TODO(), td.req) + assert.NoError(t, err) + // verify "existing" is deleted + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: existing.Namespace, + Name: existing.Name, + }, &velerov1api.DeleteBackupRequest{}) - assert.NoError(t, td.controller.processRequest(td.req)) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) - expectedDeleteAction := core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - "bar", - ) - - // first action is the Create of an existing DBR for the backup as part of test data setup - // second action is the Delete of the existing DBR, which we're validating - // third action is the Patch of the DBR to set it to processed with an error - require.Len(t, td.client.Actions(), 3) - assert.Equal(t, expectedDeleteAction, td.client.Actions()[1]) + // verify "existing2" remains + assert.NoError(t, td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: existing2.Namespace, + Name: existing2.Name, + }, &velerov1api.DeleteBackupRequest{})) }) - t.Run("deleting an in progress backup isn't allowed", func(t *testing.T) { - td := setupBackupDeletionControllerTest(t) + dbr := defaultTestDbr() + td := setupBackupDeletionControllerTest(t, dbr) - td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName) - - err := td.controller.processRequest(td.req) + td.controller.backupTracker.Add(td.req.Namespace, dbr.Spec.BackupName) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"errors":["backup is still in progress"],"phase":"Processed"}}`), - ), - } - - assert.Equal(t, expectedActions, td.client.Actions()) - }) - - t.Run("patching to InProgress fails", func(t *testing.T) { - backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() - location := builder.ForBackupStorageLocation("velero", "default").Result() - - td := setupBackupDeletionControllerTest(t, location, backup) - - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("bad") - }) - - err := td.controller.processRequest(td.req) - assert.EqualError(t, err, "error patching DeleteBackupRequest: bad") - - expectedActions := []core.Action{ - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - backup.Namespace, - backup.Name, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"InProgress"}}`), - ), - } - assert.Equal(t, expectedActions, td.client.Actions()) - }) - - t.Run("patching backup to Deleting fails", func(t *testing.T) { - backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() - location := builder.ForBackupStorageLocation("velero", "default").Result() - - td := setupBackupDeletionControllerTest(t, location, backup) - - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, td.req, nil - }) - td.client.PrependReactor("patch", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("bad") - }) - - err := td.controller.processRequest(td.req) - assert.EqualError(t, err, "error patching Backup: bad") - - expectedActions := []core.Action{ - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - backup.Namespace, - backup.Name, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"InProgress"}}`), - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - backup.Namespace, - backup.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"Deleting"}}`), - ), - } - assert.Equal(t, expectedActions, td.client.Actions()) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "backup is still in progress", res.Status.Errors[0]) }) t.Run("unable to find backup", func(t *testing.T) { - td := setupBackupDeletionControllerTest(t) - err := td.controller.processRequest(td.req) + td := setupBackupDeletionControllerTest(t, defaultTestDbr()) + + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"errors":["backup not found"],"phase":"Processed"}}`), - ), - } - - assert.Equal(t, expectedActions, td.client.Actions()) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "backup not found", res.Status.Errors[0]) }) - t.Run("unable to find backup storage location", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() - td := setupBackupDeletionControllerTest(t, backup) + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"errors":["backup storage location default not found"],"phase":"Processed"}}`), - ), - } - - assert.Equal(t, expectedActions, td.client.Actions()) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "backup storage location default not found", res.Status.Errors[0]) }) t.Run("backup storage location is in read-only mode", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() location := builder.ForBackupStorageLocation("velero", "default").AccessMode(velerov1api.BackupStorageLocationAccessModeReadOnly).Result() - td := setupBackupDeletionControllerTest(t, location, backup) + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), location, backup) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"errors":["cannot delete backup because backup storage location default is currently in read-only mode"],"phase":"Processed"}}`), - ), - } - - assert.Equal(t, expectedActions, td.client.Actions()) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "cannot delete backup because backup storage location default is currently in read-only mode", res.Status.Errors[0]) }) - t.Run("full delete, no errors", func(t *testing.T) { + + input := defaultTestDbr() + + // Clear out resource labels to make sure the controller adds them and does not + // panic when encountering a nil Labels map + // (https://github.com/vmware-tanzu/velero/issues/1546) + input.Labels = nil + backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" - restore1 := builder.ForRestore("velero", "restore-1").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() - restore2 := builder.ForRestore("velero", "restore-2").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() - restore3 := builder.ForRestore("velero", "restore-3").Phase(velerov1api.RestorePhaseCompleted).Backup("some-other-backup").Result() - - td := setupBackupDeletionControllerTest(t, backup, restore1, restore2, restore3) - - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore1) - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore2) - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore3) + restore1 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() + restore2 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-2").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() + restore3 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-3").Phase(velerov1api.RestorePhaseCompleted).Backup("some-other-backup").Result() location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ @@ -481,8 +287,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { }, } - require.NoError(t, td.fakeClient.Create(context.Background(), location)) - snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, @@ -492,26 +296,10 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { Provider: "provider-1", }, } - require.NoError(t, td.sharedInformers.Velero().V1().VolumeSnapshotLocations().Informer().GetStore().Add(snapshotLocation)) + td := setupBackupDeletionControllerTest(t, input, backup, restore1, restore2, restore3, location, snapshotLocation) - // Clear out req labels to make sure the controller adds them and does not - // panic when encountering a nil Labels map - // (https://github.com/vmware-tanzu/velero/issues/1546) - td.req.Labels = nil - - td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, td.req, nil - }) - - td.client.PrependReactor("patch", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) - snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ @@ -529,77 +317,56 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } - td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) - td.backupStore.On("GetBackupContents", td.req.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) - td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) + td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) + td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-name":"foo"}},"status":{"phase":"InProgress"}}`), - ), - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-uid":"uid"}}}`), - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - types.MergePatchType, - []byte(`{"status":{"phase":"Deleting"}}`), - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("restores"), - td.req.Namespace, - "restore-1", - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("restores"), - td.req.Namespace, - "restore-2", - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"Processed"}}`), - ), - core.NewDeleteCollectionAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - pkgbackup.NewDeleteBackupRequestListOptions(td.req.Spec.BackupName, "uid"), - ), + // the dbr should be deleted + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + if err == nil { + t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } - velerotest.CompareActions(t, expectedActions, td.client.Actions()) + // backup CR, restore CR restore-1 and restore-2 should be deleted + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: backup.Name, + }, &velerov1api.Backup{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, &velerov1api.Restore{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-2", + }, &velerov1api.Restore{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + // restore-3 should remain + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-3", + }, &velerov1api.Restore{}) + assert.Nil(t, err) + + td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) + td.backupStore.AssertCalled(t, "DeleteRestore", "restore-1") + td.backupStore.AssertCalled(t, "DeleteRestore", "restore-2") // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) - t.Run("full delete, no errors, with backup name greater than 63 chars", func(t *testing.T) { backup := defaultBackup(). ObjectMeta( @@ -609,26 +376,24 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { backup.UID = "uid" backup.Spec.StorageLocation = "primary" - restore1 := builder.ForRestore("velero", "restore-1"). + restore1 := builder.ForRestore(backup.Namespace, "restore-1"). Phase(velerov1api.RestorePhaseCompleted). - Backup("the-really-long-backup-name-that-is-much-more-than-63-characters"). + Backup(backup.Name). Result() - restore2 := builder.ForRestore("velero", "restore-2"). + restore2 := builder.ForRestore(backup.Namespace, "restore-2"). Phase(velerov1api.RestorePhaseCompleted). - Backup("the-really-long-backup-name-that-is-much-more-than-63-characters"). + Backup(backup.Name). Result() - restore3 := builder.ForRestore("velero", "restore-3"). + restore3 := builder.ForRestore(backup.Namespace, "restore-3"). Phase(velerov1api.RestorePhaseCompleted). Backup("some-other-backup"). Result() - td := setupBackupDeletionControllerTest(t, backup, restore1, restore2, restore3) - td.req = pkgbackup.NewDeleteBackupRequest(backup.Name, string(backup.UID)) - td.req.Namespace = "velero" - td.req.Name = "foo-abcde" - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore1) - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore2) - td.sharedInformers.Velero().V1().Restores().Informer().GetStore().Add(restore3) + dbr := pkgbackup.NewDeleteBackupRequest(backup.Name, "uid") + dbr.Namespace = velerov1api.DefaultNamespace + dbr.Name = "foo-abcde" + // Clear out resource labels to make sure the controller adds them + dbr.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ @@ -644,7 +409,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { }, }, } - require.NoError(t, td.fakeClient.Create(context.Background(), location)) snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ @@ -655,23 +419,7 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { Provider: "provider-1", }, } - require.NoError(t, td.sharedInformers.Velero().V1().VolumeSnapshotLocations().Informer().GetStore().Add(snapshotLocation)) - - // Clear out req labels to make sure the controller adds them - td.req.Labels = make(map[string]string) - - td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) - td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") - - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, td.req, nil - }) - - td.client.PrependReactor("patch", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) + td := setupBackupDeletionControllerTest(t, dbr, backup, restore1, restore2, restore3, location, snapshotLocation) snapshots := []*volume.Snapshot{ { @@ -690,83 +438,64 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } - td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) - td.backupStore.On("GetBackupContents", td.req.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) - td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) + td.backupStore.On("GetBackupVolumeSnapshots", dbr.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("GetBackupContents", dbr.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) + td.backupStore.On("DeleteBackup", dbr.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) - err := td.controller.processRequest(td.req) + td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") + + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-name":"the-really-long-backup-name-that-is-much-more-than-63-cha6ca4bc"}},"status":{"phase":"InProgress"}}`), - ), - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-uid":"uid"}}}`), - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - types.MergePatchType, - []byte(`{"status":{"phase":"Deleting"}}`), - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("restores"), - td.req.Namespace, - "restore-1", - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("restores"), - td.req.Namespace, - "restore-2", - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"Processed"}}`), - ), - core.NewDeleteCollectionAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - pkgbackup.NewDeleteBackupRequestListOptions(td.req.Spec.BackupName, "uid"), - ), + // the dbr should be deleted + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + if err == nil { + t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } - velerotest.CompareActions(t, expectedActions, td.client.Actions()) + // backup CR, restore CR restore-1 and restore-2 should be deleted + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: backup.Name, + }, &velerov1api.Backup{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, &velerov1api.Restore{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-2", + }, &velerov1api.Restore{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + + // restore-3 should remain + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-3", + }, &velerov1api.Restore{}) + assert.Nil(t, err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) - t.Run("backup is not downloaded when there are no DeleteItemAction plugins", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" - td := setupBackupDeletionControllerTest(t, backup) + input := defaultTestDbr() + // Clear out resource labels to make sure the controller adds them and does not + // panic when encountering a nil Labels map + // (https://github.com/vmware-tanzu/velero/issues/1546) + input.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ @@ -783,8 +512,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { }, } - require.NoError(t, td.fakeClient.Create(context.Background(), location)) - snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, @@ -794,26 +521,9 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { Provider: "provider-1", }, } - require.NoError(t, td.sharedInformers.Velero().V1().VolumeSnapshotLocations().Informer().GetStore().Add(snapshotLocation)) - - // Clear out req labels to make sure the controller adds them and does not - // panic when encountering a nil Labels map - // (https://github.com/vmware-tanzu/velero/issues/1546) - td.req.Labels = nil - - td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup, location, snapshotLocation) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, td.req, nil - }) - - td.client.PrependReactor("patch", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) - snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ @@ -831,72 +541,43 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } - td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) - td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) + td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - td.backupStore.AssertNotCalled(t, "GetBackupContents", td.req.Spec.BackupName) + td.backupStore.AssertNotCalled(t, "GetBackupContents", mock.Anything) + td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-name":"foo"}},"status":{"phase":"InProgress"}}`), - ), - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-uid":"uid"}}}`), - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - types.MergePatchType, - []byte(`{"status":{"phase":"Deleting"}}`), - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"Processed"}}`), - ), - core.NewDeleteCollectionAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - pkgbackup.NewDeleteBackupRequestListOptions(td.req.Spec.BackupName, "uid"), - ), + // the dbr should be deleted + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + if err == nil { + t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } - velerotest.CompareActions(t, expectedActions, td.client.Actions()) + // backup CR should be deleted + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: backup.Name, + }, &velerov1api.Backup{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) - t.Run("backup is still deleted if downloading tarball fails for DeleteItemAction plugins", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" - td := setupBackupDeletionControllerTest(t, backup) + input := defaultTestDbr() + // Clear out resource labels to make sure the controller adds them and does not + // panic when encountering a nil Labels map + // (https://github.com/vmware-tanzu/velero/issues/1546) + input.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ @@ -913,8 +594,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { }, } - require.NoError(t, td.fakeClient.Create(context.Background(), location)) - snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, @@ -924,26 +603,9 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { Provider: "provider-1", }, } - require.NoError(t, td.sharedInformers.Velero().V1().VolumeSnapshotLocations().Informer().GetStore().Add(snapshotLocation)) - - // Clear out req labels to make sure the controller adds them and does not - // panic when encountering a nil Labels map - // (https://github.com/vmware-tanzu/velero/issues/1546) - td.req.Labels = nil - - td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup, location, snapshotLocation) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") - td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) { - return true, td.req, nil - }) - - td.client.PrependReactor("patch", "backups", func(action core.Action) (bool, runtime.Object, error) { - return true, backup, nil - }) - snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ @@ -961,218 +623,72 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } - td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) - td.backupStore.On("GetBackupContents", td.req.Spec.BackupName).Return(nil, fmt.Errorf("error downloading tarball")) - td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) + td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(nil, fmt.Errorf("error downloading tarball")) + td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) - err := td.controller.processRequest(td.req) + _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) - expectedActions := []core.Action{ - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-name":"foo"}},"status":{"phase":"InProgress"}}`), - ), - core.NewGetAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"metadata":{"labels":{"velero.io/backup-uid":"uid"}}}`), - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - types.MergePatchType, - []byte(`{"status":{"phase":"Deleting"}}`), - ), - core.NewDeleteAction( - velerov1api.SchemeGroupVersion.WithResource("backups"), - td.req.Namespace, - td.req.Spec.BackupName, - ), - core.NewPatchAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - td.req.Name, - types.MergePatchType, - []byte(`{"status":{"phase":"Processed"}}`), - ), - core.NewDeleteCollectionAction( - velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), - td.req.Namespace, - pkgbackup.NewDeleteBackupRequestListOptions(td.req.Spec.BackupName, "uid"), - ), + td.backupStore.AssertCalled(t, "GetBackupContents", input.Spec.BackupName) + td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) + + // the dbr should be deleted + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + if err == nil { + t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } - velerotest.CompareActions(t, expectedActions, td.client.Actions()) + // backup CR should be deleted + err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: backup.Name, + }, &velerov1api.Backup{}) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) -} - -func TestBackupDeletionControllerDeleteExpiredRequests(t *testing.T) { - - now := time.Date(2018, 4, 4, 12, 0, 0, 0, time.UTC) - unexpired1 := time.Date(2018, 4, 4, 11, 0, 0, 0, time.UTC) - unexpired2 := time.Date(2018, 4, 3, 12, 0, 1, 0, time.UTC) - expired1 := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) - expired2 := time.Date(2018, 4, 3, 2, 0, 0, 0, time.UTC) - - tests := []struct { - name string - requests []*velerov1api.DeleteBackupRequest - expectedDeletions []string - }{ - { - name: "no requests", - }, - { - name: "older than max age, phase = '', don't delete", - requests: []*velerov1api.DeleteBackupRequest{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "name", - CreationTimestamp: metav1.Time{Time: expired1}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: "", - }, - }, - }, - }, - { - name: "older than max age, phase = New, don't delete", - requests: []*velerov1api.DeleteBackupRequest{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "name", - CreationTimestamp: metav1.Time{Time: expired1}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseNew, - }, - }, - }, - }, - { - name: "older than max age, phase = InProcess, don't delete", - requests: []*velerov1api.DeleteBackupRequest{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "name", - CreationTimestamp: metav1.Time{Time: expired1}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseInProgress, - }, - }, - }, - }, - { - name: "some expired, some not", - requests: []*velerov1api.DeleteBackupRequest{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "unexpired-1", - CreationTimestamp: metav1.Time{Time: unexpired1}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseProcessed, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "expired-1", - CreationTimestamp: metav1.Time{Time: expired1}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseProcessed, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "unexpired-2", - CreationTimestamp: metav1.Time{Time: unexpired2}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseProcessed, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns", - Name: "expired-2", - CreationTimestamp: metav1.Time{Time: expired2}, - }, - Status: velerov1api.DeleteBackupRequestStatus{ - Phase: velerov1api.DeleteBackupRequestPhaseProcessed, - }, - }, - }, - expectedDeletions: []string{"expired-1", "expired-2"}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - client := fake.NewSimpleClientset() - fakeClient := velerotest.NewFakeControllerRuntimeClient(t) - sharedInformers := informers.NewSharedInformerFactory(client, 0) - - controller := NewBackupDeletionController( - velerotest.NewLogger(), - sharedInformers.Velero().V1().DeleteBackupRequests(), - client.VeleroV1(), // deleteBackupRequestClient - client.VeleroV1(), // backupClient - sharedInformers.Velero().V1().Restores().Lister(), - client.VeleroV1(), // restoreClient - NewBackupTracker(), - nil, - sharedInformers.Velero().V1().PodVolumeBackups().Lister(), - fakeClient, - sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister(), - nil, // csiSnapshotLister - nil, // csiSnapshotContentLister - nil, // csiSnapshotClient - nil, // new plugin manager func - nil, // backupStoreGetter - metrics.NewServerMetrics(), - nil, // discovery helper, - ).(*backupDeletionController) - - fakeClock := &clock.FakeClock{} - fakeClock.SetTime(now) - controller.clock = fakeClock - - for i := range test.requests { - sharedInformers.Velero().V1().DeleteBackupRequests().Informer().GetStore().Add(test.requests[i]) - } - - controller.deleteExpiredRequests() - - expectedActions := []core.Action{} - for _, name := range test.expectedDeletions { - expectedActions = append(expectedActions, core.NewDeleteAction(velerov1api.SchemeGroupVersion.WithResource("deletebackuprequests"), "ns", name)) - } - - velerotest.CompareActions(t, expectedActions, client.Actions()) - }) - } + t.Run("Expired request will be deleted if the status is processed", func(t *testing.T) { + expired := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) + input := defaultTestDbr() + input.CreationTimestamp = metav1.Time{ + Time: expired, + } + input.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed + td := setupBackupDeletionControllerTest(t, input) + td.backupStore.On("DeleteBackup", mock.Anything).Return(nil) + _, err := td.controller.Reconcile(context.TODO(), td.req) + require.NoError(t, err) + + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + td.backupStore.AssertNotCalled(t, "DeleteBackup", mock.Anything) + + }) + + t.Run("Expired request will not be deleted if the status is not processed", func(t *testing.T) { + expired := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) + input := defaultTestDbr() + input.CreationTimestamp = metav1.Time{ + Time: expired, + } + input.Status.Phase = velerov1api.DeleteBackupRequestPhaseNew + td := setupBackupDeletionControllerTest(t, input) + td.backupStore.On("DeleteBackup", mock.Anything).Return(nil) + + _, err := td.controller.Reconcile(context.TODO(), td.req) + require.NoError(t, err) + + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + require.NoError(t, err) + assert.Equal(t, "Processed", string(res.Status.Phase)) + assert.Equal(t, 1, len(res.Status.Errors)) + assert.Equal(t, "backup not found", res.Status.Errors[0]) + + }) } diff --git a/pkg/restic/common.go b/pkg/restic/common.go index e8648cd6c..23c09e558 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -17,6 +17,7 @@ limitations under the License. package restic import ( + "context" "fmt" "os" "strings" @@ -26,10 +27,10 @@ import ( corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) @@ -242,18 +243,21 @@ type SnapshotIdentifier struct { // GetSnapshotsInBackup returns a list of all restic snapshot ids associated with // a given Velero backup. -func GetSnapshotsInBackup(backup *velerov1api.Backup, podVolumeBackupLister velerov1listers.PodVolumeBackupLister) ([]SnapshotIdentifier, error) { - selector := labels.Set(map[string]string{ - velerov1api.BackupNameLabel: label.GetValidName(backup.Name), - }).AsSelector() +func GetSnapshotsInBackup(ctx context.Context, backup *velerov1api.Backup, kbClient client.Client) ([]SnapshotIdentifier, error) { + podVolumeBackups := &velerov1api.PodVolumeBackupList{} + options := &client.ListOptions{ + LabelSelector: labels.Set(map[string]string{ + velerov1api.BackupNameLabel: label.GetValidName(backup.Name), + }).AsSelector(), + } - podVolumeBackups, err := podVolumeBackupLister.List(selector) + err := kbClient.List(ctx, podVolumeBackups, options) if err != nil { return nil, errors.WithStack(err) } var res []SnapshotIdentifier - for _, item := range podVolumeBackups { + for _, item := range podVolumeBackups.Items { if item.Status.SnapshotID == "" { continue } diff --git a/pkg/restic/common_test.go b/pkg/restic/common_test.go index 954defee0..7f3e0c503 100644 --- a/pkg/restic/common_test.go +++ b/pkg/restic/common_test.go @@ -17,6 +17,7 @@ limitations under the License. package restic import ( + "context" "os" "sort" "testing" @@ -28,8 +29,6 @@ import ( 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" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) @@ -369,10 +368,8 @@ func TestGetSnapshotsInBackup(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - client = fake.NewSimpleClientset() - sharedInformers = informers.NewSharedInformerFactory(client, 0) - pvbInformer = sharedInformers.Velero().V1().PodVolumeBackups() - veleroBackup = &velerov1api.Backup{} + clientBuilder = velerotest.NewFakeControllerRuntimeClientBuilder(t) + veleroBackup = &velerov1api.Backup{} ) veleroBackup.Name = "backup-1" @@ -380,12 +377,11 @@ func TestGetSnapshotsInBackup(t *testing.T) { if test.longBackupNameEnabled { veleroBackup.Name = "the-really-long-backup-name-that-is-much-more-than-63-characters" } + clientBuilder.WithLists(&velerov1api.PodVolumeBackupList{ + Items: test.podVolumeBackups, + }) - for _, pvb := range test.podVolumeBackups { - require.NoError(t, pvbInformer.Informer().GetStore().Add(pvb.DeepCopy())) - } - - res, err := GetSnapshotsInBackup(veleroBackup, pvbInformer.Lister()) + res, err := GetSnapshotsInBackup(context.TODO(), veleroBackup, clientBuilder.Build()) assert.NoError(t, err) // sort to ensure good compare of slices diff --git a/pkg/test/fake_controller_runtime_client.go b/pkg/test/fake_controller_runtime_client.go index a1f20e9bf..d1c1b6106 100644 --- a/pkg/test/fake_controller_runtime_client.go +++ b/pkg/test/fake_controller_runtime_client.go @@ -28,6 +28,15 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" ) +func NewFakeControllerRuntimeClientBuilder(t *testing.T) *k8sfake.ClientBuilder { + scheme := runtime.NewScheme() + err := velerov1api.AddToScheme(scheme) + require.NoError(t, err) + err = corev1api.AddToScheme(scheme) + require.NoError(t, err) + return k8sfake.NewClientBuilder().WithScheme(scheme) +} + func NewFakeControllerRuntimeClient(t *testing.T, initObjs ...runtime.Object) client.Client { scheme := runtime.NewScheme() err := velerov1api.AddToScheme(scheme)