From 086dbd344fbee7739822dd6e696d0b7aab305873 Mon Sep 17 00:00:00 2001 From: qiuming Date: Tue, 21 Mar 2023 14:39:25 +0800 Subject: [PATCH] Handle backup of volume by resource policies (#5901) * Handle backup of volume by resource policies Signed-off-by: Ming --- config/crd/v1/bases/velero.io_backups.yaml | 20 + config/crd/v1/bases/velero.io_schedules.yaml | 20 + config/crd/v1/crds/crds.go | 4 +- go.mod | 2 +- .../resourcepolicies/resource_policies.go | 143 ++++++ .../resource_policies_test.go | 244 +++++++++++ internal/resourcepolicies/volume_resources.go | 201 +++++++++ .../resourcepolicies/volume_resources_test.go | 412 ++++++++++++++++++ .../volume_resources_validator.go | 76 ++++ .../volume_resources_validator_test.go | 251 +++++++++++ pkg/apis/velero/v1/backup_types.go | 4 + pkg/apis/velero/v1/zz_generated.deepcopy.go | 5 + pkg/backup/backup.go | 2 + pkg/backup/backup_test.go | 3 +- pkg/backup/item_backupper.go | 50 ++- pkg/backup/request.go | 2 + pkg/builder/backup_builder.go | 12 + pkg/cmd/cli/backup/create.go | 6 + pkg/cmd/cli/schedule/create.go | 6 + pkg/controller/backup_controller.go | 16 + pkg/controller/schedule_controller.go | 4 +- pkg/podvolume/backupper.go | 31 +- 22 files changed, 1504 insertions(+), 10 deletions(-) create mode 100644 internal/resourcepolicies/resource_policies.go create mode 100644 internal/resourcepolicies/resource_policies_test.go create mode 100644 internal/resourcepolicies/volume_resources.go create mode 100644 internal/resourcepolicies/volume_resources_test.go create mode 100644 internal/resourcepolicies/volume_resources_validator.go create mode 100644 internal/resourcepolicies/volume_resources_validator_test.go diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 559d07e79..9aa001f4f 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -430,6 +430,26 @@ spec: "objectname". nullable: true type: object + resourcePolices: + description: ResourcePolicies specifies the referenced resource policies + that backup should follow + properties: + apiGroup: + description: APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in + the core API group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object snapshotVolumes: description: SnapshotVolumes specifies whether to take snapshots of any PV's referenced in the set of objects included in the Backup. diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 2583b6907..78a4f2f3d 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -466,6 +466,26 @@ spec: simply use "objectname". nullable: true type: object + resourcePolices: + description: ResourcePolicies specifies the referenced resource + policies that backup should follow + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object snapshotVolumes: description: SnapshotVolumes specifies whether to take snapshots of any PV's referenced in the set of objects included in the diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 3d8368c03..0011be428 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WAo\xdc6\x13\xbd\xebW\f\xf2\x1dr\xf9\xa4M\xd0C\v\xddR\xb7\x05\x82&\x86a\a\xbe\x14=P\xe4\xec.c\x8ad\xc9\xe1\xa6ۢ\xff\xbd\x18R\xf2j%\xd9\x1b\a\xa8n\"\x87of\xde\xcc\x1bQU]ו\xf0\xfa\x1eC\xd4ζ \xbc\xc6?\t-\xbf\xc5\xe6\xe1\x87\xd8h\xb79\xbc\xad\x1e\xb4U-\\\xa5H\xae\xbf\xc5\xe8R\x90\xf8\x13n\xb5դ\x9d\xadz$\xa1\x04\x89\xb6\x02\x10\xd6:\x12\xbc\x1c\xf9\x15@:K\xc1\x19\x83\xa1ޡm\x1eR\x87]\xd2Fa\xc8\xe0\xa3\xebÛ\xe6\xfb\xe6M\x05 \x03\xe6\xe3\x9ft\x8f\x91D\xef[\xb0ɘ\n\xc0\x8a\x1e[\xe8\x84|H>\xa0wQ\x93\v\x1acs@\x83\xc15\xdaUѣd\xb7\xbb\xe0\x92o\xe1\xb4QN\x0f!\x95t~\xcc@\xb7#\xd01o\x19\x1d\xe9\xd7\xd5\xed\x0f:R6\xf1&\x05a\xd6\x02\xc9\xdbQ\xdb]2\",\f\xd8A\x94\xcec\v\xd7\x1c\x8b\x17\x12U\x050P\x90c\xabA(\x95I\x15\xe6&hK\x18\xae\x9cI\xfdHf\r\x9f\xa3\xb37\x82\xf6-4#\xed͂\xb2l;\x12\xf6n\x87\xc3;\x1dٹ\x12\x84K0f\xae9\xc5\xfa\xe9\xe8\xf1\f\xe5D\x04L\xf6\nb\xa4\xa0\xed\xae:\x19\x1f\xde\x16*\xe4\x1e{\xd1\x0e\xb6Σ}w\xf3\xfe\xfe\xbb\xbb\xb3e\x00\x1f\x9c\xc7@z,Oy&}9Y\x05P\x18eОr\u05fcf\xc0b\x05\x8a\x1b\x12#\xd0\x1eGNQ\r1\x80\xdb\x02\xedu\x84\x80>`D[Z\xf4\f\x18\xd8HXp\xddg\x94\xd4\xc0\x1d\x06\x86\x81\xb8w\xc9(\xee\xe3\x03\x06\x82\x80\xd2\xed\xac\xfe\xeb\x11;\x02\xb9\xec\xd4\b¡GNO\xae\xa1\x15\x06\x0e\xc2$\xfc?\b\xab\xa0\x17G\b\xc8^ \xd9\t^6\x89\r|t\x01AۭkaO\xe4c\xbb\xd9\xec4\x8dz\x94\xae\xef\x93\xd5t\xdcdi\xe9.\x91\vq\xa3\xf0\x80f\x13\xf5\xae\x16A\xee5\xa1\xa4\x14p#\xbc\xaes\xe86k\xb2\xe9\xd5\xff\u00a0\xe0\xf8\xfa,\xd6E-˓\xc5\xf2L\x05X-\xa0#\x88\xe1h\xc9\xe2D4/1;\xb7?\xdf}\x82\xd1u.Ɯ\xfd\xcc\xfb\xe9`<\x95\x80\t\xd3v\x8b\xa1\x14q\x1b\\\x9f1\xd1*ﴥ\xfc\"\x8dF;\xa7?\xa6\xae\xd7\xc4u\xff#a$\xaeU\x03WyHA\x87\x90<\xabA5\xf0\xde\u0095\xe8\xd1\\\x89\x88\xffy\x01\x98\xe9X3\xb1_W\x82\xe9|\x9d\x1b\x17\xd6&\x1b\xe3\b|\xa2^\xf3\xb1v\xe7Qr\xf9\x98A>\xaa\xb7Zfm\xc0\xd6\x05\x10\v\xfb\xe6\fz]\xba\xfc\x94\xe1wG.\x88\x1d~p\x05sn\xb4\x1a\xdb\xec\xcc\x18\x1cO\x96\"c\\7\\`\x03\xd0^\xd0D\xbf$\xb4}\x1c\x03\xab\xf90\x92-S\bhi\x80\xc97\x88o\x1e\x99FD\x9a\x8c\v\xbe\xcd]\xe8\x80\x0f\xcb\x13c`\f\x06\xc4\v\xd3\xf9\xf2E̿\xba\xb9hk\x93e\xebB/\xa8\\\x17k\x06ZX\xf0\xb5\\t\x06[\xa0\x90\x96\xdb\xcf\xcdQ\x8cQ\xec.e\xf7\xb1X\x95\xcb\xc5p\x04D\xe7\x12=A=\xed\x97Q\xc0\x85r\\\x88\xd4\xefE\xbc\x14\xe7\r۬5\xc4\xec{\xf5\\\bO\xcd\xcck\xfc\xb2\xb2z\x8bB-u\\õ\xa3\xf5\xad'3\\U\xc5b1\xf2=LM\xea\x1c\x8b\x90\xa7+\xa9{\xbcW\xb6\xf0\xf7?\xd5IXBJ\xf4\x84\xeaz\xfe\a6\xcc\xf7\xf1\x87*\xbfJg\xcb\x0fPl\xe1\xb7߫\xe2\n\xd5\xfd\xf8\x93ċ\xff\x06\x00\x00\xff\xff\xc8p\x98۸\x0e\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms\xdc:rw\xfd\x8a.\xe5\xe0ݔf\xfc\\9$\xa5\x9b\x9flW\xa6ދ\xad\xb2\xb4\xdeC\x92\x03\x86\xec\x99\xc1\x13\bp\x01p\xe4\xd9T\xfe{\n_\xfc\x04Ip(Ǫ\x19\xf2\xbd\x1b\xe9\xd6\"\xe2\xf3\x01\xf5\x01%\x94\"\xa8\xe0\xc8,w\x94!\xa8\x93\xd2Xx\x8e\aŷ\xf5ԷB\xc1\x98\a\xa1`{\n8\x0f\xe7i\xec-\xd92\xbc\x05-\xab\xe1p\x8e\f[!\x18\x92\xbe\x12\xee\xd3\xe13*M\xb3\x19*\\\xf7\xc9\xe0zE\x88 \xfd\x0fvn\x11:l\x1bY\xd3\xe4\t\x81\x04j\x18\xe3\xc0X\x8b\x88\x1d\n\xc0\u007fqxg4Wf\xf4\xc9\x10[\xf0\x9a\x8b\"\xb3ڒ\v`\x82\xefQ\xbaьUx\xa6\x8c\x99\xe1%\x16\xe2\x889\x18\x85!\x91\x19\xcd\a\xbb\xca\xe8\x92!\x9d\x01\x8c,\x8f\xca\x00\xe5J#\xc9\xd7חd\x10~\xcdX\x95c~\xe7\\\x81\a\xe3\xc4\x04\x97n\xa0\x0ez|z?\xd5\xd7[\x11F3\xeb\u007fԮ\x86i\x14]\x97\xc1\x96\x9cJt~\x9a\xe1\x9aG\xaf1\x12^\u007f\xc1f\a\n\xb5ir\xfd\xcf\xd77\x86\x99\x11\xa0\x9dA\xbbC( \x12\xeb\xd9\x0fyA5\x16\x91\xf9O.\xfaDF\x10)\xc9i\x84\r\x8d\xf7\xb8\x94\t\x91\x9e=\x16\xf0\xd0\xe2\xf7eBo\xd8?\x16\x1b\x16S_\xd9\x00\x87PnHoB\x8a\x0e\xe5\x95u\xd3#\xd31T0^\f\xe5\x0e\x9e\xf5\xc0\x1bJ\u007f/tY*\x94c\xa2Xˀ\x971\x13\xbb\x90\xa8\xbd\xfe\x8e\x89r\x10\xe2i\x8e\x10\xffn\xda4^0d6f\x86-\x1eȑ\n\xe9\xa7\xde\xd8f\xfc\x8aY\xa51fˈ\x86\x9c\xeev(\r\x9c\xf2@\x14*\x17\b\x8d\x13dܱ\x83\xd6j\x8f\xfe؛G\xc3H#\xa9v\xe6c\xa8\x1b\xe3ܷ2\xe11\x88\x1a\xdf\xcbZ\xb3\x9c\x1ei^\x11f\r\x1bᙛ\x0f\xa9\xf1\x8a\x19\xc8\t&\x0fpvf3`n8\xd1q\x94\x05G\x10\x12\n\x13\x1d\f\x9b\xf6\xe3\x99\xe6\x19\x9b\xf6\x96\x18\xdb/\x9c\x88ʊ\xa1\xf2C9g\xab\xd1\x017\xa3\xa0k\x8e\xb8Ȓ\x91-2P\xc80\xd3B\xc6\xc91\xc7d\xf7\xa4\xe8\xb5\x11*F4\\ףo&6\x01\x12\xac\xbb\u007f\xa0\xd9\xc1\xb9NF\x82,\x1c\xc8\x05*\xbb\xcaIY\xb2\xd3\xd8$a\x8e\xf3~\x90\xa9\x85\xde<3K\xbe\x0f/\xb6\xf8\x9b'A76ό\x96\xecR\xb6\x16\a\xd0br\xda\xff\x98\x84\rj\xff\f\xa1\xdd\f\xba^Vh\rI)*\xeb\x01aQ\xea\xd3\rP\x1d\xde\xceA4\x81G3\xfe\x1f\x981\xcb%~\xd3\xefyQ\x89\x9f\xe4\xca\x1cDÕz\xf8? S\xac\xb1x\xf0\xb6\"\x99!\xbf\xb6{\xdd\x00\xdd\xd5\f\xc9o`G\x99F\xd9\xe3\xcc7\xad\x97K\x10#\xc5ޙ\xa7 :;\xbc\xffj#\x93\x13F\xb7 s\x86\\\xec\x06\x1e\xcb\r<\x1f\x84r6\xd5nT̂\xa4\n\xae\x9f\xf0t}3\xd0\x03\xd7\x1b~\xed\f\xfcbuS{\v\x82\xb3\x13\\۾\xd7\xdf\xe2\x04%JbR3[\x90\x91\xea*\x9bX2x\x02\xa6c]\x85`\xdc\xdc)\xac\x93\xe4\xb0\x14*\xb2\xb7:\x82ʽP\xdae\x16;n\xe9\x92,\x168\x19\xf2\xd9+ ;W\a\"d\xd8\xe17j\xaf\x97p5\\S\xd3\x1a\xd6e\xf3}F\xcc\x015\x81\xd5u\xb3\x82\x9d>\xbdv\x9b\tv\x10\x92Y\xe7b\x16n)E\x86JM\x8bH\x82\xb6\x9eI\x12\xd6\tB\xe2\x02\x18\xb7}>\x9d\x94\fO\xbaCj\x88\xb4Е\u007f\xff\xb5\x95\xbd4\x8b\xdf\xfc='|K\xf1\x02\xbbf\x8b\x82\xf4\xebD\x92P\xbcs=\xc32\xf1\x80\\h \xf7\x95]\xea\xe9\x1e\xa4\x17\xa4\xef\xc1L\x17\x94o\xec\x00\xf0\xe6\xe2f\xbdV\x92x\x8e\xe3~\x17\xfa6D\xaf_\xd8՛\xea\x11\t\x9b\xb9\x97\xd8\xe1\xdc0\xcfm\x1c\xc5D\x90\\\xe8v:\xc1\xc0-E\xfeJ\xc1\x8eJ\xa5ۈ\xa6\nE5\xb3\xfa\x9bgi\xe4\xc4\xdfKyV\xe0\xf4\xc9\xf5l%\xb2\x0e\xe29Tی\x16&\xc4\x1e\xbb)\x84@w@5 \xcfD\xc5m\xfa\xc5,u;\x84c\x81S\xd0\xc9$KS\x10\xe6A^\x15i\x04XY\xa9\xa3|2O\xd3n\xfe\x81\xd0\xd8\xd6\xf0\xf0Y\xc86=V\x94\x14{:l\v\xd5I\xed\xb2\xa9\x82|\xa5EU\x00)\f\xe9SÞ\x9d\xabi\xeap\xbc\xael\xb2p\xad\x19\xd1\xc2,\xaa\x92\xa1N]\x91\xae\x86\xc9,\x13Es\xac\r\xb3\x97\x02\xc1\x81\xc0\x8eP6RJ2|\x16\xd1vI\xac\xe1\x95\xc5傈\xb4\xc1W\x96\x14\t\x89\xd8DgqZ[\x972\xddU\xbc\x97\x98\xe6\x9e\xcd%\xa5\x83{VJjdI\\\xdaC\xf3\"F\xf8釋6x~\xb8h3\xcf\x0f\x17m\xf4\xf9\xe1\xa2\xcd??\\4\xff\xfcp\xd1\xc2\xf3\xc3E\xfb\xe1\xa2M5\x9b\xd2\xd6s\x18\xb9\xf3W#?\xceb\x91\xb0==\x85\xe2\x04|_MየS+,7\xf1^\x91\x1ay_\x04\xbd\xb2շ1\th\x8a.\x1aSR\x97\\\x9a\x05\x12\xc4\xdb\x1d'\x99)\xc2\xfc\x86Z\xf40\xe89\xb5蛩\xbe\x97\xa9E\xf7\xe8\xf5]\xee\xcbT\xa2\x87\xb9\xc7O\xa1D \xfa\x02\x17WnQ \t)v\xb7\x9f\x9e\x8f\x8c\xd8\x1bl\x00\xf7w.\xa1\x1d\x94x%W\x18\x0f*\xbc._\xf6>\xc2\xef\x01\u007f\"\x10\xa92\xc2\xf0\xfd\x91w1Ulj\xd9'SdR\xe1ਉ#\xdbEY\xdd\x02\xb8\xefS\"\x97\xcaaj\xa5\xfb\x14\xbd\x86\x8a\xa5E\xb0\xefu\x05k,>\x95\xde6\xa4\x9d\x11\xdcD\xba̝\x12\x8c\xcc\xc7\x06\xfd\xeaij\x83\x14\\T\xca\xe7\t\f\xf4\xb76]\xe1\xb70m\xf9V\xa2R}\x03\aQE*\xad'h7Sw7^m\xe7\xf7\x96Q\x93\xe3\x9bu\xf7\x17-|\xed\x1d\xe7\xdd1G\x9fJ\x97\x14Zl\xe9\xa7\x13\x1ai\xd5yg\xd7\xe4uk\xeeF\x9c\xbe\xa5\x9b\xd0\xe9G\x0fҫ\xee\xa6\xcb\xe4\x96\xd4\xda\xf5+\xe9F\x81\xceWإ\xe4\xa2f\xaa\xe9Ψ\xa1K\xac\x9f\xfe\xe6\xad\xf6\x94*\xb9\xb3j\xe3fK\x8c\x13+⺵n\xd3 \x17\xd4\xc1%\x11g\xbe\xe6mq\xa5\x9b\xaf,\x9b\x9cGr}[\xa4rm\x12\xf0hU\xdbT\xbd\xdaL\x9e{X˖^\xa56\t\xdaV\xb0\xcdצ]\xae\x02\xfd\x12Q\xf5\xb8\xaa\x99\xad/\x9b\x8d\xba\xa7\xf1\x9b\xad [R76K\xb13k\xc4\xea\x1a\xb0\x91q\x97V\x86u+\xbfF\x80\xa6ԃ\x8d\xd4{\x8d@\x9c\xac\x02K\xad\xf2\x1a\x81=cv'\xa5d\xe2\xc7\xf8E\x1b0k\xdf\xd8\xef%Q\xe7NLȎ\xbb8\x17\xa2|\xea57\xbc\f^Ӵ\xfb\x19\xf3<\xa9>,w?\x8b\x8aiZ2\xbbAx\xa4y4\r\xa5\x0fx\xaa\xafM\xf8M\u0603\x93ۓ\x85\xf4\xe9s-\x9e\xeb\x9e\x13M\x14<#c@b\xc25\x98y\xe6\xee\x8a\xc9\xc4\n\x8d\xce7\v\xce_\bᯔ\xb9q\x12lφ\xc6\xf6P\xf4\x01\v\x03%\xdc,\xb1 \xfe\x9av\x10\x9d/k\xdf\xfd\xadBy\x02qD\xd9x\f3'\x93\xdcBS&\xba\tK\xdf\xeb\x0fwCQ\xcfqn\x16\x1c\xbc\xe5΄E\xc1\xf6p\xb4p̚g5\xaf\x8dz3q\xc0H\xd3x*UԽ#\xbf\xcf\xf9\x9e\xa9\xc7z^6tX\x1e<̚\xed\x17\t \xce\x0f!&@\xa6\x1e\xd3I\xdbҞ=\x96\xf3R\xa1\xc4\\0\x91\xecE\xa5\x1d\xbby\x89\xe36\v\x8e\xd9,\b*\x96\x85\x15\xc9dJ9N\xf3\"\xc1\xc5\v\x86\x17/\x11`\x9c\x17b̀\xec\x1d\x93I9\x00\x93T\xae\x91\xbcc\x99Rn1\xbf\xa98}\xb0%\xe1@K\xc2v\xe3\x1c\xa6\t\aW\x96\x1dXI\xa0\xe1\v\x05\x1f/\x14~\xbcD\x00\xf2\xb2!\xc8l\x102+9\x93?\x9f\x9d^\x172G9\xb9\x1b\x91*j\x93B\u058b\x17\xbac\xf6r\xf3\xe1\x065Ӫ\xe3\x9a\xc6R\xca\xf5y\xf2\f~\xa1\xdc\xefn\x1a\xa1j\xd9\xf1\xce\x16I\xe3X\xc43\xf4\x8d\xd7\xe6o\x98t\xfb*\nK\"\xed\xbe\xe7\xf6\xe4\x8a\x1d\xd4\x1aޓ\xecЃ~\x88\xc6\t;!\v\xa2\xe1\xbaޔz퀛\xbf\xaf\xd7\x00\x1fD\xbd\x8d\u07be\xa3EѢd'\x13\aD`^\xb7A\x9c'\x10QaR\xfe:D\u007f?\xdcL\xec\xf7\xd0m\x1d)\x0f\bW\xe3\x05\xb8*\x1e\xf8\x10~\x82\xfb/\xd6;\xb1W\x10e\xcduL\xde\xff\b\xd1_\xff\xb6\xa6\x9f/_(\xa0\xb4\x90d\x8f\xbf\nw\xc3\xe5\x1c\r\xba\xad;כz\xad\x11\nw¹\xae\x985\xf5wm\xf6\x805\xf5x\x83{\x16\r\x961u2\xb1\x12\xb5f3\x93y|\xfc\xd5M@\xd3\x02\xd7\xef*\xb7\xc1\xb5*\x89Th\xa8\x19&\xe6:m\xcd\u007f\x0f\xe29\x96\xe5\x10~\xce?\xf7\xf1\x96hk\xfel\xed\xc7\"쏝\xfb:\x03\x89\xe6D\xf4K\xbcW+Dk1ɭ\xf6\xa8\x84\x8e\xc1i]Yl\x93\x17\xf6\xcc\xdee\xaf\x10\x1b\xd3\xdfc\x97\xbaڋL\xe7\xafuu\xf7\x9d\xfaK\x9c}\xe5h%\xed\xfd_\xfe.T{_\xd6Y7\xbbn\xeb\xcd\xd2z+V\xbd\xd5\xda\xf8\x9a1\xff+\x82\xdeHߠյЄ\x01\xaf\x8a\xad5\x131\x95Rw\xb1۸\x93\xfb\xb7\xaeNo\x82q\x8eԔk\xdc\x0f\xf2V\xb1\xb9\xde\xf9B\xbfs\xe6Z\xf7M\x9f\xab\xaa\xb2\f\x95\xdaU\x8c\x9d\xea\"\xc3%\x13\x8f\xc0\xbc\x14)>\x10\xca\u03a2\x83\xeb8B\x047\xb7Q=\x9a\xc4f_݄<\x0f\x8bw`\n\xcccKo\x97\xd1\xc1\xb3\xa0s\xaf\xfc4\x01\xee\x86=\xec\xed\xe12o\x95,\xd4\xf7\xcb>\x13հ9\xe6Q6\xe0\\O\x1b\xd2\x19h\x98\x03\x1e\x91\x83\xe0\xb6t\xd4^,\xe7n\xb8\xef\xf7\x89@mC\xf1\xb5\xa9U\xc9\x04Ƀ\x81\vΛ\xbf\x15\xfdњoyD\xf9JM\xc0\xaco\f\x8e\x10a(\x99Λ\xba\x85\x9ch\\E\x81&\x99\xfe\xa8\xae\xcd\x14\xed\xea\xf9d\xa5u\xf7\xb0\x19\xeb9*\xc1\xa1A\x8c\u007f\x83\xfb\xa9\xbfQI\rg\x96\xaa\xa2\x863\x9bSP\x1du\x14\x99\\\xa3\xa0.>M\xbbVg\xaf\x0f\xb5\x8d\x9c\a`k\xec\xc3}ͮȾ@\xa5\xc8>\xdc\x1b\xfal\x1c\xb0=r\xb4\xfe\u007f,\xf1\xee2$MQv\xf7\xd6L\x97\xca%\x99\xae\x88\x1f \x94\xc0\xb4Z\xbd\x8a)`&\xf6\xee\xa6m\x1a\xbe3\x10<Ӆ4\xf9ZR\x99\xe2ɾ\xaf\x1b\x1a\xda\xd8]\x18ˈ\xe6\xbb\x10\xc8\xe8\x9e\x1a7\xd00iO\xe4\x96\xecq\x95\t\xc6\xd0\xea\xda!^/\xb9X}\xe9\xfbg$jvj\x1f\xdam}\xca\xcfq\xdb]:E\\5\x98\xfdL\x80\xa6\x12\x9b\xefn\f\x10\x12v\xe0E\x9e\xab\xa3B\xf4\v\x15CL\xdbm\xc3\x02\xf3z\xd5\a\x92\xfe\x83\x157>\x16\x8a\xa7u\n\xf2\x9b\x907PPn\xfe1a\xaf\xcdɅ\u038b\xf0\xb7\xd7\xc1\xce\xe0}o\xda\xd4'\x90Z~$\x86\x051\x16\xa9\xc5O\x9d\xac\xe0#\x0e\x03\vw\x90\x04s\x9b\x85\x8e}\x96\xc34\xd9\xf0{)\xf6\x12\xd5pU\xad\u0bc4j\xca\xf7\x1f\x84\xbcg՞\xf2\xc6\xdfX\xd4\xf8\x9eHM\tc'\x87O\fQ\xca\t\xa3\u007f\x8fq\xa7\xfd\xe3<\xa0Z\xddF~K@c\xec\x87whL\xedh\xb8\x11\x17\x04O\xd79Y\xf0͚\xac\x19\xe5Nv\xed\xa1\x95\xad\xa8tG\xf95\xca3\"\xc8a\xcc5|\x14\x1a\xc3f\f\xed\xc24\xe6\x02\x95^\xe1n'\xa4vI\xba\xd5\n\xe8·/\xb1\xac\f\xa1\xccn&\xbb\xefz\x00\xd5M=M\xb3\xdelfBZ\xb5a\xaf{,\xc8\xc9\x1d` Yf\xa2c|\xad4a\x11\x8d\xfcM\xe5\x8b6N4\xeb\x05\xf3\xbfD<\xc7\x01\xc17\xed\xf6\xf5-2\xb5=\xb6\xe0\x1c\xe5\xec\x013g\x8d\xa2\xb6\x19\xec\xb1#\xe4\xf0,\xa9\xd6\xc6\x02\xb4w\xdbA\x1b\x9d\xcf\x18(\xa3\x05G\xee\f\x9e\xb2E\xf6w\xe3-lƳ\xff݄D\xddx\xcc\xd9\xf0\x93\x13\x86-[K\x82\x91i\xb9\xeaz\xaaB_\xc3\xca\xec@\xf8\xde\b\x95\x14\xd5\xfe\x10\xe4rĖ\x8f%\xcf+\x83\x14\x94VC\xa8\xb0ө+\xc9[\xd9\xd3\xfa\xe0D\x83.ɞF1\xf5\xbb9\xe1\xdbR\xaf\xfd-\xc0\xab\x9d\x14\xc5\xca\xf3\xc2nP\xde\xf8\x8c\xa6\xa4\xc2\x04\xec\xfa\x10%9\xb8\x8fv\xf8\xeb6\xad\x18\x94%r \xca\xe3\x93p\xbaz\x9a\xadS)EM\xa4N\x8d\x83\x1e:\x8dgB \v9\x8e\xef\x83\xcfغS\xe6w\xfd\xaf|݀\xa2<|\xd6\xca僝((\x13\x19I\xb4ɵ\xe8\xde\xf3 \xa6\xe9D0]\xf4\u007f\xdf\xe0\xe5X\xdb\xc4\xf7)^\xf0\x97^\xf3\xde\xc9\x02\xfb\xf5\x96\xba\x89\xf7\\#\xf4\xf8\x13ݹ\xed\xf0\xcc`\xfd\xe7\xff\xf7\x13\x03\xc7$/\xebդ\x83e}\xa7\xdaS\x9a\xf9V\xcb=C\xe3\xf9(Į\xef\xf6j\x91\x93~\xfei\xfd\xcf\xeb?]\x01d\x12m\xf7'Z\xa0Ҥ(\xef\x80W\x8c]\x01pR\xe0\x1dlI\xf6\\\x95j}D\x86R\xac\xa9\xb8R%ff\xac\xbd\x14Uy\a\xcd\x0f\xae\x8b\xc7\xc3\xcd\xe1϶\xb7}\xc1\xa8\xd2?\xb5^\xfeL\x95\xb6?\x94\xac\x92\x84\xd5#\xd9w\x8a\xf2}ň\fo\xaf\x00T&J\xbc\x83Of\x88\x92d\x98_\x01\xf8\xe9\xd8!W\x1e\xe1\xe3[\a!;`A\x1c.\x00\xa2D\xfe\xeea\xf3\xe5\x9f\x1e;\xaf\x01rT\x99\xa4\xa5\xb6Dq\x88\x01U@\xe0\x8b\x9d\x16HO~\xd0\a\xa2Ab)Q!\xd7\n\xf4\x01!#\xa5\xae$\x82\xd8\xc1O\xd5\x16%G\x8d\xaa\x06\r\x90\xb1Ji\x94\xa04\xd1\bD\x03\x81RP\xae\x81rд@\xf8û\x87\r\x88\xed\xaf\x98i\x05\x84\xe7@\x94\x12\x19%\x1as8\nV\x15\xe8\xfa\xfeq]C-\xa5(Qj\x1a\xe8잖T\xb5\xde\xf6\xa6\xf7\xc6P\xc0\xb5\x82܈\x13\xbaix*b\xee\x89f\xe6\xa3\x0fT5ӵ\x12\xd2\x01\f\xa6\x11\xe1\x1e\xf95<\xa24`@\x1dD\xc5r#\x85G\x94\x86`\x99\xd8s\xfa\xf7\x1a\xb6\x02-젌h\xf4\x02\xd0<\x94k\x94\x9c08\x12V\xe1\x8d%IAN ь\x02\x15o\xc1\xb3M\xd4\x1a\xfe]H\x04\xcaw\xe2\x0e\x0eZ\x97\xea\xee\xf6vOuXM\x99(\x8a\x8aS}\xba\xb5\v\x83n+-\xa4\xba\xcd\xf1\x88\xecV\xd1\xfd\x8a\xc8\xec@5f\x86\x91\xb7\xa4\xa4+\x8b:\xb7+j]\xe4\xff\x10\x04@\xbd\xe9\xe0\xaaOF\x18\x95\x96\x94\xef[?X\xa9\x9f\xe0\x80Y\x00N\xbe\\W7\x8b\x86\xd0敡\xce\xe7\x0f\x8fOm٣\xaaO}K\xf7\x96@6,0\x04\xa3|\x87\xd21q'Eaa\"ϝ\xf4Y\xd1e\x14y\x9f\xfc\xaa\xda\x16T\x1b\xbe\xff\xadBe\x84\\\xac\xe1ު\x18\xd8\"Ten$s\r\x1b\x0e\xf7\xa4@vO\x14\xbe:\x03\f\xa5\xd5\xca\x106\x8d\x05m\xed\xd8o\xec\xa8\xd6\xfa!\xe8\xb2\x11~9\x85\xf0Xb\xd6Y0\xa6\x17\xdd\xd1\xcc.\v\xd8\t\xd9\xe8\v\xa7\xae\xd6\x1d\x90\xf1%k\x9eL\xd1GNJu\x10\xda\xe8_Q\xe9~\x8b\x1eB\xf7\x8f\x9b^\x87\x80\x8cGͪ\x95Jan\xd6\xd9\v\xa1ڠ7\x80\t\x06\x10|\xb1\x1a&\xc0\xb3\x9a\xa6R\xa0+\xc9\xed*\xfd\x8c$?=\x89\xbf(\x84\xbc\xb2\xc2\x1al\xc5\rlq'$F\xe0J4\xfdMc\x94\xd2\x10FY\x94D\xa5\xd7\xf0t@CFR1\xed\xe5\x9e*x\xfb'((\xaf4\xae\a\xd0F\x18\xec\x88b\xc1\xb8\x19\xa8'\xf1Q9V͐\xef\xfdH\xb7\x16\x11_\x0e\xa8\x0f(\xa1\x14A\x05Gf\xb9\xa3\fA\x9d\x94\xc6\xc2s<(\xbe\xad\xa7\xbe\x15\n\xc6<\b\x05\xdbS\xc0y8Ocoɖ\xe1\x1dhY\r\x87sd\xd8\n\xc1\x90\xf4\x95p\x9f\x0e\x9fQi\x9a\xcdP\xe1\xbaO\x06\xd7+B\x04\xe9\u007f\xb0s\x8b\xd0a\xdbȚ&\xcf\b$P\xc3\x18\a\xc6ZD\xecP\x00\xfe\x93\xc3{\xa3\xb92\xa3O\x86\u0602\xd7\\\x14\x99Ֆ\\\x00\x13|\x8fҍf\xac\xc2\ve\xcc\f/\xb1\x10G\xcc\xc1(\f\x89\xcch>\xd8UF\x97\f\xe9\f`dyT\x06(W\x1aI\xbe\xbe\xbe$\x83\xf0kƪ\x1c\xf3{\xe7\n<\x1a'&\xb8t\x03u\xd0\xe3Ӈ\xa9\xbeފ0\x9aY\xff\xa3v5L\xa3\xe8\xba\f\xb6\xe4T\xa2\xf3\xd3\f\xd7\xdd\x00\xd5\xe1\xed\x1cD\x13x4\xe3\xff\x8e\x19\xb3\\\xe27\xfd\x9e\x17\x95\xf8I\xae\xccA4\\\xa9\x87\xff\x1d2\xc5\x1a\x8bGo+\x92\x19\xf2s\xbb\xd7\r\xd0]͐\xfc\x06v\x94i\x94=\xce|\xd3z\xb9\x041R\xec\x9dy\n\xa2\xb3Ç\xaf\xc6\xf3R\xcd\xf6C\"]\xfa\x9d\x9d\xff\x1a\xfc\xf9\xaea\x9e\x81\v6]I%\x16.\r\xfad\xa9ټ\xb1\x1eջO\xefc\xe1O\xf7I\x90\xbc\xc1D\xde\xf5\x90m\x0f\xed\x9d\xf2\xd4ixק\x8eo\\\x82\xfb\x06\b<\xe3\xc9y,\x84\x83a\x0e1\x03\x8dD:C\xe2\xd8L\xbb\x15\xb2gu>\xdb;U\x14\xdc\U000cc9d4f=\x02\x1a\x9c\xa8\xf2[\x02\x86\x92\xe6\x85%\x84ʹ\xa6\x13\x0f\xec6H\xd0E\xf3\x93\x83tE\x12\x9e@\xfb3\xa6Y\xb3\xad\xb5\x85d\x19\xfbF9\x16\x99Up\xa0e\xe2D\x8d\x99\xb3\xb9\x01\xb1\xab7B\xbe\x10F\xf3z '\xf7\x1b>\xee\rw\x9fOBo\xf8\r|\xf8J\x95ߋz/P}\x12ھy\x15r:\xc4\xcf \xa6\xebh\x97\x17wj\xdbС\xbd\xa3\x92 \xdc\xeeٸ\b\xaff\x0fU\xb0\xe1&n\xf1\xf4\xb0\xfbcn\xb8i\xfb\xd0}\x8aJ\xd9-\x13.\xf8ʚ\xcaul$G\xecD\x90Bv82D\xad\x1e\xd4\r\x98\b\xf6\xc9X\x12\xd7\xdf\xed\xf81\x92a\x1e2\xfev\x9f\x8ah\xdc\xd3\f\n\x94\xfb)\xc3\xd1~J\xa3\xdf\xd3PHԺ\xeeY(ai\xa6=<^uG\x13\xd2\xddgeVnB\xab\xc0\xec٦#\xdbS\xe3M\xe7gdM\xac\xf5?f\xa9K\xf2\xdcV\x16\x10\xf6\xb0@\xe3/\xe0\xc5\xd0\xf6;Ĝ\x85,\x88\xdd0\xf8oc\xe6\xac@\xff\x0f\x94\x84ʄ5\xfc\xce\x16\n0\xec\xf4\xf5Y\xac\xf60f\x04\xaa\xc0\xf0\xf7H\xd8p\xe3329at\v2g\xc8\xc5n\xe0\xb1\xdc\xc0\xcbA(gS\xedF\xc5,H\xaa\xe0\xfa\x19O\xd77\x03=p\xbd\xe1\xd7\xce\xc0/V7\xb5\xb7 8;\xc1\xb5\xed{\xfd-NP\xa2$&5\xb3\x05\x19\xa9\xae\xb2\x89%\x83'`:\xd6U\b\xc6͝\xc2:I\x0eK\xa1\"{\xab#\xa8<\b\xa5]f\xb1\xe3\x96.\xc9b\x81\x93!\x9f\xbd\x02\xb2su B\x86\x1d~\xa3\xf6z\tW\xc355\xada]6\xdfg\xc4\x1cP\x13X]7+\xd8\xe9\xd3k\xb7\x99`\a!\x99u.f\xe1\x96Rd\xa8Դ\x88$h\xeb\x99$a\x9d $.\x80q\xdb\xe7\xd3I\xc9\xf0\xa4;\xa4\x86H\v]\xf9\x0f_[\xd9K\xb3\xf8\xcd\xdfs·\x14/\xb0k\xb6(H\xbfN$\t\xc5{\xd73,\x13\x0fȅ\x06r_٥\x9e\xeeAzA\xfa\x1e\xcctA\xf9\xc6\x0e\x00o/n\xd6k%\x89\xe78\xee\xf7\xa1oC\xf4\xfa\x85]\xbd\xa9\x1e\x91\xb0\x99{\x89\x1d\xce\r\xf3\xdc\xc6QL\x04Ʌn\xa7\x13\f\xdcR\xe4o\x14\xec\xa8T\xba\x8dh\xaaPT3\xab\xbfy\x96FN\xfc\x83\x94g\x05N\xbf\xb8\x9e\xadD\xd6A\xbc\x84j\x9b\xd1\u0084\xd8c7\x85\x10\xe8\x0e\xa8\x06䙨\xb8M\xbf\x98\xa5n\x87p,p\n:\x99di\n\xc2<ȫ\"\x8d\x00++u\x94O\xe6i\xda\xcd?\x12\x1a\xdb\x1a\x1e>\v٦NJ\x92bO\x87m\xa1:\xa9]6U\x90\xaf\xb4\xa8\n \x85!}jسs5M\x1d\x8eוM\x16\xae5#Z\x98EU2ԩ+\xd2\xd50\x99e\xa2h\x8e\xb5a\xf6R 8\x10\xd8\x11\xcaFJI\x86\xcf\"\xda.\x895\xbc\xb2\xb8\\\x10\x916\xf8ʒ\"!\x11\x9b\xe8,Nk\xebR\xa6\xbb\x8a\x0f\x12\xd3ܳ\xb9\xa4tp\xcfJI\x8d,\x89K{h^\xc4\b?\xfdp\xd1\x06\xcf\x0f\x17m\xe6\xf9ᢍ>?\\\xb4\xf9燋\xe6\x9f\x1f.Zx~\xb8h?\\\xb4\xa9fS\xdaz\x0e#w\xfej\xe4\xc7Y,\x12\xb6\xa7\xa7P\x9c\x80\xef\xab)|\x11uj\x85\xe5&\xde+R#\uf2e0W\xb6\xfa6&\x01M\xd1EcJ\xea\x92K\xb3@\x82x\xbb\xe3$3E\x98\xdfP\x8b\x1e\x06=\xa7\x16}3\xd5\xf72\xb5\xe8\x1e\xbd\xbe\xcb}\x99J\xf40\xf7\xf8)\x94\bD_\xe0\xe2\xca-\n$!\xc5\xee\xf6\xd3\xf3\x91\x11{\x83\r\xe0\xfe\xc6%\xb4\x83\x12\xaf\xe4\n\xe3A\x85\xd7\xe5\xcb\xdeG\xf8=\xe0O\x04\"UF\x18\xbe?\xf2.\xa6\xea81\xfbd\x8aL*\x1c\x1c5qd\xbb(\xab[\x00\xf7}J\xe4R9L\xadt\x9f\xa2\xd7P\xb1\xb4\b\xf6\xbd\xae`\x8d\xc5/\xa5\xb7\rig\x047\x91.s\xa7\x04#\xf3\xb1A\xbf:\xf1\xec \x05\x17\x95\xf2y\x02\x03\xfd\x9dMW\xf8-L[\xbe\x95\xa8T\xdf\xc2AT\x91J\xeb\t\xda\xcd\xd4ݍW\xdb\xf9\xbde\xd4\xe4\xf8v\xdd\xfdE\v_{\a/T\x1f\"x\xbe\x1c\x90\xdb]q\xbeo\x17҇\x05\xe7\x8f0\xf7\x05\t\x84\x04N٘\x8d\xaa\xcfyw\xcc\xd1/\xa5K\n-\xb6\xf4\xd3\t\x8d\xb4꼳k\xf2\xba5w#N\xdf\xd2M\xe8\xf4\xa3\a\xe9Uw\xd3erKj\xed\xfa\x95t\xa3@\xe7+\xecRrQ3\xd5tg\xd4\xd0%\xd6O\u007f\xf3V{J\x95\xdcY\xb5q\xb3%Ɖ\x15q\xddZ\xb7i\x90\v\xea\xe0\x92\x883_\xf3\xb6\xb8\xd2\xcdW\x96M\xce#\xb9\xbe-R\xb96\tx\xb4\xaam\xaa^m&\xcf=\xaceK\xafR\x9b\x04m+\xd8\xe6k\xd3.W\x81~\x89\xa8z\\\xd5\xcc֗\xcdF\xdd\xd3\xf8\xcdV\x90-\xa9\x1b\x9b\xa5ؙ5bu\r\xd8ȸK+ú\x95_#@S\xea\xc1F\xea\xbdF NV\x81\xa5Vy\x8d\xc0\x9e1\xbb\x93R2\xf1c\xfc\xa2\r\x98\xb5o췒\xa8s'&d\xc7]\x9c\vQ~\xe957\xbc\f^Ӵ\xfb\x19\xf3<\xa9>,w?\x8b\x8aiZ2\xbbAx\xa4y4\r\xa5\x0fx\xaa\xafM\xf8U\u0603\x93ۓ\x85\xf4\xcb\xe7Z<\xd7='\x9a(xAƀĄk0\xf3\xcc\xdd\x15\x93\x89\x15\x1a\x9do\x16\x9c\xbf\x10\xc2_)s\xe3$؞\r\x8d\xed\xa1\xe8\x03\x16\x06J\xb8YbA\xfc5\xed :_־\xfb[\x85\xf2\x04∲\xf1\x18fN&\xb9\x85\xa6Lt\x13\x96\xbe\xd7\x1f\ue1a2\x9e\xe3\xdc,8xǝ\t\x8b\x82\xed\xe1h\xe1\x985\xcfj^\x1b\xf5f‑\xa6\xf1T\xaa\xa8{G~\x9f\xf3=S\x8f\xf5\xbcn\xe8\xb0^)\xfcx\x8d\x00\xe4uC\x90\xd9 dVr&\u007f>;\xbd.d\x8err7\"U\xd4&\x85\xac\x17/t\xc7\xec\xe5\xe6\xc3\rj\xa6U\xc75\x8d\xa5\x94\xeb\xf3\xe4\x19\xfcD\xb9\xdf\xdd4Bղ\xe3\x9d-\x92Ʊ\x88g\xe8\x1b\xaf\xcd\xdf0\xe9\xf6U\x14\x96D\xda}\xcf\xed\xc9\x15;\xa85| ١\a\xfd\x10\x8d\x13vB\x16D\xc3u\xbd)u뀛\xbf\xaf\xd7\x00\x1fE\xbd\x8d\u07be\xa3EѢd'\x13\aD`^\xb7A\x9c'\x10Qa\n\xe3?\bF緧>\xb7[\xd3\xc8}\v\xf6>!cGj:\x95\xbei4\xfa!\xbaw\x9f\xe0N0&^\x16Fؤ\xa4\xffj/\xe7\x9dO\xb4\xbc{\xd8ئAX쥾u\xd9N\x8d\xf4\x16\x8d\x11l\xa63\xb6\x887\xbb\x0e\xc4H\xf9[\xfd\xa7\x15\xd8\xda\bӱ+\x8d\\)\x9eQ\x1e\x0f\x1b\x87\xdd\xda\xca\v\xe1'\x10\xb6\x00C\x1f\xa8\xccW%\x91\xfa\xe46\xfcoj\x1cƓ-\xc1\x14N\xa5DF-\xc6\xf0\x96\xd7(m\xc3e\xafv\xc7\xedTv\xf7+\xfb\x14=\a\x8f\xf1\xf3u\xb3'\xeb.\x88ǸW\xb1\xb2\x94\x8a\xbc\x8eV\nM,J\xe5\xef(\xf5\x976\xce,\xca\xc7n\xebH\xcdN\xb8\xaf2\xc0U\xf1l\x84\x91\xb1\x87/6d\xa8ױ7\xe2>(\b)\x99\xfe\x15j\u007f\xbe|\xf5\x8e\xd2B\x92=\xfe,ܵ\xb3s4\xe8\xb6\xee\xdc9\xecMy\xa8\xa6\v\"\x11sq\xfd\x05\xb8=`M\x91\xec\xe0\xf2S\x83elUMH\x90\xd6lf2OO?\xbb\thZ\xe0\xfa}\xe5v\x9d͒Wh\xa8\x19&\xe6:m\xcd\u007f\x0f\x11\xa5\t\xf6\x16\xd1\x16\u007fZxK\xb4\x85\xb8\xb6 k\x11\xf6\xc7\xce%\xba\x81Ds\"\xfa%ޫ\x957i1ə\u0a04\x8e\xc1i\xdd#n3\x8a\xf6 \xede\xef\xf5\x1bs\xaa\xc6nZ\xb6\xb7\v\xcfߵ\xec.!\xf67\xab\xfbr\xeeJ\xdaK\xf9\xfc\x05\xc5\xf6\x12\xbb\xb3\xae[\xde\xd6\x15\fu}\x84z\xa7\xb5\t\x00c\xea+\x82\xdeH\xdfZ\xcb\vM\x18\xf0\xaa\xd8Z\xdf-\xa6R\xea.\xb6\xb6b\xb2\xa8\xc2Y\xe1\t\xc69RS\xaeq?H&\xc7\xe6z\xef\xaboϙk\xdd7}\xae\xaa\xca2TjW1v\xaa+\u007f\x97L<\x02\xf3R\xa4\xf8H(;\x8b\x0e\xae\xe3\b\x11\xdc\xdcF\xf5h\x12\x9b}\xc9!\xf2<,ށ)0\x8f\xad\x87_F\aς\xce\xc7\x1e\xa6\tp?\xeca\xaf\xf4\x97y\xab\x8e\xa8\xbe\xf4\xf9\x85\xa8\x86\xcd1Ǫ\x01\xe7zZ?\xcc@\xc3\x1c\xf0\x88\x1c\x04\xb7\xf5\xdc\xf6\xb6G\xf7ى~\x9f\b\xd46\x14_0^\x95L\x90<\x18\xb8\x10Q\xf9O\x15\x16\xd8\xf2#1,\x88\xb1H-~\x14l\x05\x9fp\x18X\xb8\xd3]\x98ۭ\xa1طrL\x93\r\u007f\x90b/Q\rW\xd5\n\xfeJ\xa8\xa6|\xffQ\xc8\aV\xed)o\xfc\x8dE\x8d\x1f\x88Ԕ0vr\xf8\xc4\x10\xa5\x9c0\xfa\xf7\x18w\xda?\xce\x03\xaa\xd5m\xe4\xb7\x044\xc6~x\x8f\xc6Ԏ\x86\x1bqA\xf0t\x9d\x93\x05߬IeS\xeedמ$ۊJw\x94_\xa3<#\x82\x1c\xc6\\\xc3'\xa11\xec\x90\xd2.Lc.P\xe9\x15\xeevBj\x979_\xad\x80\xee|\xf8\x12K\x95\x12\xcal\x85\x87\xfb\xd8\x0eP\xdd\x14\xb95\xeb\xcdf&\xa4U\x1b\xf6\x0eւ\x9c\\\x12\x8dd\x99\x89\x8e\xf1Vi\xc2\"\x1a\xf9\x9bj\x8am\x9ch\xd6\v\xe6\u007fI\xc9-n\xda\xed\xeb\x04Tm\x8f-8G9{\xea\xd3Y\xa3\xa8m\x06{\x16\x109\xbcH\xaa\xb5\xb1\x00\xed\x12\x18\xd0F\xe73\x06\xcah\xc1\x91\x8b\xbc\xa7l\x91\xfd\xddx\v\x9b\xf1-\xb9nB\xa2n<\xe6l\xf8\xc9\tÖ\xad%\xc1ȴܑ\x17\xaaB_\xc3\xca\xec@\xf8\xde\b\x95\x14\xd5\xfe\x10\xe4rĖ\x8f\xc0\xcd+\x83\x14\x94VC\xa8P~\xa0+\xc9[[\x1a\xf5i\xa6\x06]\x92=\x8fb\xea\xb7X\xc3\a\xdfn\xfd\xd5ܫ\x9d\x14\xc5\xca\xf3\xc2V\r\xdc\xf8m\x06I\x85\t\xd8\xf5!Jrp_\xd2\xf1w\xe0Z1(K\xe4@\x94\xc7'\xe1ʃi\xb6N\xa5\x145\x91:5\x0ez\xec4\x9e\t\x81,\xe48\xbe\x8f~\x1b\xc5]\xfdp\xdf\xff\xf4\xde\r(\xca÷\xe6\xdc&\x8d\x13\x05e\"#\x896\xb9\x16-\b\x19\xc44\x9d\b\xa6\x8b\xfeo\x1b\xbc\x1ck\x9b\xf8!\xc5\v\xfe\xd2k\xde;\xeec?\xa9T7\xf1\x9ek\x84\x1e\u007f\xa0;W\xa3\x92\x19\xac\xff\xf8\u007f~\x8c\xe7\x98\xe4e\xbd\x99t\xb0\xac\xefT{J3\x1fPz`h<\x1f\x85\xd8\xf5\xdd\xde,rҏ煝\x97\x8c9\xc3g\x10/\x13\x89\x1dϋ6_-Լ\xec\xec^\x88\xfd\xf4\xda\xdc\x1a\xfb\xabo\x16\x895=\x84H\xb4\x19\x99F\x1d\u007f\xceF\x9b\xad`3\xe08\xf2=\x9a^\x00z\xa1p3j\a\x06/\xad\x02\xcd[kۏ\xe4\xdf4\x19l\x92eh\xc4\xf5S\xffs\xa7\xd7\xee\xdbaዦ\xf6\xcfLpgn\xd5\x1d\xfc\xc7\u007f]\x81\xdf\"\xf9\x12>]j^\xfeo\x00\x00\x00\xff\xff\x16\xad\x10\xed\x1av\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\xe9\xa5x\x0e\xff\xfeϤMה\x13\x1b\x8f\xe2\x87\xfeǵw)d\x94/d\xf1'\xa7:&}\x1d\x84\xbf\xfdc\x92\xaeB\xf1\\>i\xd1\xe4\u007f\x03\x00\x00\xff\xff\x1d\r\x93\v\x97\x1c\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96Ms\xe36\x0f\xc7\xef\xfa\x14\x98}\x0e{y$\xefN\x0f\xed\xe8\xd6\xcd\xee!\xd36\xe3I2\xb9tz\xa0I\xd8\xe2F\"Y\x00t\xeav\xfa\xdd;$%\xbf\xc8v6=\x947\x91 \xf0\xe7\x0f\x04Ī\xae\xebJ\x05\xfb\x84\xc4ֻ\x16T\xb0\xf8\x87\xa0K_\xdc<\xff\xc0\x8d\xf5\x8b\xed\xc7\xea\xd9:\xd3\xc2Md\xf1\xc3=\xb2\x8f\xa4\xf13\xae\xad\xb3b\xbd\xab\x06\x14e\x94\xa8\xb6\x02P\xceyQi\x9a\xd3'\x80\xf6N\xc8\xf7=R\xbdA\xd7<\xc7\x15\xae\xa2\xed\rRv>\x85\xde~h\xbeo>T\x00\x9a0o\u007f\xb4\x03\xb2\xa8!\xb4\xe0b\xdfW\x00N\r\u0602\xc1\x1e\x05WJ?\xc7@\xf8{D\x16n\xb6\xd8#\xf9\xc6\xfa\x8a\x03\xea\x14xC>\x86\x16\x0e\ve\xff(\xaa\x1c\xe8sv\xf5)\xbb\xba/\xae\xf2joY~\xbaf\xf1\xb3\x1d\xadB\x1fI\xf5\x97\x05e\x03\xb6n\x13{E\x17M*\x00\xd6>`\vwIVP\x1aM\x050\xf2\xc82kP\xc6dª_\x92u\x82t\xe3\xfb8Ldk0Țl\x90L\xf0\xb1\xc3|D\xf0k\x90\x0e\xa1\x84\x03\xf1\xb0\xc2Q\x81\xc9\xfb\x00\xbe\xb2wK%]\vM\xe2\xd5\x14\xd3$d4(\xa8?ͧe\x97\x04\xb3\x90u\x9bk\x12X\x94D\x9eD\xe4\xb8\xd6;\xa0#\xbe\xa7\x02\xb2}\x13:ŧ\xd1\x1f\xf2µ\xc8\xc5f\xfb\xb1\x90\xd6\x1d\x0e\xaa\x1dm}@\xf7\xe3\xf2\xf6黇\x93i8\xd5z!\xb5`\x19Ԥ4\x81+\xd4\xc0;\x04O0x\x9a\xa8r\xb3w\x1a\xc8\a$\xb1\xd3\xd5*㨪\x8efg\x12\xde'\x95\xc5\nL*'\xe4\fm\xbc\x04hƃ\x15\x98\x96\x810\x102\xbaR`'\x8e!\x19)\a~\xf5\x15\xb54\xf0\x80\x94\xdc\x00w>\xf6&U\xe1\x16I\x80P\xfb\x8d\xb3\u007f\xee}s:g\n\xda+9\xe4g\x1a\xf9\xd29\xd5\xc3V\xf5\x11\xff\x0f\xca\x19\x18\xd4\x0e\bS\x14\x88\xee\xc8_6\xe1\x06~I\x98\xac[\xfb\x16:\x91\xc0\xedb\xb1\xb12u\x13\xed\x87!:+\xbbEn\fv\x15\xc5\x13/\fn\xb1_\xb0\xddԊtg\x05\xb5D\u0085\n\xb6\xce\xd2]\xee(\xcd`\xfeGc\xff\xe1\xf7'Z\xcf.H\x19\xb9\xd0_\xc9@*\xf3\x92\xf6\xb2\xb5\x9c\xe2\x00:M%:\xf7_\x1e\x1ea\n\x9d\x931\xa7\x9f\xb9\x1f6\xf2!\x05\t\x98uk\xa4\x92\xc45\xf9!\xfbDg\x82\xb7N\xf2\x87\xee-\xba9~\x8e\xab\xc1\nOW2媁\x9b\xdcbSQ\xc7`\x94\xa0i\xe0\xd6\xc1\x8d\x1a\xb0\xbfQ\x8c\xffy\x02\x12i\xae\x13ط\xa5\xe0\xf8\xef07.Ԏ\x16\xa6\xf6}%_\x17\x8a\xf6!\xa0N\x19L\x10\xd3n\xbb\xb6:\x97\a\xac=\xc1Kgu7\x15\xed\x8c\xee\xbe\xc0\x9b\x93\x85\xcb\x05\x9dơM\xceW\xae\x1e\x1er\xee,\xe1\xec\x16\xd6p\xd6s_璛\xe1\xbf$S:\xf1\xc8FG\"trԟեMoe\x81D\x9e\xcefg\xa2\xbed\xa3\xfc\x04P\xd61(\xb7\x1b7\x82tJ\xe0\x05)\x95\x81\xf61\xf5\x194`\xe2\x19\xbf\x11\xcb\xf1\xbf$\x90\xd7\xc8ܜ\xd9Y\xc1ႦW\xb2\x93Fz^\xa8U\x8f-\bE\xbc\x92YE\xa4v\xb3\xb5\xfc\xcf\xfa\x06\x82e\xb2\xb9\x94\x83\xfd\u007f\xfa\x9bIȸ]\x1c\xce#\xd5p\x87/\x17foݒ\xfc\x86\x90\xe7W>-.\v\xbd\xfdc\xe0\r\x94.^ʳIN\xfd\xce\x1cQd\xf1\xa46\xc7\\9\xae\xf6\xfd\xbb\x85\xbf\xfe\xae\x0e\xf7Zi\x8dA\xd0\xdc\xcd_i\xefޝ<\xb7\xf2\xa7\xf6\xae\xbc\x8c\xb8\x85_\u007f\xabJ(4O\xd3\xeb)M\xfe\x13\x00\x00\xff\xff--\nM\xde\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x93ܦ\x13\xbdϧ\xe8\xf2\xef\xe0_\xaa,\x8d]9$5\xb7d\xed\xc3V\x1cǵ\xeb\xec%\x95\x03\x83z$\xb2\b\b\xdd\xccz\xf3\xe9S\r\xd2\xfc\xd1hfLJp\x134\xcd\xe3\xf1\xfa\x81\x16UU-T0\x0f\x18\xc9x\xb7\x02\x15\f~et\xf2E\xf5\xe3\x8fT\x1b\xbfܾ[<\x1a\u05ec\xe0&\x11\xfb\xfe\x0eɧ\xa8\xf1=n\x8c3l\xbc[\xf4ȪQ\xacV\v\x00\xe5\x9cg%\xdd$\x9f\x00\xda;\x8e\xdeZ\x8cU\x8b\xae~Lk\\'c\x1b\x8c9\xf9\xb8\xf4\xf6m\xfdC\xfdv\x01\xa0#\xe6\xe9_L\x8fĪ\x0f+p\xc9\xda\x05\x80S=\xae\xa0\xf1O\xcez\xd5D\xfc;!1\xd5[\xb4\x18}m\xfc\x82\x02jY\xb4\x8d>\x85\x15\xec\a\xca\xdc\x01P\xd9\xcc\xfb!\xcd]I\x93G\xac!\xfeen\xf4\xa3\x19\"\x82MQ\xd9S\x10y\x90\x8ck\x93U\xf1dx\x01@\xda\a\\\xc1'\x81\x11\x94\xc6f\x010\xec=ê\x86\xddmߕT\xba\xc3^\x15\xbc\x00>\xa0\xfb\xe9\xf3\xed\xc3\xf7\xf7G\xdd\x00\r\x92\x8e&pfp\x82\x19\f\x81\x82\x01\x01\xb0߁\x02\xe5@E6\x1b\xa5\x196\xd1\xf7\xb0V\xfa1\x85]V\x00\xbf\xfe\v5\x03\xb1\x8f\xaa\xc57@Iw\xa0$_\t\x05\xeb[\xd8\x18\x8b\xf5nR\x88>`d3\xb2\\ځ\xb8\x0ez'\xc0_\xcb\xdeJ\x144\xa2*$\xe0\x0eG~\xb0\x19\xe8\x00\xbf\x01\xee\fA\xc4\x10\x91\xd0\x15\x9d\x1d%\x06\tRn\xd8A\r\xf7\x18%\rP\xe7\x93mD\x8c[\x8c\f\x11\xb5o\x9d\xf9g\x97\x9b\x84!Y\xd4*\x1e\xe5\xb0o\xc61F\xa7,l\x95M\xf8\x06\x94k\xa0W\xcf\x101\xf3\x94\xdcA\xbe\x1cB5\xfc\xea#\x82q\x1b\xbf\x82\x8e9\xd0j\xb9l\r\x8fE\xa5}\xdf'g\xf8y\x99\xebì\x13\xfbH\xcb\x06\xb7h\x97d\xdaJE\xdd\x19F\xcd)\xe2R\x05Se\xe8.\x17V\xdd7\xff\x8bC\x19\xd2\xeb#\xac\xfc,2#\x8eƵ\a\x03Y\xf3\x17N@T_\x04S\xa6\x96]쉖.a\xe7\xee\xc3\xfd\x17\x18\x97·1e\xbf(g7\x91\xf6G \x84\x19\xb7\xc1X\x0e1+Or\xa2k\x827\x8e\xf3\x87\xb6\x06ݔ~J\xeb\xde0\x8db\x96\xb3\xaa\xe1&;\r\xac\x11Rh\x14cSí\x83\x1bգ\xbdQ\x84\xff\xf9\x01\b\xd3T\t\xb1\xd7\x1d\xc1\xa1IN\x83\vk\a\x03\xa3\x93\x9d9\xafI\xa9\xdf\a\xd4rzB\xa0\xcc4\x1b\xa3si\xc0\xc6GP\xfb\xca\x1f\b\xac\x8f2\xcfWn\x06\xa7b\x8b<\xed\x9d`\xf9\x92\x83d\xf9\xa7N\x1d\x1b\xcd\xff\xb1nk\xf1\n\x1a\x80\x14\xf7\xf8\xae>\xc9x\x1e\x03̪w\x16\xc9(b\xa1Ax\x15+\x10\x93:\xc4t\xba\xb44t\xa9\x9f_\xa0\x82\x9f3揾\xbd8~\xe3\x1d\x8b\xdc/\x06=x\x9bz\xbcw*P\xe7_\x88\xbde\xec\u007f\v\x18\xcbUz1t\xbc\x91w\xb7ԅ\xc0dϮ{\x87\xe2\xf7x~\xa7C\xc0UY\xae\xc04D^\xb5ћ\xfb\xdbo\xa1\xf0L\xf8\xc5C:S\xb6c\xcb\xd7\xf3\xcb\x1a\x94\v~ԠL)w\x16\x82<{\xa2CF\xda\xdb\xe7\x93\xe1n6#\xc0Sgt\x97'f\x01\x8b3\x13ym\xb2\xcf};|\xa9{\x13q\xa6\x88\xaa\\\\3\xdd\x02\xfe\xa4\xfb\x8c[\x9d[\xa0\x1a\x1c\xe4*\xc7cʼn\xbe\xc1\xf3r\xfcH\xb5N1\xa2\xe3!K~\x03L'\\kz\xa3S\xfc~\xf7\xf1\x05\xe7{\xbf\x8f̯\\e\\A\x13\"VdZy\xb9Șx_\xf6\xa4S2J;~I\x1d\x135{\xa2\xf85\x98R0/@\xfc\xb0\v,\x06\x8d\xae\\\xbeӷbN\x88\x94\x1f6ZM\x9fT\xd2\xd6\b\rZdl`\xfd\\n\x9agb\xecOqo|\xec\x15\xaf@.\xe5\x8a͌\x8c\xe4=\xaf\xd6\x16W\xc01\x9dS\xd9\xec\xc6C\xa7h\xa6\f\x8f\xf6\xfcYb愱+Ƌʀ\xb3\xf7A\x05\x9f\xf0i\xa6\xf7s\xf4\x1a\x89\xf0\xb4\x8c\xce\xeed\xb6\bN:I^N\xcd\x01KÃ|\xe8ٗ\x8c\xd2\x1a\x03c\xf3i\xfa\x97\xf3\xea\xd5\xd1oK\xfe\xd4\xde5\xa6\xfc\xa0\xc1\x1f\u007f.JVl\x1eƿ\x11\xe9\xfc7\x00\x00\xff\xff:@\xbd\xf3\x1a\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z\xdds\x1b\xb7\x11\u007f\xe7_\xb1\xe3<\xa8\x991\x8f\x89\xdbi;|s\xa4\xa6\xa36\x915\x96\xad\x17\x8f\x1f\xc0\xc3\xf2\x0e\xd1\x1d\x80\x028\xcal&\xff{g\xf1q\xbc\x0f\x90\x944ur/6\xf1\xb1\xf8a\xbfw\xa1\xc5r\xb9\\0-\xee\xd1X\xa1\xe4\x1a\x98\x16\xf8š\xa4_\xb6x\xf8\xbb-\x84Z\xed\xbe_<\b\xc9\xd7p\xd9Y\xa7\xda\xf7hUgJ\xbc\u00ad\x90\xc2\t%\x17-:ƙc\xeb\x05\x00\x93R9FÖ~\x02\x94J:\xa3\x9a\x06ͲBY\xf38\xfa\x01\xed\xe4\xfa\x8b\x99\xdb\x1e\xd1~[aޞ\xc3\xf4\xee\xfb\xe0@\xcb\x1a[\xb6\x8e+\x95F\xf9\xf6\xf6\xfa\xfe\xcfw\xa3a\x00m\x94F\xe3Dr\xe8\xe1\x1bı\xc1(\x8cY}A\x04\xc3*\xe0\x14\xc0\xd0\x06\xab\bc\xc8#\x86 \x0eaIu\rZ\x94nȒ\xf4\xa9-0\tj\xf3\v\x96\xae\x80;4D&\t\xa6Tr\x87Ɓ\xc1RUR\xfc\xb7\xa7mI\xd7\xe8І9\x8cq\xe5\xf0y\xd7/Y\x03;\xd6t\xf8\x1a\x98\xe4в=\x18\xa4S\xa0\x93\x03z~\x89-\xe0ge\x10\x84ܪ5\xd4\xcei\xbb^\xad*\xe1R\xfc.U\xdbvR\xb8\xfdʇb\xb1\xe9\x9c2v\xc5q\x87\xcdʊj\xc9LY\v\x87\xa5\xeb\f\xae\x98\x16K\x0f]\xfa\x18^\xb4\xfc\x1b\x13#\xbe\xbd\x18a\x9d)F\xf8|x=!\x01\n\xb0 ,\xb0\xb85\xdc\xe2\xc0\xe8\xe4 \xdf\xff\xe3\xee\x03\xa4\xa3\xbd0\xa6\xdc\xf7|?l\xb4\a\x11\x10Ä\xdcbt0[\xa3ZO\x13%\xd7JH\xe7\u007f\x94\x8d@9e\xbf\xed6\xadp$\xf7\xffth\x1dɪ\x80K\x9fԐ\xc3\xec4i./\xe0Z\xc2%k\xb1\xb9d\x16\xbf\xba\x00\x88\xd3vI\x8c}\x9a\b\x86\xf9\xd8tq\xe0\xda`\"%MG\xe45Ʉ\xee4\x96$=b \xed\x14[\x11=\x14\xb9s6]^\x8c\b\xe7\r\x97\xbe\xacw\x9a.\x82\\p\x99\xecI\xd8\xe4\xc0\xa7&\x87\x19VΈ\x024S/\xdb\xef\x19F.\x1b\x1dl1\xa3pD\f\xf4I\xc5\xf1\xcc=n\x14\xc7\x1cl\xda\n\xaefA[)\xe3#\u007f\xd4I9?\x85>%\x9f\x05L+~\x06W<\x91\x81\xc1-\x1a\x94%&\xc7u*\x9d\xc9 \x1b&\x1as\x8cǕ\x02Nx\xf5,ⷷ\xd7ɓ'&F\xecn~\xee\x19\xfeз\x15\xd8p\x1f\xe8Ο}q\xbd\r\x87y\x9f\xe6\x140\xd0\x02Cb\xda\a\t\x10\xd2:d\x1c\xd46K\x91\xca' \xc37\x18w\xbc\x0e\x1e,\xba\xcaCh!\xde\x03#\xdf)8\xfc\xeb\xee\xdd\xcd\xea\x9f9\xd6\xf7\xb7\x00V\x96h}^\xee\xb0E\xe9^\xf7\xa5\x02G+\frJ\xfc\xb1h\x99\x14[\xb4\xae\x88g\xa0\xb1\x9f\xde|\xces\x0f\xe0Ge\x00\xbf\xb0V7\xf8\x1aD\xe0x\uf593\xd2\b\x1b\xd8\xd1S\x84G\xe1j1\r\xa6=\aH\xbd\xe2\xb5\x1f\xfdu\x1d{@P\xf1\xba\x1dB#\x1ep\r\xaf|Zs\x80\xf9+\xd9\xceo\xaf\x8eP\xfdS0\xedW\xb4\xe8U\x00\xd7\xc7\xe1\xa1\xd1\x1d@\x06\xcb3\xa2\xaa\xf0\x90UM?\x1fT\xc8U\u007f\v\xca\x10\a\xa4\x1a\x90\xf0\x84Iz\xc1Q\"\x9f\x81\xfe\xf4\xe6\xf3Q\xc4c~\x81\x90\x1c\xbf\xc0\x1b\x10\xb1\xd8Ҋ\u007f[\xc0\a\xaf\x1d{\xe9\xd8\x17:\xa9\xac\x95\xc5c\x9cU\xb2ه\xf6\x881\tʄyp\xcdL\ueffa*\x13C;C\x88\xf6\xcb\xd8\xf6[2\xc9\xe9\xffVXG\xe3/\xe2`'\x9ed\xbe\x1f\xaf\xaf~\x1f\x05\xefċl\xf5H\x02\x1etd\xd8\xe58\x93\x98\xbd\x1f-N\xa9c&c\xed\xd7<+3t\xacʤb\xc3\xf6䩄\xed$\aƭ\x18VY`\x06\x81A\xcb4I\xee\x01\xf7\xcb\x10\xe25\x13\x14\x9f)\x04\xf7}\x0e`Z7\"\x1b\x8ac \x8fIh\xe4\x04\x15ڬ\xb2\xc7\ue795ð\xafsF\n\x1f\aK\x93\f\xcet\x96\\\x9d\xb3\xd4Q\xbfi\x8e\x16e\xd7Ρ,\xe1Ai\xc12\xe3\x06\xad\x13ef\xe2\xd5<\xd38!\xac\xc0\xcb3<\x88-\xe8L\xf1\x12E\x112\xbd\xbe\x80\xf1]\xc7\\\x85p\xbc<8\n\x91*t\xca[\xc7\x10\x97\xf9Rr\xb2\x86J\xabɐV|1ed\xa6\xf3\x98&G\x9d\xd1!\xd2y}\xed\x1b\xdeϨ\xb0C#?\xf24\xf8S\x97\xda\xfbTL\xbc\xb4\xc6.\x15\xe5\xe9㧕\xd3⽜\xef\xf0\xed,ã\xba\x8b\x96\xacw\xd0\xf6\x8fg\xe4\x8ad\x18\x90\v;}\x04#j\xc8}\x12M9\xfe\x96\x89\x069\xa4\xb7\x9d\xe9\x9e\f\xd5!\x95\rn\xc9\xdd\a\xd3K\xa5i\x84\xd7'\xaa5\x82\xf5}\xa2\v{\x82fg\x91\xfb\x9eF\x86\t\xf3\xe4u\xabL\xcb\\\xe8k.\xb3De\xd74l\xd3\xe0\x1a\x9c\xe9\xe6\xd3',\xb1EkYu\xce\x14\u007f\x0e\xabB\xc5\x1e\xb7\x00ۨ\xce\xf5%\xfb\xc8=^بS\xcf\xeb\x1ad\x8b\xe1\xb1:3*VlLڛ\xc6\xef\x19:\x82Ã\xa0G\xb5\xc1|\xd0\u007f\x89O\x00\xf0\x0fZ\xe7\x10Қ\x9c\x81\xf5\xde뤅\xc1\t\xa7|\x83\x8f\x99\xd1\xd9C\xdcp\xf22\x99Lf\xeeGo\rϺ\u007f<\xe8\x1c\v\xe22\xa8U\x93\x8cY9ր\xec\xda\r\x1a\xe2\xc3f\xefЎ\xddy\xae?\xe3\xeb\xba\x03\x1b\a\xfb\x93\xfc\x02\xa5X\xaa\x96L\xfa>*Y\x97S\xc0\x85\xd5\r\xdbg\b\xa7\x8b\xf8܍\x8c\x8b\\\xc0A\x9f\x93Qk4~\xea\xb9}%\x8f\xe9J\xc9#\x95F\xb2g!\xdd_\xffr\"\xd3\x13\xd2a5\t\x0eq\x9e\xd8\xf9\x03\x9d\xf2uN8\x91\xc4Xɴ\xad\x95\xbb\xbe:\xa3\x05w\xfd\xc2d\r\xb3\xe79\xec\xa9EUȉ\xaa\xf7-\xcf2\xd5\xf1\x13\xf09\xa8\xa3\xc5g\xa2P||\xceŠ;\xd4̐\xa5\xfb7\x81\xcb\xe9\xa3\xd5k\xb0\xc27:)\xf3\f\xa9hhCX\nN\x94Z)\x83\x19\x97\t\xf3\xb02\n\"c\xf8\xbfg\xfc\xc8\xea\xc9l\xd0#\xe7\x03ڱY>\x1c\xe96\xfdC\xd0\x1a~\xfdmqHlXI\xc5\x13\xf2\x9b\xe9\x1fYĔ3\xfdՄ\xffY*\x19*\t\xbb\x86O\x9f\x17\xe9\xd9\xf2>\xfd1\x04\r\xfe/\x00\x00\xff\xff\xb0\xddǼ\x99\"\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4YKs\x1b\xb9\x11\xbe\xf3Wti\x0f\xcaV\x99\xc3]'\x95\xa4x\xb3\xa5lJɮ\xac2e]\\>\x80\x83\xe6\f\x963\x00\x02`H3[\xfb\xdfS\x8d\a9/\x92\xa2*Z\xcf\xc5\x16\xd0h|\xf8\xd0/4'\xd3\xe9t´xBc\x85\x92s`Z\xe0W\x87\x92\xfe\xb2\xd9\xfa\xef6\x13j\xb6\xf9q\xb2\x16\x92\xcfᦱN\xd5\x1fѪ\xc6\xe4x\x8b+!\x85\x13JNjt\x8c3\xc7\xe6\x13\x00&\xa5r\x8c\x86-\xfd\t\x90+錪*4\xd3\x02e\xb6n\x96\xb8lD\xc5\xd1x\xe5i\xeb\xcd\x0f\xd9߲\x1f&\x00\xb9A\xbf\xfcQ\xd4h\x1d\xab\xf5\x1cdSU\x13\x00\xc9j\x9c\x83V|\xa3\xaa\xa6F\x83\xd6)\x836\xdb`\x85FeBM\xacƜv-\x8cj\xf4\x1c\x0e\x13aqD\x14N\xf3\xa0\xf8\x93\xd7\xf31\xe8\xf1S\x95\xb0\xeeߣ\xd3?\v뼈\xae\x1aê\x11\x1c~\xd6\nY4\x153\xc3\xf9\t\x80͕\xc69\xdc\x13\x14\xcdr\xe4\x13\x80H\x80\x876\x05ƹ\xa7\x94U\x0fFH\x87\xe6\x86T$*\xa7\xc0\xd1\xe6Fh\xe7)\xdb\xeb\x01\xb5\x02W\"m\xe9\xe9fB\nY\xf8\xa1\x00\x01\x9c\x82%BD½2\x80_\xad\x92\x0f̕sȈ\xb8L+\x9eɤ3\xca\x04\xce\xef{\xa3nG\xe7\xb0\xce\bY\x1cC\xf6\u007f\x06\xd5\xc1\xf3\xa0\xf83\x91<\x96\xe8e\x12\x9aFW\x8aq4\xb4y\xc9$\xaf\x10\xc8r\xc1\x19&\xed\n\xcd\x11\x14i\xd9\xe3Nw\x91|J\xfaZ3\x97\xb0s\t\x15A\xb6\xb3\xfdS{\xe8ܾ\x0f\x8a\xc7\x05\x10\x8d\x1a\xacc\xae\xb1`\x9b\xbc\x04f\xe1\x1e\xb7\xb3;\xf9`Ta\xd0\xda\x11\x18^<\xd3%\xb3]\x1c\v?\xf1\xba8V\xca\xd4\xcc\xcdAH\xf7\u05ff\x1c\xc7\x16\x17eN9V\xbd\xdf9\xb4\x1d\xa4\x8f\xfdဖ\x9c\xad\x88\xd7\xffM\xe0.\tҭ\x92]^\xdf\xf7F\xc7\xc0\xb6\x94\xa6@\x9c\r\x82hG뻢\xab\x8f3\x17\x06\xc2\xf4\xe6\xc7\x10\xca\xf2\x12k6\x8f\x92J\xa3|\xf7p\xf7\xf4\xe7Eg\x18@\x1b\xa5\xd18\x91\xa2k\xf8ZY\xa55\n]f\xafIa\x90\x02N\xe9\x04mp\x8a0\x86\xa3f5\xff\xce\xc4\xfck\xaf;X\aN\x17>\x9f\xebN\xdc\x00%;\x10\x16X\\\x1aNq :\x85\xec\x8f\xffX\xdc\xcf\xfe9\xc6\xfc\xfe\x14\xc0\xf2\x1c\xad\xf5\xf9\x1ak\x94\xee\xcd>gs\xb4\xc2 \xa7\xc2\x05\xb3\x9aI\xb1B벸\a\x1a\xfb\xf9\xed\x97q\xf6\x00~R\x06\xf0+\xabu\x85o@\x04\xc6\xf7\xe1/ٌ\xb0\x81\x8e\xbdF\xd8\nW\x8a~\xd2\xda3@\xd6\x15\x8f\xbd\xf5\xc7ul\x8d\xa0\xe2q\x1b\x84J\xacq\x0eW\xbe\x12<\xc0\xfc\x8d\x1c\xeb\xf7\xab#Z\xff\x14\x1c芄\xae\x02\xb8}\xbek{\xe4\x01\xa4+\x99\x03gDQ\xe0\xa1\x10\xed\u007f>xSH\xfc\x1e\x94!\x06\xa4j\xa9\xf0\x8a\xe9\xf6B\x00\xfd\xf9헣\x88\xbb|\x81\x90\x1c\xbf\xc2[\x102p\xa3\x15\xff>\x83Go\x1d;\xe9\xd8W\xda)/\x95\xc5c\xcc*Y\xedB\xb5\xbfA\xb0\xaaF\xd8bUMC\xbd\xc1a\xcbv\xc4B\xba8\xb27\x06\x9a\x19w\xd2ZS\x95\xf1\xf8\xe1\xf6\xc3< #\x83*|\xbc\xa3\xec\xb4\x12T5P\xb9\x10r\x9e\xb7\xc6A\xd2L\x9fm\x82\xf98\x05y\xc9d\x81\xe1\xbc\b\xab\x86\xb2Pv\xfd\x12?\x1e\xa6\xfe\xf4\x8d\x94\x00\xfd\xc0\xf1͒\xe83\x0f\xe7+\xd5g\x1c\xae\xfd\xd6:y\xb8u\xb3D#ѡ?\x1fW\xb9\xa5\xa3娝\x9d\xa9\r\x9a\x8d\xc0\xedl\xab\xccZ\xc8bJ\xa69\r6`g\xfe\xc9<\xfb\xce\xff\xf3\xe2\xb3\xf8\xd7\xf5s\x0f\xd4y\xf4\xbf\xe6\xa9h\x1f;{ѡR\xad\xf8\xfc2q5\xacTN0\x11\f\xe0\f\a\xb1\x994\xf22\x8a\xf6\x13*E?B\xaf\x11oE\xe3!\xf6R\xbb\xa2\x874\x95\xbd]\x84\xd3\xf1\xf7^OF+>\xe9\x93\xd6v\xc9\xde\xe4\xc1\xa1\xfa\x13][\xed\xcdv\x9a\x9c\xed\xd3\f\x9fʾ\x83v\xc9c9t\xed\"\xef!f\xbb\xd4ˣ\aˋ\x9f˹\xa2\xc7@\xf7W\x8b\xd36p3\\\xe1{S\x86G\x9f\x105\xfa7hh8n\x99M\x9b\x8c\xdd7\xb4\xf4\x85\xa5>O\x92:\xe4\xbeT\xa7\x97Ċ\x89\n9\xec\u007f7\xf1\xcdq\xeb\x9b4\xd7c\x95iR\xd4X\xe4>n\x8c\x80\x1e\xaeK}O\xce\x1cNI\xc5@B6UŖ\x15\xce\xc1\x99f8}½j\xb4\x96\x15\xe7\xfc\xeb\x97 \x15^\xf1q\t\xb0\xa5j\xdc\xfe\x19\x1f\x1d-Rqm\xa3\x15\\\xd6J(\x99=\a\xe5\x81d\xc6,n\xef\xf2\xa7M\x0eN\x84\xb2{\u070e\x8c\x0e\xfa\xd0\xedɛdB#s?y븈\x80\xb8\xd19\x0e\xa2\x18\x94\xaaJ֭\x1c%\xa5\xa6^\xa2!\"|\xf3;1\x92\x02\xc7X_Ŀ\xa7\x0eL\x1e4\xa4X\x18T\xc5\x17bΤo\x13\x92\xfd:\x05\\X]\xb1݈\xdet\x12_2\x91\xf9\x92\x1f\x1d,&y!\xb9\xbf\x9f\xbb\xb4\x9f\xb3o\xee\x8f\x17tc?\x15\x8c\xddB\xbb\xefߛ\xdf\xff\xaa\xf1:;\x9c(\xe2\xacc\xc6=7\xec-:\xc2\xe7\"\x9eW=\x1e\xefڡk\x18\xa8\xba\xdb\xfc\x911j\x94\xa8\xc1\xa0G\xce[\xbac/\xb4=\xd2,\xf7\x9d\xfe9\xfc\xf6\xfb\xe4\x90\xeeXNU;\xf2\xfb\xfeOڱVI\xbfP\xfb?s%\xc3O\xcav\x0e\x9f\xbfL 6M\x9f\xd2\xcf\xce4\xf8\xbf\x00\x00\x00\xff\xffe\xe5\xd5&\b \x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xdc:\xa5\x02\x1b\x84\xa6.\x99\xc5r\r\xf7\x12>\xb2\n\xc5Gf\xf0\xea\f J\x9b\x15\x116\x8f\x05}}x:\xd8S\xad\xf7GT^\x13\xfc\n\xa7\xff\xb1\xc6bpbh\x1a߆c\x0e[\xa5\aʁ\xa6\xac\a@Ӈ\x96\x1e\u007f\xfaI\x83\x9d\xfes\xb2\x95\xffh\a\x92\xfc\xd0&\x1a\xc9\u007fmЩ8\u007fbq\xa4RF !\xeeω\xc5z\xf4\xff\x04M\xe9\xc1\x1f\x85hJ,[m;\xc2\xe5dǟF\x13\x9c9b\\\x92\xfc\x93\xfa\xa7m\xcb\xee_R\xa7\x89\x1d3\x8d@\x12ȥ\x87\a\\:d\x93\x94\xa6\x87[\xac\x12\x9b\x9b\xc5\x0e\x9c\x9dc\x1b\x81w`u\x83\x13\x94aZ\xb3\xe3\x04a\xa2mΥK;>(\x04\xc1\v\xec\x1b\nO\x1aod\x98\x1e\xef\b~\xe7T\xe1\x86\xd4Y\xc4\xf2A\t^\x1c\x17I\x93\x9a\x14\x8f[8|Q\x827\xb8g\a\xae\x1a\x9d\xc0\x89\x8e$\x8d}\xea,i\xa7M\x15)\xb3\x00\xa5\xbc\f\xe3$\xb5\xf6J=-1\xff\xcf4\xa6S\xdbP8\xb7.\xe2\xa2\x03\xbb\x83\x15\xdd \xe0\x0f,\x1a\x9b\xd8&@\xd98\v\xa24\xd4\xca\xd8i\xc6O+\x1f\xf0\xfa`JjaNjF\x98\x05]\x19YG\x88\x0e\xf4\xa6\x92H{\xad\x88u\xddX\xad\x1a?\xf6\xd4\xc0\xf5(\x9e\xa6\bl\x98\xc1\x12T\x10\xfbF\xa0\tk\x95\x8e\xfd\x9db\xb9\x9d\x04\xdd\"\xef]\r\xc16(\xc0\xa0\xc0\xc2*=\xa6d\x0e=\xfd\x93\xa3,'\xe8\x98P\x9bC\xf9\xef\x10\x9b\x01\t$\xe6\xcf{^\xec\xbd\x17@\xb2\xe9\xe0@\xa9\xd08\xcdA\x9e\xeaq\nIX\xe2}XdNwt\xcf\u0099:\x85\x97\xd2'ݓ\xa1o\xbbgA\xf3\x8e4Kx\x9f4\x9d\xdd\xf3\xff\x93\xb0є\\ \xb4\xf7\xa3\xa9\xaf+\xb4.\xaa\"o\xff~\vX\xd5\xf6x\v\xdcƷK\x10\x99\x10\xbd\xf5\xff\xc0\x8c9_\xe2\xefOg\xbe\xaa\xc4\xcfre\t\"q\xa5]\xfe\x0f\xc8\x14g,\x1e\x83\xad\xc8f\xc8_\xfb\xb3n\x81o[\x86\x94\xb7\xb0\xe5¢>\xe1̋\xce\xcbk\x10#\xc7\xde\xd1S1[\xec?\xfd \xcf\xc6t\x19\xa8L\xba\x9cN\xf6>q\f\x12\x86\x86y\x01.\xb8\xf8\x95k\xac|\\\xfc\xcdQ\xb3{\xe3\x02\x8a\x0f\x9f\u007f\xc2r\x8e<\x90'y#D>\x9cl\xb6\xbftp\xf4s\xd1\b\xaeO\x1b4\xf9\x8c\xc7-0x£\xf7X\x98\x04b\x0e\xb3\xce\xdfM\x86Oc\xe2\xb8ԋw\x8f\xf1\xe8\xc0\x84\\\xca\xe2\xec\\Q\xf0\xcf\x13&\xfc\xfd\xd43 \xed)D\xb8\x9e\x92\xf4\xc2\x11\xc2E\xde\xf9\xc4\x03\x97\x17\x8b\xbah\x199\xc8W$\U00049d3f\x00͖m\xbd\xfc\xa1c\xec[\xe3YD\xa7`\xcf\xebLD]\xfaР;-13\xf6\x9d\t^\xb6\vy\xb9\xbf\x97\xd3\xde\xf0\xf0\xf9\xac콼\xf5!\x99qR\xf2\x93B\xf3YY\xf7\xe6*\xe4\xf4\x1b\xbf\x80\x98~\xa2;^ҫm\xa2C?Ŗ!\xdc\xfe\xb9\xf7\x99\x94\x96=\xdc\xc0\xbd\xa4\xc0%\xd0\xc3%L\xfdr\xf3\xf6a\xf8T\x8dq94\xa9\xe4ʙ\xcauj%O\xecL\x90J\x0f82\xdeZ\xbb\xa8_0\x13\xec7\xb2$~\xbeO\x01\vV`\x19\xa3M\x97\xb8d\x16w\xbc\x80\n\xf5n\xcep\xf4\x9f\x9a\xf4{\xde\x162\xb5\xae\u007fΔ\xb0<\xd3\x1e\x9f\xa0\xba\xcb\xe5ͬ\xe8\xe4f\x8c\x8a\xcc^\x1c:\x91\xaf\x9c\x1e\xba\x8c\x913\xb1\xce\xffX\xa4.+KW\\b\xe2\xe1\f\x8d\u007f\x06/ƶ\xdfo\xcc[Ȋ\xd5t~\xff\x87̜\x13\xe8\xff\x85\x9aq\x9dq\x86?\xb8:\x91\xc0\xc1ܐ\x19\xeb/C+p\x03\xc4\xdf\x03\x13\xe3Lx\x029E\xba\x05\x857\xe4j;\xf2Xn\xe1y\xaf\x8c\xb7\xa9[\x8e\"\x95\xb2\x19>\xdc\xc0\x9b'<\xbe\xb9\x1d\xe9\x817\xf7\xf2\x8d7\xf0g\xab\x9b\xd6[PR\x1c\u135b\xfb\xe6%NP\xa6$f\r\x93\xc9\"\xaf\xed\xd5\x16\xaa\xaa\xd8i\x95/k\xab\x1f\xfd\xcc(\xd3\x01\x90\xe7\xbe\xde5\xee\\\xe6\xbb{Q\x86\\}\xef\x99\xdb=\x97\xc0\xe2\xf1G\x1d\x04\x8aA\xad\x965\x91\u007f\xf6\xcc\xc0\x06Q\xb6\xb9\xf1߃\xbd\xae\xb8\xbcw\v\xc0\xfbW\xb7\xefБ\xeb\"vFR\xb7\fm_8\x8b\x93\xeb\x1a\xa9\x12\x9e\xf7\xa8q \x15\xe3\x847y\x8c\x99 \xa5\xb2\xfd\xbc\x02\xc1\xadU\xf9\xd6\xc0\x96kc\xfb\x1b\xcd\x15\xb8\xc6\xe4\x8aÙ\x1c&\xec\xbe\xf1\nUc/\xe0\xc1\xa7n\xf6\xa0@[\xb1\x1f\xbcj*`\x95j2\x8c\xbb\u007fȾ𪭢\x06\x0e<3n\xdbz\x92˰XE\\\xaa\x05\xda\\\x16opK\xea\xa8P\xd2\xf0\x12u\xac\xf2{\xcerE\aw˸hR\xe5\x9b\xd4sn\x98*?i}Q\x94\xfa\xc5\xcf\xece\r\xf7\xeayH\xa0l\x12\xec\xd9\x01\x81o\x81[@Y\x10_P{\x95\xed\x96\b\xc4p\xa4\xc9\x16\xcb<\x05O\x0fʦ\xca#\xc0ʝl.g\x93b\xfd\xe1?3.\xae\xc16\x92\xbcˏ\xc6/\xdd\xec\xdf\xe4h\xb4J%߄m\x10\xbe\"+\x8f\xf1|0k)Tu2\xa0@7\xb2\xaf\x11\xafp2Ή\xef\xc2.^3p\xe3\x92g0\xf6$\x9f\xcfm\xdf\xdb!\x10W\xf5vh\x81\xd6\xd0]\x92\x9a\xb9\x1f\x00 S\x19\x1dg\xb7\xf7Vj\xce\xf0|6H\x01*\x96>\xe9E\xe63\xf8Ѿwi\xa2\f\x9e\xc4\xee|\xd7%\x8b\xb3\xfd\t\xf9Y\xa2\x1f\xab\xae]a咂\xfa\x80\xabF>I\xf5,W.\xa64\x8b\xd9\xfav\xf1\x8b\x15\xc7o\xa94\x86\xe2\x95/\x02\xd1\xfe^A)d\xb3\xf9\xac\xc0xN\n\x96Ԑoc\x9d\xf8sq\x17s\xeb\xcfL\x0e5Ǐ\xbe\xff4\xb7\xb7\xe9>=\xab\xe7?<\xef\xd1\xeeQ\xc7\xc6֕\xeb\xe1M\xa9ծ4ٹ\xc2m\xb3\x13\xc9O\xf4\xa6|\x13\xdeI\xfbS\xdaW\x96\x8d\x10\xb7$ج\x11ַ\xa2\xea&!DY=@\x1b\xa5\x04\xb2Ӷ\u061c\"\xfaR\xe9|\xd8\x0f֖\xaecC\x98\x8a\x8b$0\xf4\xbc\xf4]\x9f\xfd\xba\xec\xb0\x06\xee\xb2?q\xa7\xff\xf0V\xb1\x8c\xf2\xf6BQ{\xbe\x81n\x8e^c\xb1\xe9S\xac\x93\xc10.tV\xfe\xbe\xc8g\xb1\xfaR\x87s0\xe9/\x0e)\x98\x98r\xd2\x19\xe047E}\xae\xc6\xccxJ\xb9\xd0)\f\x19%\x82\xf8\xa1p'QE\xc0\xc6e\x1d\xc3i\v\x9d\xce\xdc\xc0{ث&\xd1]5C\x9d\x85Z\xfbt\x85=\xe4\x93Ѳ\xc3\xfb\xf5\xf0\x1f\xabB\xbd\xdd%O\x12\xd8=\xef\xdbT\x88s.d\xc9\x0f\xbcl\x98\x18\x1c\xb2\x9eXt\xd2\x03J\x83\xe4\"Uj#\xb1\x8a\xf3\ab\x04_j\x9fr?[\x1d\xcd{tye\xf9\x8b\x8b\xf1\xc3b\xfb\x84\x91:7\xfb\x9c\xdfu\x98_n\x9f\xaf\x8f\x9fSd?-\xa1O\x02].\xad\xe78\xe3\ve\xf4\v\x8a癍S/α\xe7\x94\xc7/*\x8a/\xf6\x16e\x96\u0087E\xeey\x90g\x14\xc0\xb3\x88\xb3\\\xec>\xbb\xc4\x1dJʳxd\x17\xb6\x13%\xebY\xc0\x93\xe5\xec\xb9B\xf5<\xc9\x13E\xec\xfc\xf2\xf4,hW\xba^.J\xbf^\xeb\xd9kD\x01Ӫf\xb1\xb0\xfc\xa2(!\xa3t|N\xc1x\x91b\x17\x16\x87\xdb\xe2\xefĺ疄\x87%\xdf\t\xa09\x85\xe0\x89B\xef\x04\xc4\xd9\xf2onyw\x02\xf6\x82ٝ\x95\x92\x99?\xdb\xc0\xe2o\xac\xae\xb9܍9\x9f+\x1f\xb3\xb21\xaa\x0e\xf7\xd7\x1c\bG\xdf\xff\x1fDN\xa9%\xfd7\x97\x89(+\xe6ո\xb4j\r\x1f\xe4q\x04\xd7\xf5{'\xa3\x8a\xe1G9\xb4\xadg.D\xff\xc3\x13\a\xb6\x0f*|\xc3eұ>\r\x9c\xf2\x99\x93LQz\xe0\xef.\x05U_N\x86\xf73u\xf3\xfes\xcau\xe6v\u007f\xa1\xff\\5\xc2\xf2:y\x88k\xad\x0e\xdc\xe5\xfd\xf6xl\xe9\xf9w\xe5>\xf9\xd8\x1c\x1d\xa4/_\xdb\xf3\xb5>\t\x05X\xeaT<\xa3\x10\xc0\xcc\x18\xfd\xc2\u007f\xf6X\xa8\x95\xfb\x92\x898\x19\xe5!|\x1ey\xeb\xce`*\x06\x97\U00043f0a\xc0\xb8O'M\"\xc71i]\xe6=\\\uf33bw\xbf6\xa8\x8f\xa0\x0e\xae\xd0\x1b\\\x9e\x85\x9ej\xaf)\fEdQw\x05\x05\xe8?\xb6=\xf1\xfc;\x8d\x01\x1f\xa4\xb7\xc1I\xb0'{tpHiu\xd1\x0e\xe9g\nd&\x86&\xa1J\xd5\xceN\xcbì\xa9\xc9mH\xben\xecs~\xf4\xb3\xe8w\\%\x02\xba<\x06\x9a\x01\x99\xdb`\x9cW\x94Xl(\xbeV,\xb4\x14\re\xbb\x81y\r\xc3\xd7h\x14>\xa3A\xf8\x8c\xa8輸(\x9bL9\x8d\xc0W\x89\x8e\xae\x18\x1f]#B\xba,FZ\x00y\xd2\xe0\x9bӺ\x9bUGˮ\xc2\xe4\xd4͖K\xb7\xf3-\xb9\x19\xad\xb8\x19\xf5\x9d\xa5\x9df\xb4ܞ\xd7j\x9bA\xc3+EOW\x8a\x9f\xae\x11A]7\x86Z\x8c\xa2\x16%g\xf6\xef\x8b\xcb\x00\xb1`\xfcY\x95\xf8\xa0\xb4]r\xf8\x1fN\xc7'\x8at\xbd H\x89\x12d\x1c\x9a\xaa\x05\x90/\x1f\xfc\xf8ːJ\xd7\xd3\xc2\xfa\x0fߗ\xf0\xf9\xda\x0e\x9cG\x84\\\xd2\x18\x9f%\xf0\xa0\xf9\x0e\x17#Ym\xf6\x89\xf0\xee\xe5\xc8Ho\xa2\xfd\xdd \x05\x19d\xd3\x14\x05\x1a\xb3mD,d\xbb\xfbް\x8cÓ}\x97\x11\x873*\xb6i\xeflջ\xed\xe8&\x833&\xa1&gTd\xc1j\xdb\xe8 \xe6E\xa3\xb5C\xd9\xff\xa7\xb6\xa3\x1b\x9a\x06`\xa7\x95V\xe84\x1a܉7/!\x1f\xc73\xdc=h\xba\xecU\xd6\xfb\x17ٴ\x05\xf3T~\x8a\x99\xb6٩\\\xf7`{0ν'\xd0X\x02\x1eP\x82\x92\xae\x93\x18˹F\xb8o.\xfe\xd5\a\xd4oM\v\xc7\xd5\xfa\xb7Jãeڶ[\x1fK\xc4V\xe9\x8a\xd9;(\x99\xc5\x15;\xccB\xa6/\x99\xd2z9U\xe9Z\x92\x83?\xeb\xfa\x88\x1d{\x85\b\x8d\xc4\x15\x1a\xc3v\xf1\xe6\xa4g\xd4\b;\x94D\xe2\xb9kt\xba^\xecp\x82ۖ\x10\xa2\x16+l\xc3\xc2\x02\xdeR\xb6I\xdcT\x02\xd0_\xceFC\xd8n\xf2\xdcpiq7J\x9f\x86>\xf0\xaf\xc8\xcc\xe9e~#B\xfc\xdc\x1f\x1b\x82_O\x03\xff\xe18\xf3\xfd\x1b\xee\xee7\xcb5\xcey\bʭ|VWF\xbdgfI]>И\xf6\x03\x89ޡl5\xe5\u05c9=\xa5\x1b\xb6W\xf0\x19\x9f\x13o\u007fvB\xef\x12\x1a飴\x82{\xf9\xa0Վb\x8cğ\xbf0n\xb9\xdc\xfd\xac\xf4\x83hv\\\xb6\xdd1\xe7\r~`\xdar&\xc4\xd1\xef'1\xf7c<̉\xff\x96gO\xfc1Ǥ\x80\xf3b4\xe0\x87u\xc1\x11\x97\xfe\xa0\xbb\xcf\x136\xaa\xb1\xfdS\xf1\xd6t\a&\x9d\x99u\xd0\xd6\xf0YY\x8cI7>\x04\xca\rl\xd0\xd8\x15n\xb7J[\x1f\x8c\xadV\xc0\xb7AQ\xa7\x82\fƅ\xf35\xfcU\x84䀴\x85\xdf\xd6\xf2)\rL\x1eA\xbbS᜔\x8a\x1d}\xf7 +\x8a\x86\xf4\xc0;cYʠ\xbdȵu\xceM\x90扼Ĩ\t\xab\x1d\xdf~\xe8\xd8T\x1bԮ=\x9a\xfe\xf6\xa4s_Nx\x15\x94,8\x80\xfb\xbe\xa2\xf7\xe1\x16\x18:\xce\xe9\xf8xN\xf9\xb8\xff\x95e\xe2~\xdaQ\x1b\xf6\n\xb7\x83#\x02n\xfa\x18\x8d\xc1\x9dk\xd3mB\xdcĩijb\xcf\xe4\x8e\xc4G\xabf\xb7\x8f\"8\xa5\xa9\xa7\xf2!\x8d\xbb\xf2\xadv'\xd5\xc4\xe4\xb5m\xb4\xec\xe5^B:\xbb\xec\xb6;\at\x9e\x843~\xa6\xee\x9a\xe5:\x9d\xf1\xc1\u007f\xf5\x90\x92\x99\x94\xb731y\x82\xfe\x89\xfd\xb3v\n3GY\xccw\xf0\xf9Fm>\xd3W?G\x8c$\xbe\xad\x06\xbc\x04\xdfvr>\xbe\x9d\xd7+\x8e\x9d/u\x0e\xf2\xd3~\xf6+\x90ë\xf4Kh\xe1gN\x1d<\x87_b\xe7g\xb1;d\x1bP\x92\x83骺\xa3\x9c\x06D\xb7\xed(\n\x0fF\xc3\xf6\x18p\x1f\x9f7y\xe1l[\xe0\x15\x18U\x8f\x0f;\xb5t\xc7h\xf3\r\xb5\xe1Y\x02eVCҸ\x9e#\x84Q\xf6\x87\bQ\x06\x14 #\xcf\x1e\xc8\xd1\xf4\x14\"o\xa1(:ĝ\xa7\n\xc0\xff\b\xf8@\x06.#\xb3s\xe5\xcd\x19\xc7\u009aP!\xa1\x90\xe2\x1e\x95\x1b\x91\\\x85G^\x14vIc)\x0f='\xab[ȶ(,\xc8H®&\xb3\xb3\x01\x92\xfd\xa8\x8cp\xa1\r\xb2|\xb3z)\xe6Ꮼ\xa8s̯\x8bZ\x1bT\xb7\x14\xf4\x84`pT\xad\fx\xf8q\xaa\xbfw7\n\x9e!q!smbl\xa4\xae\x1d\xc7\xe3X\xa1\x8b\xf3\x88\xa3\x1e\xcd֣p\xbc\xdd\xc0\xcd\x0e4Ɣ\x90\x91\xb0\xfa\xcb\xeaҲ\xdf\x0f>6\x8c\x06\xa6\xb0\xa1ĸDp\x83e\x84\x1e\xb3\xcac\x01\xa3\x98R\xec8\xc1\xa66\x1a=\x87I#\xbd\a,\x12\xa1\xc5oäf\xf8?\v\x9b\xce⎶\xdb+\x8c\vbM\xc1\xb5\xe9qf\xe8\x8e7\x98Q\xe4F\x14\"\x97\x99\v\a\x93\x14M\x87\x13\xbfg\x9a\x9d#\xd011n\xe4\xc6\xcb\xe6\x9eŤ\xef\x0fH\xb0\xbd\x94\x0f)D\xfaOj׆i\x90ٝ>\xd8\xe2\x9e\x1d\xb8Tz\x18\xeb\xe3\x0f\xccj\x13\xb5]\xcc@\xcew;T\x04\xcbnO5\xbbYSĚvR\xa9\xa8iƟ̫e:1\xcfR#6\x15\x1b\fD\xa1\x82E\x9c|Hkis~\xe0y\xcd\nkt\x99\xc8\xdc\xfcX\x83_\xccU\x98\x11\x88\x13\xfc\x9di\x0f\xb3 .\xf5b<)\x90\x82\xa8R\xaa\x98\x12v\xe5\x14L\x9c\f[fC\xb3X@\xd4\x16U\x17\xa8=*Ιl\xf5\xcee\xcb)\xb7=R\xb0-\x16\xa0\xb1\xc0\xccH\x15'O\x8a\x10\xb8\x92\xaa?#\x94\x1dѤ\xfd\xe8eV\x89\xb6\x85\u009b=\xcf\xf6\xce\xf5#)\xb3\xb0 \x97\xa8\xad\xc6`UU\x1c\xa7&\r)\x92\xe1\a\x9bS\x1amIP\x1fC\xb81EҖD\x1dܖ\x19mܧz#6\xafD\xef\xa1)\x9e$\xec7'ݟ_؉\xdc\x1c\xb5\xf5బ\xcc\xf1\x12\xb8\t\xb5)P{N\x9d\xfe\x931\xee\xbc\xd5r3\xec\xfd\xec\xab\xe5Y\xb8֠\xf1'a\x9a5V\xb7\xdeV-bاn\xcfKໆa\xf9%\xecxa\xd0\xfaRs\x88v\x1c\x9dY\xce='\x81Rm/\x95\x92\x99l\xff\xb1\xd9TM\xe81\xa0\xd5\x10\x80\xf3\xcbC\fcy\x90\x00\x12\x1a\xa7\u009e\ap\x85\xa5;g\xb8\xb3룭\xb1\x1e\xe0\xfb\xcf\x1fb\xe1`\xbf$J\xeaɤ\xde\x0f<\x9d.\nv\x82I ;\x93\xb2nZ\x13\xe3\xb9ӤK`\xf0\x80G\xe7Y\x8dnՌ\x15b-k@*\xb4G[V\x8d<\xe0т\xf2gUI\U0001620a+\x0fxLm: *\xe1\xe7w\xc9\x1du\xa9\xc2\xce\"e)\xb5\xa5!\xaa_;`d\xdada\x99R\n%P\xfc\xcci7\f\xeb\x1d\xd0>\xe0\xf1\x8dv\xec\xa3U\xb3\xe7\xd5\x02\n\x90\xc2\x06\x8dv\x85\x85\x93\xc9\xef\xac\xe0y3\x98]'\v ވK\xf8,\r\xfd\xf3\xf1\aׄ\xa2\xc8\xe1\x83D\xfdY\x1a[\xf3\xa2$v\x938\x93\xc0\xae\xb3]\x96\u0099\x05\xa2ˢ\xf1[\x1c\xac\t%\x11m\xd8\xc65\xdc\b\x8a\xcf\x1c}\x96\xb0i\x8f\x019\x87VYk{\xb6)\xa4X[3\x1dF[\x00\xb4\x8b\x97g\x95T=N].\x848\x8a\xa2G\uf3ac\x95\xfb\xe5\xe4Tx\xaa(\xac\n\x96a\x1e\xcex\xec\x1143x\xcf3(Q\xdd#Td7҅j\x81&w\xe5\f)Lw-B\xf1fa\xe4Du\xac\xaci\xd5'\xb6\flNj\x1e9o\x9en\x9e6Kkޭ?\x94D\xfdn\x82\xd42˲\x90_\xa7>\x88Cҹ\x1f%\xb3\x87@\xff \xf3j\xc5\xfb\xd74kȸ\xd2\x1bxo\xd3\xc3\n\xec\xf6\x0f\xbb\x84\x9d\xa1\x92@\x12&\\\x03\xc9Ɂ\x15\xe4>\x90\xf2\x16\x80\x85s&\xe4\xeeăJS1\x8f{\xa9\x9d\xcdo\x0e\xa9V\x0fx\\]\x9eh\xafՍX\xa5\xc1$\x9d\u007f\xa2\xb4\x1a\xafE\x8a\xe2\b+\xfb\xdb\xca:fK\x96\xc8\x19\xce\xdb\x02\xa9Nnj\x13\xb1\x96\x84\x02\x14k\a\xaf\x85:7\xe9J\xe4\xc2\xcf\xcd\"Y\xa6+\xa9#g\xf5\x11\xb4\xbeJm\xdc\x06`\xcf\xdd\x1e\xd9!L\x89\xfe\xfc\xae!\xb0\x9d=/3R\x85\xd4 R\xbb\x83\rr⼞\xe7\xbd;\xb9\xf1\xbb\x91\x0e0\x05\x99\xabVC8\x9d\xber\x87G\xf4\xffy\x98\x99u\x96,\xecJ\xc9\f\xb5\x9e\x17\xa5D\xcb1\xb3a\xdbl\xd62\x17\xbc\xed\x92Ts\xcaVr(\xcb\\q\"\xed\x19\x81\xcd\xc7\x1f\x9d}gRC\xf4w\x8a(\x9f\x83#ش\xe1\xb2d\xc34\xb5dt\xaf]\xef\xb0\x00=0\x170\xa9\xfb\xda*\x95e~\xb3\x17\xc9ߛ\xe3Qrqc\a\x82w/\xe6\xac@P\xe5xn(s\x1d\xfa\xb7\fi*R\xe3W\b\xc9NҞ\xd5(\xecq\xf6\xf4$#\x9dS@δ\x90\xa6\xbbY\xe3Gz\xa3aǕ6-\xc2\v\xa0rm\x93>^6\xc6\x14\x1f\x95:;\xc4\xfc\xe2zw\xb6\x15\xf7\xf2ѧ\b.\t\xac\x03\xf1\xf7\xec\x80\xc0w\xc0\r\xa0\xc8d-\xec\x86\x17\xa9\v\x1af\x01D\xc7DgL\x12mf\xa7\xb3\xa8\xcbt\x82\xac\xadtr1\xbb;\xd6\xed\xf23\xe3i\xbbSp\x1e[\xcdT\x1a\xdeX\xe9\xe7\x16\xfa|\xbcn.h\xc9~\xf0\xb2.\x81\x95Ė%q\xe3\xcee\xf2\x85\xc4Q\xc7\xebGƍ\xcf\xc7w\a\xab˴i&˪@\x83!G/\x93B\xf3\x1c\x1b\xf7\xc1\xf3\u007f4\xe31V\x18\xec\x18/j\xb5@G/\xe6\xccҸͫ\xa7\xe7\x0f\xc6\xd2\x11Y[b&n\xba/p\x9a\xe7\xedG\xa5\x96\xb9\xcc_\x15>\xbfkZ)NR*\xe7\xbc\xd3Y\x98\xd6{\xed{\xa7^x\x998\xc6\xdc\xd3Y\xa8\x16\x93W\xf7\xb4)\xaf\xee\xe9\xab{\xfa\xea\x9e\x0eʫ{\xfaꞾ\xba\xa7\xe3\xe5\xd5=\xed\x94W\xf74\xd9~\xa4`\xb8\xb6;\xb7\x13\r\x92\xb0JL\xc1\x98C{f,\x9fi\xe4/W,ɐ\xbe\x19\xef9r\xb7\xc6_\x8cXی\xfb\x98Դ\xa9+\xad\xd1kR\xa6iI\x86\xc5\xe4n%&x\xe1\xcfpw% p\xeeݕ\x9b\xa9\xfe\xcfwwţ9\xdc8w\x97\"\xc0Č^\xfa͕@\x87\xdeͷ\b\xd4\xe6>\x9cO\fsiH%\xb2p\xa4\xe3rH\xf2Ȩ1\a\xb6\x87\xc6h\x9b\xdf8i\xfe$\xd9r\xd1}\x83\x93\\˗\xb9@\x13\x91\x94\x05\\]\xfde\xf5\xc7 \xffYT\x8f\x12\xdb\xfd/6\xbb\x96\xb0NѺ\x8b\xe9\xdd\xf4\xc8~\x9a\xea\x1fG\x9aϑ\xe1\xd4;31\xbd\x14Wg\x1db\xaa\xde\x1d\x8d\xdf7-\r\x96_*o\xb9\xd2oJߌt{\xc2]i\xa6\x8f\"\xdb+)d\xad\xfd\x8e\x0e\x8d\xf0>s\x97\xc7\xc3@\xfa\xf4rs\x8cM\x1a\xde\xc1^֑{\x193tMȖ\x8d\xe7\xc8\xfa\xac\v4\xec\xf0n\xd3\xff\xc5H\x9f1\x1b\xc1\xfa\x91\x9b\xbd\xbb\xc1\xcf\xf2\x9c\x1c\xf3ε\x9c\xb0x\xfd+\x1eC\xc1\x8b@\x94\n\x04/\x9cT\x06\b}\xa3\xf9\xa5r[|g\xfb)\xf3\x1bM\xe9y\xb5K\xb3i\x9b\xfc\xc7y\xaf\xf8\t9\xb4\xcb.8\xcd\xe6˦ \r)Y\xb2\xe3\xf9\xaf3P\x97\xe4Ʀ\xee!&\xe4\xc1\xa6g\xbf\xa6\x91\a\xec\xdb;\xa99\xaf\xc9Qgj~\xeb\xcbd\xb5&\xe6\xb2v2TgA\x9e\x99\xc1\x9aL\xb0\xb4l\xd5\xe4\x1c\xd5N\xe6\xe9<\xb5&2S\xc7\xf3MgA\x8e壦d\x99&᚜[\xdad\x8cΟ\x84<)\xa3\xf4\xf9\xef\xae<\xe7>\xc5t~hRVh\xd2^\xc6<\xceIy\x9fK\xb3=\x93\xa8\xba4\xb3\xb3\xc9ڜ\x188)\x9f\xf34Wsj*\xb3Y\x9c\xf1\f\xcd)\xb0c\xb9\x9b\ty\x99\x13 \xbb\x19\x9b\x8b݀Yi\x9ai0\xfe>V(\xf3\xb6\xb6\xf8-$𩓖\xaa\xe7\x02\xa7\x84j_\x06]\x88\xf7\xc1\xeb\x1bs\xabはs\xb6\xcfp\xab# ovPօ\xe1U\xd1y\xa0\xca\xec\xf1\xd8<\x80\xf3\x8b\xb4WǷG\v\xed˷F\x80c \xfb\x01\x02\xd3\xf0\x88EA\xff\x9eP!s\xcf\xc1er\x8dds\xe2\xc7x\xfe\xe1\x1f\xff\x96ܥ\xdbز\xf7\xea\xad=+\tRx/茘t\xda\xd9u>\xba\xad\xfb{\x8d\xea\b\xf2\x80\xaa\xf1j\xa2b\xd6\u07b7\xf4KSS\x84\x17T\x89\xd7I\xeeQ¾j\x89\xaf\x86fA\xc3{\xe1\xcc\xec\x10W\v\x8btH\x1b\x1cM\xa9N\x8a\x85b \x84l D\xfa\xa7\xf8\xd2K. \xbeD\xa8\xf4\x1c\xc1R\x92[\xf1\x12\x01\xd3K\x85LK\x83\xa6%\xa9\x17I\x17\b_\"tZ\x12<-\xf2\x00\xd3/\b\xbe\xd4\xc5\xc0\x17\b\xa2\xce\x0e\xa3\x16\x91.\xf5\xe2\xdf\xe2`*a~3\x17\xfdN<\xae\x04\x90\xd1\v~\xe3\x01U\x02ē\x8b}\xb3!U\xca:\x18\x06]O\xbe\xa6\x97\x9c\x86\xb4\xe8,<5\x85(\xed\x98z\xfe\xfa]\u2d7b\xc4C\xec\x14\xec\x13\xaf\xd7-\xbfV\x97H\xe73\x83\xadɡ\x13\xaf\xcf-\n\xb7\xce\f\xb8&!N]\x97\x9b\x0e\xb9\xa6\xb7ӆ\xd7\xe4\xcep'\x12$l\xb6ɓ\x8fI\xa4\xcaQ͞8-\x11\xcdY\xa1\x1c\xc4D\xfd\xf1\ag-\xe1\xadOj\xd5=͊qG6\xafxd\xf0W.\xfc\xd99\taǿ\xe8\x1d\x89\xb5\xceO\xfcĥ\xf58\xfd#\xd9\xee,McŔ=U\xdf\x1e]z\x8e\xde\xc0G\x96\xed\x9b\x11\" \xed\xb8{\xa6a'U\xc9\f\xac\x9aCʷn\x00\xfa{\xb5\x01\xf8Y6\x89\x1c\x9dW\xba\"P5/\xab\xe2H\xd1\x0f\xac\xba`\x9e&8Q\xe1\xd3\xfe\xf1_\xff\xbaiB\xfc{\xdb\xef1\xf6\xf8\xb4\u007f\xe45\xc0\x9e`3\x13G\xf8\xfa\xddzQ\xf6\xf1\xba\xac}\xe4\xcf\xfbH!\x02\x1e\xbc\x01\x18\x01\x19{=z\x11\xb1\xe2I-\xdaH\xc5\xee\xf1\x93t\x0f|\xa7P\xabߣ\xf7ƻ\xd7Q!\xc5\xcd\xdfY\x8d\xcc,|\x9aa\b\xb0\xcd|=ys\x98\xb0\x8d)\xaf\x99\xf5mL\x910\xb9\xbb\xbbOnB\x86\x97\xb8\xf9P\xbbc\xd0uŔF\xa2t\x98\xa8봍۷\xbd|\xb4\x8f\xf6v_\xe1\xee|\xe7\x00m\xa6\xad\xcde:k6\x87\xde;ׁt)\xc2\xfe}\xbcgG\x9dt\x988\x95\xe7 wQXLk\x99q\xab\x81\xecV\x90Mp}\xb9'.\xa7,Ʉ\xb2\xa85~y\x14\xa8\xbe\x85\x85\xaaoD\xec\x99\xed\x1e\t\xffv\xd21\xfaĶ\x91V\xef\r\x9a\x8f\xd9;\xe1\t\xa4ݓ\xe4aO\x8b\xeb\xe6!\xfaS\xd2ͬ\xff\xf8\xda\x1f\xf7Y\xd7\xe3o\xbf\xaf\x9b\xe7\xe8/\x12(\xeb\x9e\\O\xf9Ҁ{\x9b=c\x95\xa9\x957\xabY\xad\xec{\x9f\x04\x04\xdds\x98\xe7}k\xa0\xfdv\xcb\f/ۯ\xb9\xb4Q\xfe\xec\xb7cF\xf8\xd7|- \xfa|\xbe\xb3\xa9\xee\xdb.k\x82\u007f\x1e;Gׁ}\x1fu\xee\xcb\nԦ\xb9\x9c\xe0\tm;\x86wUoc\xa8\x8fg\x9b\xaf\xe13>\x8e\xd4~\x144\x89ӳ4\x97R\x8e\xb9\xdd\x1b\x18\xfb\xce\xca\xe4\x14\x0fM/\x9b\xcf?\xa2-\xfajn\xd0|\x90}d߬o\x9a\xb8\xdc\xfd1\xb6\xfe3߹\x8d\x9b\x8c\xe6\xf4/'-\xa2\x8akRi\xc5\x14\xd6\xe8\x92:\xa9Ԩ\x0e\xf6\x91\xf8 $ކwk\xeam\xfb\xcc-\xfc\xe3\u05cbvU\xb2,\xc3\xca\xf8,7\x87\xb9\xfb\xa6\xd5\xca=\xfa\x1e>Ye\xff̤p\x0e\xb6\xbe\x82\xff\xfe\xdf\v\xf0\x06\xf8{\xf8.\x15U\xfe\u007f\x00\x00\x00\xff\xff\xe7ip\xed\x01l\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ms$+rw\xfd\x8a\f\xf9\xf0\xec\ruϛ\xf0\xc1\x0e\xddƚyv\xc7>\xcf(Fڹ\xd8>\xd0U\xd9j\x9e\xaa\xa0\x16\xa8ִ7\xf6\xbfo\x90@}uQE\xb5\xa4x\x1f!.3\xaa\x86$\xc9L\xf2\x03\x12\xb8X\xadV\x17\xac\xe2\xdfPi.\xc55\xb0\x8a\xe3w\x83\xc2\xfe\xa5\u05cf\xff\xae\xd7\\\xbe;\xbc\xbfx\xe4\"\xbf\x86\x9bZ\x1bY~E-k\x95\xe1G\xdcq\xc1\r\x97\xe2\xa2D\xc3rf\xd8\xf5\x05\x00\x13B\x1af?k\xfb'@&\x85Q\xb2(P\xad\x1eP\xac\x1f\xeb-nk^\xe4\xa8\bx\xe8\xfa\xf0\xe3\xfa\xdf\xd6?^\x00d\n\xa9\xf9=/Q\x1bVV\xd7 ꢸ\x00\x10\xac\xc4k\xd0\xd9\x1e\xf3\xba@\xbd>`\x81J\xae\xb9\xbc\xd0\x15f\xb6\xb7\a%\xeb\xea\x1a\xda\x1f\\#\x8f\x89\x1bŝoO\x9f\n\xae͟{\x9f\u007f\xe6\xda\xd0OUQ+Vt\xfa\xa3\xaf\x9a\x8b\x87\xba`\xaa\xfd~\x01\xa03Y\xe15|\xb6]U,\xc3\xfc\x02\xc0\x0f\x8c\xba^\x01\xcbs\"\x15+n\x15\x17\x06Ս,\xea2\x90h\x059\xeaL\xf1\xca\x10)\xee\f3\xb5\x06\xb9\x03\xb3\xc7n?\xb6\xfc\xa2\xa5\xb8ef\u007f\rkM\xf5\xd6՞\xe9\xf0\xab#\x91\x03\xe0?\x99\xa3\xc5M\x1b\xc5\xc5\xc3Xo\x1f\xe0FI\x01\xf8\xbdR\xa8-ʐ\x13g\xc5\x03<\xedQ\x80\x91\xa0jA\xa8\xfc\a\xcb\x1e\xebj\x04\x91\n\xb3\xf5\x00O\x8fI\xff\xe3\x1c.\xf7{\x84\x82i\x03\x86\x97\b\xccw\bOL\x13\x0e;\xa9\xc0칞\xa7\x89\x05\xd2\xc3֡\xf3\xf3\xf0\xb3C(g\x06=:\x1dPA\xaa\xd7'\x12ك\xf9\xe1\x01\x13\x80\x11\x89*Vk\x12\x8e\xb6\xf5m\xf7\x93\x03\xb0\x95\xb2@&.\xdaJ\x87\xf7N\xf6\xb2=\x96\xec\xdaW\x96\x15\x8a\x0f\xb7\x9bo\xffz\xd7\xfb\f\x03Y\xf2\x94\x02\xae\x81\xc17\x9a\x18\xa0\xfc\x14\x06\xb3g\x06\x14ZΣ0\xb6F\xa5p\x15\xa8\x9b7 \x01\xa4\x82\n\x15\x979\xcf\x02W\xa8\xb1\xde˺\xc8a\x8b\x96A\xeb\xa6A\xa5d\x85\xca\xf00\xf5\\騚\xce\xd7\x01\xc6?\xd8A\xb9ZN\x12Q\x93\xf0\xf9\t\x85\xb9\xa7\x83\x9b\x1f\\\xb7\xf8\x13\x93z\x80\xc1Vb\x02\xe4\xf6\x17\xcc\xcc\x1a\xeePY0\x01\xebL\x8a\x03*K\x81L>\b\xfe\xff\rlm\xa5ސ0\x1a\xf4\xfa\xa0-4\x81\x05+\xe0\xc0\x8a\x1a\xaf\x80\x89\x1cJv\x04\x85\xb6\x17\xa8E\a\x1eU\xd1k\xf8o\xa9\x10\xb8\xd8\xc9k\xd8\x1bS\xe9\xebw\xef\x1e\xb8\t*6\x93eY\vn\x8e\xefH[\xf2mm\xa4\xd2\xefr<`\xf1N\xf3\x87\x15Sٞ\x1b\xccL\xad\xf0\x1d\xab\xf8\x8aP\x17\xa4f\xd7e\xfeO\x81\xa3\xfa\x87\x1e\xae'\xf3\xcd\x15R\x84\x13\x1c\xb0\x1a\xd1\t\x8ck\xeaF\xd1\x12\xda~\xb2\xd4\xf9\xfa\xe9\xee\xbe+L\\\x0f\xa9Ot\xefHX\xcb\x02K0.v\xe8g\xf4Nɒ`\xa2\xc8+Ʌ\xa1?\xb2\x82\xa3\x18\x92_\xd7ے\x1b\xcb\xf7\xbf֨\x8d\xe5\xd5\x1an\xc8\xeeX9\xac+;\x03\xf35l\x04ܰ\x12\x8b\x1b\xa6\xf1\xd5\x19`)\xadW\x96\xb0i,\xe8\x9a\xccaeG\xb5\xce\x0f\xc1\xbcE\xf8\x15\xe6\xf8]\x85Yo\xca\xd8v|\xc73\x9a\x18\xa4=\x1b\x150Р\xae\x8c\xcfZ\xfa\x85\xd4\xd4\xf0\xeb\x00\x0f\xa7\xcbB\xaf\xa8\xad\xfd0{\xe2pkƬ\\9hV\xa7\b9\xe4\xee\x98\x16\xecP\xc2C\x99\xc1\xa4\xaf\xf5R\xed\xdb\tL\xf0\xaan\x1d\xc1\xf1\x84\xab\xf4\x13\x96\x95U\x1b3(\xde\xfbj\x16EK\x9f\xbcq\xa7\x82\xe1\x0fjVz\xed\n'ʍ\xbaۣ\xe5ہ\xe7^{\x9dp\x15&9kK\xa6\xf9\x9d`\x95\xdeKcm\x9c\xac\xcdX\xad\xc1\x00n\xee6\x83F\x1d\xce[\xacȆ\x13\xa3\x8d\x84'\xc6O9튕˛\xbb\r|\xb3.\x11\x06\x98\xe0,9\x98Z\tR\xc7_\x91\xe5\xc7{\xf9\x17\x8d\x90פ\x95\x82]\xbe\x8a\x00\xde\xe2\xceNz\x85\x16\x86m\x80J\xd99\xa0\t5Y\x9b59\x1c9\xeeX]\x18\xaf丆\xf7?B\xc9Em\xf0\x94\xef0\xcd{G$\x02\xe7F\xa3\xef\xe5O\xda12\x81\xa4\x1f#MG\xa6T%s8P\xbd\x18Uy\x81\xa0\x8f\xda`\t[\x0f\xa5\xb1\xd5\xc4\x15\xd2\aE\xe1\xc1h\xd8\x1e\x03\xee\xe3\xe3\xb6^8\xdb\x16x\rF\xd5\xe3\xddNM\xdd1\xda|Emx\x96@\x99\xcb!i\\\xcb\x11\xc2(\xfa!B\x94\x01\x05\xac\x91g\x8f\xd6\xd1\xf4\x14\xb2\xdeBQt\x88;O\x15\x80\xff\x15\xf0\xd1\x1a\xb8̚\x9dko\xce8\x16dB\x85\x84B\x8a\aT\xaeG\xeb*<\xf1\xa2\xa0)\x8d\xa5<\xf4\x9c\xacn\xb1\xb6Eaa\x8d$\xecjkv\xd6`e?*#\\h\x83,__\xbe\x16\xf3\xf0{V\xd49\xe67E\xad\r\xaa;\x1b\xf4\x84`pT\xad\fx\xf8i\xaa\xbdw7\n\x9e\xa1\xe5B\xe6\xea\xc4\xd8h\x9bv\x1c\x8fc\x85.γ\x1c\xf5h\xb6\x1e\x85\xe3\xed\x1a6;\xd0\x18SBF\xc2\xe5\x9f.\xaf\x88\xfd\xbe\xf3\xb1n40\x85\r%\xc6%\x82\x1b,#\xf4\x98U\x1e\v\x18Ŕb\xc7\t6\xb5\xd1\xe89L\x1ai=`\x91\b5~\x1d&5\xdd\xffQ\xd8t\x16w4-\xaf0.,k\n\xaeM\x8f3Cw\xbc\xc1\xccFn\x96B\xd6e\xe6\xc2\xc1\xb4\x8a\xa6É\xdf2\xcd\xce\x11\xe8\x98\x187r\xe3es\xcfb\xd2\xf7;$\xd8^\xca\xc7\x14\"\xfd\x97\xad׆i\x90\xd1J\x1flq\xcf\x0e\\*=\x8c\xf5\xf1;f\xb5\x89\xda.f \xe7\xbb\x1d*\v\x8b\x96\xa7\x9aլ)bM;\xa9\xb6\xa8iƟ\x8c\xabe\xbae\x1eQ#6\x14\n\x06\xa2P\x81\x10\xb7>$Yڜ\x1fx^\xb3\x82\x8c.\x13\x99\x1b\x1fk\xf0\x8b\xb9\n3\x02q\x82\xbf3\xeda\x14\x96K\xbd\x18O\n\xb4AT)UL\t\xbbr\n&N\x86-\xa3\xd0,\x16\x10\xb5E\xd5\x05j\x8f\x8as&[\xbds\xd5r\xca-\x8f\x14l\x8b\x05h,03R\xc5ɓ\"\x04\xae\xa4\xea\xcf\beG4i?z\x99U\xa2m\xb1\xe1͞g{\xe7\xfaY)#X\x90KԤ1XU\x15ǩAC\x8ad\xf8\xce\xe6\x94F[\x12\xd4\xc7\x10nL\x91\xb4%Q\a\xb7eF\x1b\xf7\xa9ވ\xcd\x1b\xd1{h\x8ag\t\xfb\xe6\xa4\xf9\xcb\v\xbb%7GM\x1e\x1c\x96\x959^\x017\xe1k\nԞS\xa7\xff`\x8c;o\xb6l\x86\xad_|\xb6\xbc\b\xd7\x1a4\xfe L#cu\xe7m\xd5\"\x86\xfd\xdcmy\x05|\xd70,\xbf\x82\x1d/\f\x92/5\x87h\xc7љ\xe5\xdcK\x12(\xd5\xf6\xdaR2\x93\xed?5\x8b\xaa\t-\x06\xb4\x1a\x02p~y\x88a\x88\a\t \xa1q*h?\x80+,\xdd>\xc3=͏\xf6\vy\x80\x1f>\u007f\x8c\x85\x83\xfd\x92(\xa9'\x83\xfa0\xf0t\xba(\xd0\x00\x93@v\x06EnZ\x13\xe3\xb9ݤ+`\xf0\x88G\xe7Y\x8d.Ռ\x15\xcbZրTH[[\xa4F\x1e\xf1H\xa0\xfc^U\x12\xbc%\xa2\xe2\xca#\x1eS\xab\x0e\x88j\xf1\xf3\xab䎺\xf6\x03\x8d\"e*\xb5\xa5!\xaa\x9f;`d\xda`a\x99R\n%P\xfc\xcca7\f\xebm\xd0>\xe2\xf1\a\xed\xd8gg͞W\v(`\x156h\xa4\x19\x16v&\xbf\xb1\x82\xe7Mg4O\x16@܈+\xf8,\x8d\xfd\xe7\xd3w\xae-\x8a\"\x87\x8f\x12\xf5gi\xe8˫\x92\xd8\r\xe2L\x02\xbb\xc64-\x853\v\x96.\x8b\xfaoq \x13jE\xb4a\x1bװ\x116>s\xf4Y¦=\x06\xe4\x1cZe\xadioSH\xb1\"3\x1dz[\x00\xb4\x8b\x97g\x95T=N]-\x848\x8a\xa2G\xef\xdeZ+\xf7\xcbɮ\xf0TQX\x15,\xc3<\xec\xf1\xd0\x1643\xf8\xc03(Q= T\xd6n\xa4\v\xd5\x02M\xee\xca\x19R\x98\xeeZ\x84\xe2\xcd\xc2Ȏ\xeaXY\xd9Y\x9fX3\xb09\xa9zd\xbfy\xbaz\xda(ɼ\x93?\x94D\xfdn\x82\xd42˲\x90_\xa7>\x88Cҹ\x1f%\xa3M\xa0\xbfY\xf3J\xe2\xfd\xf74kȸ\xd2k\xf8@\xe9a\x05vۇU\xc2NWI -&\\\x83\x95\x93\x03+\xac\xfb`\x95\xb7\x00,\x9c3!w'\x1eT\x9a\x8ay\xdaK\xedl~\xb3Iu\xf9\x88\xc7˫\x13\xedu\xb9\x11\x97i0\xad\xce?QZ\x8d\xd7\"Eq\x84K\xfa\xed\x92\x1c\xb3%S\xe4\f\xe7m\x81T'W\xa5D\xac%\xa1\x80\x8d\xb5\x83\xd7b\x1b7\xe9Jօ\x9f\x1bE\xb2LWRG\xf6\xea#h\xddJm\xdc\x02`\xcf\xdd\x1eY!L\x89\xfe\xfc\xaa!\xb0\x1d\xed\x97\x19\xa9Bj\x90U\xbb\x83\x05r\xcby=\xcf{\xb7s\xe3W#\x1d`\x1bd^\xb6\x1a\xc2\xe9\xf4K\xb7yd\xff?\x0f3#g\x89`WJf\xa8\xf5\xbc(%Z\x8e\x99\x05\xdbf\xb1\x96\xb9\xe0m\x97\xa4\x9aS\x96\x92CY\xe6\x8a[Ҟ\x11\xd8|\xfa\xdeYw\xb6j\xc8\xfe\x9d\"\xca\xe7\xe0\b\x946\\\x96l\x98\xa6\x96\x8c\xee\x8dk\x1d&\xa0\a\xe6\x02&\xf5P\x93RY\xe67{\x91\xfc\xad9\x1e%\x17\x1b\xea\b\u07bf\x9a\xb3\x02A\x95㹡\xccMh\xdf2\xa4\xf9\x90\x1a\xbfBHv\x92\xb4W\xa3\xb0\xc7\xd9ӝ\x8ctN\x81u\xa6\x854\xdd\xc5\x1a\xdf\xd3\x0f\x1av\\i\xd3\"\xbc\x00*ה\xf4\xf1\xba1\xa6\xf8\xa4\xd4\xd9!\xe6\x17\u05fa\xb3\xac\xb8\x97O>EpI`\x1d\x88\xbfg\a\x04\xbe\x03n\x00E&kA\v^V]\xd8n\x16@tLt\xc6$\xd1fv\x1a\x8b\xbaL'Ȋ\xa4\x93\x8b\xd9ձn\x93\x9f\x18O[\x9d\x82\xf3\xd8j\xa6\xd2\xf0\xc6J?\xb7\xd0\xe7\xe3usAK\xf6\x9d\x97u\t\xac\xb4lY\x127\xee\\&_H\x1cu\xbc~b\xdc\xf8||\xb7\xb1\xbaL\x9bf\xb2\xac\n4\x18r\xf42)4ϱq\x1f<\xffG3\x1ec\x85\xc1\x8e\xf1\xa2V\vt\xf4b\xce,\x8dۼzz\xf9`,\x1d\x91\x15\x113q\xd1}\x81\xd3&5m\xeaJk\xf4\x9a\x94i;%\xc3dr\xa7\x12\x13\xbc\xf0\x178\xbb\x12\x108\xf7\xec\xcaf\xaa\xfd˝]\xf1h\x0e\x17\xceݡ\b01\xa3\x97~r%Сw\xf2-\x02\xb59\x0f\xe7\x13\xc3\\\x1aR\x89,l\xe9\xb8\x1c\x92<\xd2ḱ\xed\xa11Z\xe7WN\x9a?I\xb6\\t\xde\xe0$\xd7\xf2u\x0e\xd0D$e\x01W/\xfft\xf9\xfb \xffYT\x8f\x12\xdb\xfd/6\xba\x96\xb0NѺ\x83\xe9\xdd\xf4\xc8~\x9a\xea\xefG\x9aϑ\xe1\xd4331\xbd\x14Wg\x1db\xaa\xde\x19\x8d\xdf6-\r\x96_*o\xb9\xd2OJoF\x9a=\xe3\xac4\xd3G\x91\xed\x95\x14\xb2\xd6~E\xc7\xf6\xf0!s\x87\xc7CG\xfa\xf4ps\x8cM\x1a\xde\xc3^֑s\x193tMȖ\x8d\xe7\xc8\xfa\xac\v4\xec\xf0~\xdd\xff\xc5H\x9f1\x1b\xc1\xfa\x89\x9b\xbd;\xc1\xcf\xf2\xdc:\xe6\x9dc9a\xf2\xfa[<\x86\x82\x17\x81(\x15\b^8\xa9\f\x10\xfaF\xf3K\xe5\x96\xf8\xce\xf6S\xe6\x17\x9a\xd2\xf3j\x97f\xd36\xf9\x8f\xf3^\xf13rh\x97\x1dp\x9a͗MA\x1aR\xb2d\xc7\xf3_g\xa0.ɍM]CLȃM\xcf~M#\x0f\xd0\xdd;\xa99\xaf\xc9Qgj~\xeb\xebd\xb5&\xe6\xb2v2TgA\x9e\x99\xc1\x9aL\xb0\xb4l\xd5\xe4\x1c\xd5N\xe6\xe9<\xb5&2S\xc7\xf3MgA\x8e壦d\x99&᚜[\xdad\x8c\xce\xef\x84<+\xa3\xf4\xe5Ϯ\xbc\xe4:\xc5t~hRVh\xd2Z\xc6<\xceIy\x9fK\xb3=\x93\xa8\xba4\xb3\xb3\xc9ڜ\xe88)\x9f\xf34Wsj(\xb3Y\x9c\xf1\f\xcd)\xb0c\xb9\x9b\ty\x99\x13 \xbb\x19\x9b\x8b݀Yi\x9a\xa90~?V(\xf3\xb6\xb6\xf85$\U000390d6\xaa\xe7\x02\xa7\x84j_\x06M,\xef\x83\xd77\xe6V\xc7\x03_\xe7l\x9f\xe1VG@nvPօ\xe1Uѹ\xa0\xca\xec\xf1\xd8\\\x80\U000cb923\xe3\xdb#A\xfb\xf2\xb5\x11\xe0\x18\xc8~\x80\xc04\xb7\xb2\xe0i[\x95_\xbb-\xf8\xc8\r9t\v\x9d\xb5T3\xf4\xab\x02\x00\xf2\x0f\xfb7\xe6\xeedQȧ3W\"X\xc5\xff\x93\x9e\xa5H[\xb8\xfap\xbb\xa1\xeaA\xb0\xe8I\x8b&\x95\xad\x11\xb3-N\xeb\xe7v\xe0\xe4It\xa1\x8e\xa4\x926\u007fN@\xa4K\xe1\x83\xdb\xe0\xb5r&\xadR\xba\xdd8,\xd7$[L\x1cA\xfak\xbf\xb9\xcaW\x15S\xd1\x1d7\xf0\"\xa1\xafz\x18\x06\xb3<\xb7\xbc4i\xa5N/\xb9\xef\x96\x1e\xcd\xc3}\xf7\xb4\x13{\xac\xfa{\xdcD\xe9\x0e=\x9f\x83\xd3\xf4)\xe2\xd9\xf3ï\x80Ӵ\a\xb4\"*F~\x8a\xe6\xc6\xcdLr\xedo\xf8\xf6W\x18'L\xf2\xbb~\x8b\xb1\x1b\xe6\xfdM\xce\x01\xf6\x84.\xb7\xf2y\xfb\x8dB\xa5F7xq\xf6\x81PX\xe6\x1a\\\xf4\x19\x01\x19\xbb\"\x1e^&sM\x1b\xa9\xd8\x03\xfe,\xdd-\xfe)\xd4\xea\xb7\xe8=\xe4\xe0\x1d\x91\x90\xc7\xea\x05+\xa6\xb9\xfc؆\x00\xdb\xf4\xf6\x93\x8b\xc5-\xb6\xb1y;#\x8b\xc6\x14\t\x83\xbb\xbf\xff\xd9\r\xc8\xf0\x12\xd7\x1fk\x97\xeb`\x95\x8cFK\xe90P\xd7h\x1b\x9f\x8a{\xf9D7sw\xaf\xda\xef\x90.Eؿ\x8d\xb7\xec\xf8\f\x1d&N%3\xc9]\x14\x16\xd3Zf\x9c\xdc\fZ\xef\xa5,\xf6\u05fb\xc7v\xca]\x9cP\x16\xb5\xc6/O\x02\xd5\xd70Q\xf5F\xc4\xee\xd2\xef\x91\xf0/'\r\xa3\xf7\xe8\x1bI\xce͠\xfa\x98S+<\x81\xb4{w ,\\sݼ6qJ\xba\x99\xf9\x1f\x9f\xfb\xe3jy5\xfe\xc0êys\xe2\"\x81\xb2\xee]\x85\x94\xe7D\xdc\x03\f\x19\xabL\xad\xbc?\x95Պ.\xf5\xb5@\xd0\xddy{ރ\"\xed\x03M3\xbcl\x9flj\x97\xf2f\x1f\x88\x1a\xe1_\xf3$H\xf4\x8d\f\xe78\xbb\a\x9cV\x16\xfey\xec\x1c\x9d\at\t\xf2\xdc\xf3)\xb6Ns\x02\xc9\x13\x9a\x1a\x86˓\xefb\xa8\x8f\x1f)Y\xc1g<\xf5ZW\xf0I\xd8A\x9c\x9auwn\x04sZ\x00\x1c{Lir\x88\x87\xa6\x15\x1d\xda\x19\xd1\x16}57\xa8>H1\xa4\x87)\x9a*\xee\x80\xce\x18[\xff\x99\xef\xdc\xealf\xc7\xf4/'5\xa2\x8akRi\xc5\x14\xd6\xe8\x94:\xf9\xa8Q\x1d\xe8%\x88 $ކw\xbf\xd4\xdb\xf6.k\xf8\xdb\xdf/\xdaYɲ\f+\xe3SY\x1d\xe6\xee\xe1\xbaK\xf7\xb2Cx\x97\x8e\xfe̤pQ\xb4\xbe\x86\xff\xf9\xbf\v\xf0\x06\xf8[x|\xce~\xfcG\x00\x00\x00\xff\xff\xd8\xf1\x03\x9c\xe6o\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4VO\x8f\xeb4\x10\xbf\xe7S\x8c\x1e\x87w!\xe9{\xe2\x00\xca\r\x15\x0e+`\xb5\xda>\xed\x05qp\x9di;\xacc\x9b\xf1\xb8K\xf9\xf4\xc8v\xb2m\x93\x94]\x90\xf0-\xf6\xfc\xf9\xcdo\xfed\xaa\xba\xae+\xe5\xe9\t9\x90\xb3-(O\xf8\xa7\xa0M_\xa1y\xfe.4\xe4V\xc7\xcf\xd53ٮ\x85u\f\xe2\xfaG\f.\xb2\xc6\x1fpG\x96\x84\x9c\xadz\x14\xd5)Qm\x05\xa0\xacu\xa2\xd2uH\x9f\x00\xdaYag\fr\xbdG\xdb<\xc7-n#\x99\x0e9\x1b\x1f]\x1f?5\xdf6\x9f*\x00͘տP\x8fAT\xef[\xb0ј\n\xc0\xaa\x1e[\b\xc8II\x94\xc4\xc0\xf8G\xc4 \xa19\xa2Av\r\xb9*x\xd4\xc9\xf1\x9e]\xf4-\x9c\x1f\x8a\xfe\x00\xaa\x04\xb4ɦ6\xd9\xd4c1\x95_\r\x05\xf9\xe9\x96\xc4\xcf4Hy\x13Y\x99e@Y \x1c\x1c\xcb\xfd\xd9i\r!py!\xbb\x8fF\xf1\xa2r\x05\x10\xb4\xf3\xd8B\xd6\xf5JcW\x01\fLe[\xf5\xc0\xc5\xf1s1\xa7\x0fث\xe2\x04\xc0y\xb4\xdf?\xdc=}\xb3\xb9\xba\x06\xe80h&/\x99\xef\x85Ȁ\x02(\x18P\x808PZc\b\xa0#3Z\x81\x82\x12\xc8\xee\x1c\xf79G\xaf\xa6\x01\xd4\xd6E\x019 d\x05\xd9*\x03Ge\"~\r\xcavЫ\x130&/\x10텽,\x12\x1a\xf8\xc51f2[8\x88\xf8ЮV{\x92\xb1\xeb\xb4\xeb\xfbhIN\xab\xdc@\xb4\x8d\xe28\xac:<\xa2Y\x05\xda\u05ca\xf5\x81\x04\xb5Dƕ\xf2Tg\xe86w^\xd3w_\xf1Ч\xe1\xe3\x15V9\xa5\xca\n\xc2d\xf7\x17\x0f\xb9!\xfe!\x03\xa9\x1dJ}\x14\xd5\x12ř\xe8t\x95\xd8y\xfcq\xf3\x05F\xd79\x19S\xf63\xefg\xc5pNA\"\x8c\xec\x0e\xb9$qǮ\xcf6\xd1vޑ-ե\r\xa1\x9d\xd2\x1f\xe2\xb6'\tc\xed\xa6\\5\xb0Σ\b\xb6\b\xd1wJ\xb0k\xe0\xce\xc2Z\xf5h\xd6*\xe0\xff\x9e\x80\xc4t\xa8\x13\xb1\xefK\xc1\xe5\x14\x9d\n\x17\xd6.\x1e\xc61w#_\vݽ\xf1\xa8S\x06\x13\x89I\x9bv\xa4s{\xc0\xce1\xa8%\x95\xe6]H\xb2ƿ\xc42L\x92\x82f2_R\u007f\xbe\x8dfy\x9c䗃\n8\xbd\x9c`zH2S\xff\x86v\xa8O\xda`1Q\xa6\t\xbe\r%\x1d\xb4\xb1\x9f\xfb\xac\xe1\x1e_\x16n\x1fإɚ\xe7\xfa\xf5\xb9Q\x1bP\xfe7{\xb2\xb3p\xa7\x91\x15\xa9\xfc\x0f\xbb\x1c\xd5\x17\x03z0\x04\x1c\xadM};\x9b\x90\x19\xc8t\x92\xcfdH\xb0_@\xb3\x88\xe7\xce\xee\\\xde\x04Tr\xac\xa4\xf4\x13\x0e\xc9\x1e\xfc\x14\\\v\x06o纜\xf9\xf0z\x17\xa1\xe5\xe4?\xe9\u007fSN\xe3\x86\x18\x17}\xd7\x19\xd5\xe2C\xf2\xb8\xc4\xf8r\u007f\r(\xa31jk\xb0\x05\xe18\xd7.\xba\x8aY\x9d\xa6U3\x96\xday\x9fz\xa3\x80f\n\xa9O^\x0ehou\x03\xbc\xa8锿\xf2\f\xdb\xd3-\xd5\xf5\xebr8o\xa9R\xba-\xa4\xd9]\v-p\xf6.R\x16\xb3WJzq\xf3\x98\x11\xb2\xb9\x94\x1dg\xc6Uk\x8c\x8b\xc8<\x86\x9b\x10\x16\x93=\xbb\xcc滋\xf0\x828V\xfb1\xe0\xf3\xe8M\x9b\x9a\x17\xec\xee\xa7+\xee\x87\x0fW\xbbj\xfe\xd4\xcevT6t\xf8\xf5\xb7\xaaX\xc5\xeei\\0\xd3\xe5\xdf\x01\x00\x00\xff\xff\xfb\xb1p\x12\x1b\f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WO\x8f۶\x13\xbd\xfbS\f\x92\xc3^\"9\xc1\xef\xf0+t)\x82M\x0fA\xf3g\x11o\xf7R\xf4@\x93#\x8b]\x8aTgHm\xddO_\f)\xad\xbd\xb6\x9cl\x8aV\x17C\x149|\xf3\u07bc!\xbd\xaa\xaaj\xa5\x06{\x87\xc46\xf8\x06\xd4`\xf1ψ^\u07b8\xbe\xff\x81k\x1b\xd6\xe3\x9bս\xf5\xa6\x81\xeb\xc41\xf4_\x90C\"\x8dﰵ\xdeF\x1b\xfc\xaaǨ\x8c\x8a\xaaY\x01(\xefCT2\xcc\xf2\n\xa0\x83\x8f\x14\x9cC\xaav\xe8\xeb\xfb\xb4\xc5m\xb2\xce \xe5\xe0\xf3\xd6\xe3\xeb\xfa\xff\xf5\xeb\x15\x80&\xcc\xcbom\x8f\x1cU?4\xe0\x93s+\x00\xafzl`\f.\xf5\xc8^\r܅\xe8\x82.\x9b\xd5#:\xa4P۰\xe2\x01\xb5콣\x90\x86\x06\x0e\x1fJ\x88\tW\xc9\xe9.G\xdbL\xd1>L\xd1\xf2\x04g9\xfe\xfc\x95I\x1f,\xc7\xe8\xcdK\x9a,\xcaWO\xb0ƽT\x11G\xb2~w\xf4!\x1b\xe1+\n\x88\aJ!\x94\xa5%\x8b\x03\xd12$\xec|\xf9is\v\xf3\xd6Y\x8cS\xf63\uf1c5|\x90@\b\xb3\xbeE*\"\xb6\x14\xfa\x1c\x13\xbd\x19\x82\xf51\xbfhgџ\xd2\xcfi\xdb\xdb(\xba\xff\x91\x90\xa3hU\xc3u\xeeB\xb0EH\x83Q\x11M\r\xef=\\\xab\x1eݵb\xfc\xcf\x05\x10\xa6\xb9\x12b\x9f'\xc1q\x03=\x9d\\X;6\xd8\xd4\xde.\xe8\xb5\xec\xe4̀\xfa\x89\x81$\x8am\xed\xe4\xec6\xd0\t\xafj\xf6\xf9r\xbc\xfa\xc9\xf4e\x83C\xe9\xfe\xadݝ\x8e\x02(c\xf2١\xdc\xcdŵ_!l!\xef뼓\x14j\x1bH\x10\x8d\xd6 Us\x9e\x13\x92DS\xc2\x16\x9d\xe1\xfa,\xe4\x05\xces*\x84F4V\xee\x1c\xe8S$\x8f\x13\xf3᧬/\x94\x1f\x02\xe4ң~\xea\xb1>\xa27\xb9\xa9\x9f\xa1\t\xb9\x86\x19\r<\xd8\xd8\x15s\xb8\xe3C\xeay*\xc8s\x8f\xfb\xa5\xe1\x13\xec\xb7\x1d\xca\xcc\xd2N\x11\x185a\x14\x1c\x8cN\xcc+ά\x01>&\xce\xf6R\x8b\x11AZ\x845\xf3\xea{ܟ\x13\r\xdf\x12w:\xef\xbf\r\xf9J\xce\xc5\x190a\x8b\x84>.Z\\\xee\x1e\xe41bv\xb9\t\x9a\xc5\xe0\x1a\x87\xc8\xeb0\"\x8d\x16\x1f\xd6\x0f\x81\xee\xad\xdfUBxU\n\x81\xd7\xf9ް~\x99\u007f.\xa4|\xfb\xf9\xdd\xe7\x06\xde\x1a\x03!vH\xa2Z\x9b\xdc\\hG\xa7ݫ\xdcq_A\xb2\xe6ǫ\u007f\xc2K\x18\x8as\x9e\xc1\xcd&W\xff^N\xee\fJ(\xda\x14U\x02\x81\xf4M\x11\xbb\x9f\xd4,\xfda\xa9\x10gL\xdb\x10\x1c\xaa\xf3ғ\xeek\t\xcd9\xa4Jv\xf8\x1e\x9b\xcd\xce\xfd\x86\xc9n\xa6ibx\xc9j^6\x17B\xb9\x97\xe4[\x8a\xda\xe1%\xa3/p\xbc\x9cJ\xf5\xb8\xc1\xb3ZtT1\xf1\xf77\xe9\xbcl\x9a\xb9\x9d\x1a\xb5N$\x05=\xc5\\\xb8\xd0\xfc;\x8dz\xe8\x14/\xb8\xed\x19\xa8od\xe5,\x83\xb3-\xea\xbdvX\x02Bh\x17\xaa\xe9\xbb ˃>\xf5K\xa5\xf5vT֩\xadÅo\xbfxu\xf1\xebE\xf1\x17\xf5<\x1bd\xb9\xb5\x98\x06\"\xa5\x12{\xaa\xb2i䠾\xd2\xd2\\\xd0|:\xfd\xdb\xf1\xe2œ\u007f\x0e\xf9U\a_\xceDn\xe0\xd7\xdfV%*\x9a\xbb\xf9\xa2/\x83\u007f\a\x00\x00\xff\xff\xe4\xf3S\x85\xb2\r\x00\x00"), } diff --git a/go.mod b/go.mod index 7e433f515..5f926abf2 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( google.golang.org/api v0.74.0 google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.6 k8s.io/apiextensions-apiserver v0.24.2 k8s.io/apimachinery v0.25.6 @@ -144,7 +145,6 @@ require ( gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/component-base v0.24.2 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go new file mode 100644 index 000000000..9fe2b5642 --- /dev/null +++ b/internal/resourcepolicies/resource_policies.go @@ -0,0 +1,143 @@ +package resourcepolicies + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" +) + +type VolumeActionType string + +const Skip VolumeActionType = "skip" + +// Action defined as one action for a specific way of backup +type Action struct { + // Type defined specific type of action, currently only support 'skip' + Type VolumeActionType `yaml:"type"` + // Parameters defined map of parameters when executing a specific action + Parameters map[string]interface{} `yaml:"parameters,omitempty"` +} + +// VolumePolicy defined policy to conditions to match Volumes and related action to handle matched Volumes +type VolumePolicy struct { + // Conditions defined list of conditions to match Volumes + Conditions map[string]interface{} `yaml:"conditions"` + Action Action `yaml:"action"` +} + +// currently only support configmap type of resource config +const ConfigmapRefType string = "configmap" + +// resourcePolicies currently defined slice of volume policies to handle backup +type resourcePolicies struct { + Version string `yaml:"version"` + VolumePolicies []VolumePolicy `yaml:"volumePolicies"` + // we may support other resource policies in the future, and they could be added separately + // OtherResourcePolicies: []OtherResourcePolicy +} + +type Policies struct { + Version string + VolumePolicies []volumePolicy + // OtherPolicies +} + +func unmarshalResourcePolicies(YamlData *string) (*resourcePolicies, error) { + resPolicies := &resourcePolicies{} + if err := decodeStruct(strings.NewReader(*YamlData), resPolicies); err != nil { + return nil, fmt.Errorf("failed to decode yaml data into resource policies %v", err) + } else { + return resPolicies, nil + } +} + +func (policies *Policies) buildPolicy(resPolicies *resourcePolicies) error { + for _, vp := range resPolicies.VolumePolicies { + con, err := unmarshalVolConditions(vp.Conditions) + if err != nil { + return errors.Wrap(err, "failed to unmarshl volume conditions") + } + volCap, err := parseCapacity(con.Capacity) + if err != nil { + return errors.Wrapf(err, "failed to parse condition capacity %s", con.Capacity) + } + var p volumePolicy + p.action = vp.Action + p.conditions = append(p.conditions, &capacityCondition{capacity: *volCap}) + p.conditions = append(p.conditions, &storageClassCondition{storageClass: con.StorageClass}) + p.conditions = append(p.conditions, &nfsCondition{nfs: con.NFS}) + p.conditions = append(p.conditions, &csiCondition{csi: con.CSI}) + policies.VolumePolicies = append(policies.VolumePolicies, p) + } + + // Other resource policies + + policies.Version = resPolicies.Version + return nil +} + +func (p *Policies) Match(res interface{}) *Action { + vol, ok := res.(*StructuredVolume) + if ok { + for _, policy := range p.VolumePolicies { + isAllMatch := false + for _, con := range policy.conditions { + if !con.Match(vol) { + isAllMatch = false + break + } + isAllMatch = true + } + if isAllMatch { + return &policy.action + } + } + } + return nil +} + +func (p *Policies) Validate() error { + if p.Version != currentSupportDataVersion { + return fmt.Errorf("incompatible version number %s with supported version %s", p.Version, currentSupportDataVersion) + } + + for _, policy := range p.VolumePolicies { + if err := policy.action.Validate(); err != nil { + return errors.Wrap(err, "failed to validate config") + } + for _, con := range policy.conditions { + if err := con.Validate(); err != nil { + return errors.Wrap(err, "failed to validate conditions config") + } + } + } + return nil +} + +func GetResourcePoliciesFromConfig(cm *v1.ConfigMap) (*Policies, error) { + if cm == nil { + return nil, fmt.Errorf("could not parse config from nil configmap") + } + if len(cm.Data) != 1 { + return nil, fmt.Errorf("illegal resource policies %s/%s configmap", cm.Name, cm.Namespace) + } + + var yamlData string + for _, v := range cm.Data { + yamlData = v + } + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + if err != nil { + return nil, errors.WithStack(err) + } + + policies := &Policies{} + if err := policies.buildPolicy(resPolicies); err != nil { + return nil, errors.WithStack(err) + } + + return policies, nil +} diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go new file mode 100644 index 000000000..83e1c2488 --- /dev/null +++ b/internal/resourcepolicies/resource_policies_test.go @@ -0,0 +1,244 @@ +package resourcepolicies + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLoadResourcePolicies(t *testing.T) { + testCases := []struct { + name string + yamlData string + wantErr bool + }{ + { + name: "unknown key in yaml", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + unknown: {} + storageClass: + - gp2 + - ebs-sc + action: + type: skip`, + wantErr: true, + }, + { + name: "reduplicated key in yaml", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + capacity: "0,100Gi" + storageClass: + - gp2 + - ebs-sc + action: + type: skip`, + wantErr: true, + }, + { + name: "error format of storageClass", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + storageClass: gp2 + action: + type: skip`, + wantErr: true, + }, + { + name: "error format of csi", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + csi: gp2 + action: + type: skip`, + wantErr: true, + }, + { + name: "error format of nfs", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + csi: {} + nfs: abc + action: + type: skip`, + wantErr: true, + }, + { + name: "supported formart volume policies", + yamlData: `version: v1 + volumePolicies: + - conditions: + capacity: "0,100Gi" + csi: + driver: aws.efs.csi.driver + nfs: {} + storageClass: + - gp2 + - ebs-sc + action: + type: skip`, + wantErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := unmarshalResourcePolicies(&tc.yamlData) + + if (err != nil) != tc.wantErr { + t.Fatalf("Expected error %v, but got error %v", tc.wantErr, err) + } + }) + } +} + +func TestGetResourceMatchedAction(t *testing.T) { + resPolicies := &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + { + Action: Action{Type: "volume-snapshot"}, + Conditions: map[string]interface{}{ + "capacity": "10,100Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + { + Action: Action{Type: "file-system-backup"}, + Conditions: map[string]interface{}{ + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + } + testCases := []struct { + name string + volume *StructuredVolume + expectedAction *Action + }{ + { + name: "match policy", + volume: &StructuredVolume{ + capacity: *resource.NewQuantity(5<<30, resource.BinarySI), + storageClass: "ebs-sc", + csi: &csiVolumeSource{Driver: "aws.efs.csi.driver"}, + }, + expectedAction: &Action{Type: "skip"}, + }, + { + name: "both matches return the first policy", + volume: &StructuredVolume{ + capacity: *resource.NewQuantity(50<<30, resource.BinarySI), + storageClass: "ebs-sc", + csi: &csiVolumeSource{Driver: "aws.efs.csi.driver"}, + }, + expectedAction: &Action{Type: "volume-snapshot"}, + }, + { + name: "dismatch all policies", + volume: &StructuredVolume{ + capacity: *resource.NewQuantity(50<<30, resource.BinarySI), + storageClass: "ebs-sc", + nfs: &nFSVolumeSource{}, + }, + expectedAction: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + policies := &Policies{} + err := policies.buildPolicy(resPolicies) + if err != nil { + t.Errorf("Failed to build policy with error %v", err) + } + + action := policies.Match(tc.volume) + if action == nil { + if tc.expectedAction != nil { + t.Errorf("Expected action %v, but got result nil", tc.expectedAction.Type) + } + } else { + if tc.expectedAction != nil { + if action.Type != tc.expectedAction.Type { + t.Errorf("Expected action %v, but got result %v", tc.expectedAction.Type, action.Type) + } + } else { + t.Errorf("Expected action nil, but got result %v", action.Type) + } + } + }) + } +} + +func TestGetResourcePoliciesFromConfig(t *testing.T) { + // Create a test ConfigMap + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": "version: v1\nvolumePolicies:\n- conditions:\n capacity: '0,10Gi'\n action:\n type: skip", + }, + } + + // Call the function and check for errors + resPolicies, err := GetResourcePoliciesFromConfig(cm) + assert.Nil(t, err) + + // Check that the returned resourcePolicies object contains the expected data + assert.Equal(t, "v1", resPolicies.Version) + assert.Len(t, resPolicies.VolumePolicies, 1) + policies := resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + }, + Action: Action{ + Type: Skip, + }, + }, + }, + } + p := &Policies{} + err = p.buildPolicy(&policies) + if err != nil { + t.Fatalf("failed to build policy with error %v", err) + } + assert.Equal(t, p, resPolicies) +} diff --git a/internal/resourcepolicies/volume_resources.go b/internal/resourcepolicies/volume_resources.go new file mode 100644 index 000000000..0f8fae0a1 --- /dev/null +++ b/internal/resourcepolicies/volume_resources.go @@ -0,0 +1,201 @@ +package resourcepolicies + +import ( + "bytes" + "fmt" + "strings" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" + corev1api "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type volumePolicy struct { + action Action + conditions []VolumeCondition +} + +type VolumeCondition interface { + Match(v *StructuredVolume) bool + Validate() error +} + +// Capacity consist of the lower and upper boundary +type Capacity struct { + lower resource.Quantity + upper resource.Quantity +} + +type StructuredVolume struct { + capacity resource.Quantity + storageClass string + nfs *nFSVolumeSource + csi *csiVolumeSource +} + +func (s *StructuredVolume) ParsePV(pv *corev1api.PersistentVolume) { + s.capacity = *pv.Spec.Capacity.Storage() + s.storageClass = pv.Spec.StorageClassName + nfs := pv.Spec.NFS + if nfs != nil { + s.nfs = &nFSVolumeSource{Server: nfs.Server, Path: nfs.Path} + } + + csi := pv.Spec.CSI + if csi != nil { + s.csi = &csiVolumeSource{Driver: csi.Driver} + } +} + +func (s *StructuredVolume) ParsePodVolume(vol *corev1api.Volume) { + nfs := vol.NFS + if nfs != nil { + s.nfs = &nFSVolumeSource{Server: nfs.Server, Path: nfs.Path} + } + + csi := vol.CSI + if csi != nil { + s.csi = &csiVolumeSource{Driver: csi.Driver} + } +} + +type capacityCondition struct { + capacity Capacity +} + +func (c *capacityCondition) Match(v *StructuredVolume) bool { + return c.capacity.isInRange(v.capacity) +} + +type storageClassCondition struct { + storageClass []string +} + +func (s *storageClassCondition) Match(v *StructuredVolume) bool { + if len(s.storageClass) == 0 { + return true + } + + if v.storageClass == "" { + return false + } + + for _, sc := range s.storageClass { + if v.storageClass == sc { + return true + } + } + + return false +} + +type nfsCondition struct { + nfs *nFSVolumeSource +} + +func (c *nfsCondition) Match(v *StructuredVolume) bool { + if c.nfs == nil { + return true + } + if v.nfs == nil { + return false + } + + if c.nfs.Path == "" { + if c.nfs.Server == "" { + return true + } + if c.nfs.Server != v.nfs.Server { + return false + } + return true + } + if c.nfs.Path != v.nfs.Path { + return false + } + if c.nfs.Server == "" { + return true + } + if c.nfs.Server != v.nfs.Server { + return false + } + return true + +} + +type csiCondition struct { + csi *csiVolumeSource +} + +func (c *csiCondition) Match(v *StructuredVolume) bool { + if c.csi == nil { + return true + } + + if v.csi == nil { + return false + } + + return c.csi.Driver == v.csi.Driver +} + +// parseCapacity parse string into capacity format +func parseCapacity(capacity string) (*Capacity, error) { + if capacity == "" { + capacity = "," + } + capacities := strings.Split(capacity, ",") + var quantities []resource.Quantity + if len(capacities) != 2 { + return nil, fmt.Errorf("wrong format of Capacity %v", capacity) + } else { + for _, v := range capacities { + if strings.TrimSpace(v) == "" { + // case similar "10Gi," + // if empty, the quantity will assigned with 0 + quantities = append(quantities, *resource.NewQuantity(int64(0), resource.DecimalSI)) + } else { + if quantity, err := resource.ParseQuantity(strings.TrimSpace(v)); err != nil { + return nil, fmt.Errorf("wrong format of Capacity %v with err %v", v, err) + } else { + quantities = append(quantities, quantity) + } + } + } + } + return &Capacity{lower: quantities[0], upper: quantities[1]}, nil +} + +// isInRange returns true if the quantity y is in range of capacity, or it returns false +func (c *Capacity) isInRange(y resource.Quantity) bool { + if c.lower.IsZero() && c.upper.Cmp(y) >= 0 { + // [0, a] y + return true + } + if c.upper.IsZero() && c.lower.Cmp(y) <= 0 { + // [b, 0] y + return true + } + if !c.lower.IsZero() && !c.upper.IsZero() { + // [a, b] y + return c.lower.Cmp(y) <= 0 && c.upper.Cmp(y) >= 0 + } + return false +} + +// unmarshalVolConditions parse map[string]interface{} into volumeConditions format +// and validate key fields of the map. +func unmarshalVolConditions(con map[string]interface{}) (*volumeConditions, error) { + volConditons := &volumeConditions{} + buffer := new(bytes.Buffer) + err := yaml.NewEncoder(buffer).Encode(con) + if err != nil { + return nil, errors.Wrap(err, "failed to encode volume conditions") + } + + if err := decodeStruct(buffer, volConditons); err != nil { + return nil, errors.Wrap(err, "failed to decode volume conditions") + } + return volConditons, nil +} diff --git a/internal/resourcepolicies/volume_resources_test.go b/internal/resourcepolicies/volume_resources_test.go new file mode 100644 index 000000000..c3e967ba2 --- /dev/null +++ b/internal/resourcepolicies/volume_resources_test.go @@ -0,0 +1,412 @@ +package resourcepolicies + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + corev1api "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func setStructuredVolume(capacity resource.Quantity, sc string, nfs *nFSVolumeSource, csi *csiVolumeSource) *StructuredVolume { + return &StructuredVolume{ + capacity: capacity, + storageClass: sc, + nfs: nfs, + csi: csi, + } +} + +func TestParseCapacity(t *testing.T) { + var emptyCapacity Capacity + tests := []struct { + input string + expected Capacity + expectedErr error + }{ + {"10Gi,20Gi", Capacity{lower: *resource.NewQuantity(10<<30, resource.BinarySI), upper: *resource.NewQuantity(20<<30, resource.BinarySI)}, nil}, + {"10Gi,", Capacity{lower: *resource.NewQuantity(10<<30, resource.BinarySI), upper: *resource.NewQuantity(0, resource.DecimalSI)}, nil}, + {"10Gi", emptyCapacity, fmt.Errorf("wrong format of Capacity 10Gi")}, + {"", emptyCapacity, nil}, + } + + for _, test := range tests { + test := test // capture range variable + t.Run(test.input, func(t *testing.T) { + actual, actualErr := parseCapacity(test.input) + if test.expected != emptyCapacity { + assert.Equal(t, test.expected.lower.Cmp(actual.lower), 0) + assert.Equal(t, test.expected.upper.Cmp(actual.upper), 0) + } + assert.Equal(t, test.expectedErr, actualErr) + }) + } +} + +func TestCapacityIsInRange(t *testing.T) { + t.Parallel() + + tests := []struct { + capacity *Capacity + quantity resource.Quantity + isInRange bool + }{ + {&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(10<<30, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), true}, + {&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(10<<30, resource.BinarySI)}, *resource.NewQuantity(15<<30, resource.BinarySI), false}, + {&Capacity{*resource.NewQuantity(20<<30, resource.BinarySI), *resource.NewQuantity(0, resource.DecimalSI)}, *resource.NewQuantity(25<<30, resource.BinarySI), true}, + {&Capacity{*resource.NewQuantity(20<<30, resource.BinarySI), *resource.NewQuantity(0, resource.DecimalSI)}, *resource.NewQuantity(15<<30, resource.BinarySI), false}, + {&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(15<<30, resource.BinarySI), true}, + {&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), false}, + {&Capacity{*resource.NewQuantity(10<<30, resource.BinarySI), *resource.NewQuantity(20<<30, resource.BinarySI)}, *resource.NewQuantity(25<<30, resource.BinarySI), false}, + {&Capacity{*resource.NewQuantity(0, resource.BinarySI), *resource.NewQuantity(0, resource.BinarySI)}, *resource.NewQuantity(5<<30, resource.BinarySI), true}, + } + + for _, test := range tests { + test := test // capture range variable + t.Run(fmt.Sprintf("%v with %v", test.capacity, test.quantity), func(t *testing.T) { + t.Parallel() + + actual := test.capacity.isInRange(test.quantity) + + assert.Equal(t, test.isInRange, actual) + + }) + } +} + +func TestStorageClassConditionMatch(t *testing.T) { + tests := []struct { + name string + condition *storageClassCondition + volume *StructuredVolume + expectedMatch bool + }{ + { + name: "match single storage class", + condition: &storageClassCondition{[]string{"gp2"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "gp2", nil, nil), + expectedMatch: true, + }, + { + name: "match multiple storage classes", + condition: &storageClassCondition{[]string{"gp2", "ebs-sc"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "gp2", nil, nil), + expectedMatch: true, + }, + { + name: "mismatch storage class", + condition: &storageClassCondition{[]string{"gp2"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "ebs-sc", nil, nil), + expectedMatch: false, + }, + { + name: "empty storage class", + condition: &storageClassCondition{[]string{}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "ebs-sc", nil, nil), + expectedMatch: true, + }, + { + name: "empty volume storage class", + condition: &storageClassCondition{[]string{"gp2"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, nil), + expectedMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + match := tt.condition.Match(tt.volume) + if match != tt.expectedMatch { + t.Errorf("expected %v, but got %v", tt.expectedMatch, match) + } + }) + } +} + +func TestNFSConditionMatch(t *testing.T) { + + tests := []struct { + name string + condition *nfsCondition + volume *StructuredVolume + expectedMatch bool + }{ + { + name: "match nfs condition", + condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil), + expectedMatch: true, + }, + { + name: "empty nfs condition", + condition: &nfsCondition{nil}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil), + expectedMatch: true, + }, + { + name: "empty nfs server and path condition", + condition: &nfsCondition{&nFSVolumeSource{Server: "", Path: ""}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20"}, nil), + expectedMatch: true, + }, + { + name: "server dismatch", + condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20", Path: ""}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: ""}, nil), + expectedMatch: false, + }, + { + name: "empty nfs server condition", + condition: &nfsCondition{&nFSVolumeSource{Path: "/mnt/data"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", &nFSVolumeSource{Server: "192.168.10.20", Path: "/mnt/data"}, nil), + expectedMatch: true, + }, + { + name: "empty nfs volume", + condition: &nfsCondition{&nFSVolumeSource{Server: "192.168.10.20"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, nil), + expectedMatch: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + match := tt.condition.Match(tt.volume) + if match != tt.expectedMatch { + t.Errorf("expected %v, but got %v", tt.expectedMatch, match) + } + }) + } +} + +func TestCSIConditionMatch(t *testing.T) { + + tests := []struct { + name string + condition *csiCondition + volume *StructuredVolume + expectedMatch bool + }{ + { + name: "match csi condition", + condition: &csiCondition{&csiVolumeSource{Driver: "test"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{Driver: "test"}), + expectedMatch: true, + }, + { + name: "empty csi condition", + condition: &csiCondition{nil}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{Driver: "test"}), + expectedMatch: true, + }, + { + name: "empty csi volume", + condition: &csiCondition{&csiVolumeSource{Driver: "test"}}, + volume: setStructuredVolume(*resource.NewQuantity(0, resource.BinarySI), "", nil, &csiVolumeSource{}), + expectedMatch: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + match := tt.condition.Match(tt.volume) + if match != tt.expectedMatch { + t.Errorf("expected %v, but got %v", tt.expectedMatch, match) + } + }) + } +} + +func TestUnmarshalVolumeConditions(t *testing.T) { + testCases := []struct { + name string + input map[string]interface{} + expectedError string + }{ + { + name: "Valid input", + input: map[string]interface{}{ + "capacity": "1Gi,10Gi", + "storageClass": []string{ + "gp2", + "ebs-sc", + }, + "csi": &csiVolumeSource{ + Driver: "aws.efs.csi.driver", + }, + }, + expectedError: "", + }, + { + name: "Invalid input: invalid capacity filed name", + input: map[string]interface{}{ + "Capacity": "1Gi,10Gi", + }, + expectedError: "field Capacity not found", + }, + { + name: "Invalid input: invalid storage class format", + input: map[string]interface{}{ + "storageClass": "ebs-sc", + }, + expectedError: "str `ebs-sc` into []string", + }, + { + name: "Invalid input: invalid csi format", + input: map[string]interface{}{ + "csi": "csi.driver", + }, + expectedError: "str `csi.driver` into resourcepolicies.csiVolumeSource", + }, + { + name: "Invalid input: unknown field", + input: map[string]interface{}{ + "unknown": "foo", + }, + expectedError: "field unknown not found in type", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := unmarshalVolConditions(tc.input) + if tc.expectedError != "" { + if err == nil { + t.Errorf("Expected error '%s', but got nil", tc.expectedError) + } else if !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("Expected error '%s', but got '%v'", tc.expectedError, err) + } + } + }) + } +} + +func TestParsePodVolume(t *testing.T) { + // Mock data + nfsVolume := corev1api.Volume{} + nfsVolume.NFS = &corev1api.NFSVolumeSource{ + Server: "nfs.example.com", + Path: "/exports/data", + } + csiVolume := corev1api.Volume{} + csiVolume.CSI = &corev1api.CSIVolumeSource{ + Driver: "csi.example.com", + } + emptyVolume := corev1api.Volume{} + + // Test cases + testCases := []struct { + name string + inputVolume *corev1api.Volume + expectedNFS *nFSVolumeSource + expectedCSI *csiVolumeSource + }{ + { + name: "NFS volume", + inputVolume: &nfsVolume, + expectedNFS: &nFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"}, + }, + { + name: "CSI volume", + inputVolume: &csiVolume, + expectedCSI: &csiVolumeSource{Driver: "csi.example.com"}, + }, + { + name: "Empty volume", + inputVolume: &emptyVolume, + expectedNFS: nil, + expectedCSI: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Call the function + structuredVolume := &StructuredVolume{} + structuredVolume.ParsePodVolume(tc.inputVolume) + + // Check the results + if tc.expectedNFS != nil { + if structuredVolume.nfs == nil { + t.Errorf("Expected a non-nil NFS volume source") + } else if *tc.expectedNFS != *structuredVolume.nfs { + t.Errorf("NFS volume source does not match expected value") + } + } + if tc.expectedCSI != nil { + if structuredVolume.csi == nil { + t.Errorf("Expected a non-nil CSI volume source") + } else if *tc.expectedCSI != *structuredVolume.csi { + t.Errorf("CSI volume source does not match expected value") + } + } + }) + } +} + +func TestParsePV(t *testing.T) { + // Mock data + nfsVolume := corev1api.PersistentVolume{} + nfsVolume.Spec.Capacity = corev1api.ResourceList{corev1api.ResourceStorage: resource.MustParse("1Gi")} + nfsVolume.Spec.NFS = &corev1api.NFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"} + csiVolume := corev1api.PersistentVolume{} + csiVolume.Spec.Capacity = corev1api.ResourceList{corev1api.ResourceStorage: resource.MustParse("2Gi")} + csiVolume.Spec.CSI = &corev1api.CSIPersistentVolumeSource{Driver: "csi.example.com"} + emptyVolume := corev1api.PersistentVolume{} + + // Test cases + testCases := []struct { + name string + inputVolume *corev1api.PersistentVolume + expectedNFS *nFSVolumeSource + expectedCSI *csiVolumeSource + }{ + { + name: "NFS volume", + inputVolume: &nfsVolume, + expectedNFS: &nFSVolumeSource{Server: "nfs.example.com", Path: "/exports/data"}, + expectedCSI: nil, + }, + { + name: "CSI volume", + inputVolume: &csiVolume, + expectedNFS: nil, + expectedCSI: &csiVolumeSource{Driver: "csi.example.com"}, + }, + { + name: "Empty volume", + inputVolume: &emptyVolume, + expectedNFS: nil, + expectedCSI: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Call the function + structuredVolume := &StructuredVolume{} + structuredVolume.ParsePV(tc.inputVolume) + // Check the results + if structuredVolume.capacity != *tc.inputVolume.Spec.Capacity.Storage() { + t.Errorf("Capacity does not match expected value") + } + if structuredVolume.storageClass != tc.inputVolume.Spec.StorageClassName { + t.Errorf("Storage class does not match expected value") + } + if tc.expectedNFS != nil { + if structuredVolume.nfs == nil { + t.Errorf("Expected a non-nil NFS volume source") + } else if *tc.expectedNFS != *structuredVolume.nfs { + t.Errorf("NFS volume source does not match expected value") + } + } + if tc.expectedCSI != nil { + if structuredVolume.csi == nil { + t.Errorf("Expected a non-nil CSI volume source") + } else if *tc.expectedCSI != *structuredVolume.csi { + t.Errorf("CSI volume source does not match expected value") + } + } + }) + } +} diff --git a/internal/resourcepolicies/volume_resources_validator.go b/internal/resourcepolicies/volume_resources_validator.go new file mode 100644 index 000000000..a9b5f6e3a --- /dev/null +++ b/internal/resourcepolicies/volume_resources_validator.go @@ -0,0 +1,76 @@ +package resourcepolicies + +import ( + "fmt" + "io" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +const currentSupportDataVersion = "v1" + +type csiVolumeSource struct { + Driver string `yaml:"driver,omitempty"` +} + +type nFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `yaml:"server,omitempty"` + // Path is the exported NFS share + Path string `yaml:"path,omitempty"` +} + +// volumeConditions defined the current format of conditions we parsed +type volumeConditions struct { + Capacity string `yaml:"capacity,omitempty"` + StorageClass []string `yaml:"storageClass,omitempty"` + NFS *nFSVolumeSource `yaml:"nfs,omitempty"` + CSI *csiVolumeSource `yaml:"csi,omitempty"` +} + +func (c *capacityCondition) Validate() error { + // [0, a] + // [a, b] + // [b, 0] + // ==> low <= upper or upper is zero + if (c.capacity.upper.Cmp(c.capacity.lower) >= 0) || + (!c.capacity.lower.IsZero() && c.capacity.upper.IsZero()) { + return nil + } + return errors.Errorf("illegal values for capacity %v", c.capacity) + +} + +func (s *storageClassCondition) Validate() error { + // validate by yamlv3 + return nil +} + +func (c *nfsCondition) Validate() error { + // validate by yamlv3 + return nil +} + +func (c *csiCondition) Validate() error { + // validate by yamlv3 + return nil +} + +// decodeStruct restric validate the keys in decoded mappings to exist as fields in the struct being decoded into +func decodeStruct(r io.Reader, s interface{}) error { + dec := yaml.NewDecoder(r) + dec.KnownFields(true) + return dec.Decode(s) +} + +// Validate check action format +func (a *Action) Validate() error { + // Validate Type + if a.Type != Skip { + return fmt.Errorf("invalid action type %s", a.Type) + } + + // TODO validate parameters + return nil +} diff --git a/internal/resourcepolicies/volume_resources_validator_test.go b/internal/resourcepolicies/volume_resources_validator_test.go new file mode 100644 index 000000000..a18e48513 --- /dev/null +++ b/internal/resourcepolicies/volume_resources_validator_test.go @@ -0,0 +1,251 @@ +package resourcepolicies + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestCapacityConditionValidate(t *testing.T) { + testCases := []struct { + name string + capacity *Capacity + wantErr bool + }{ + { + name: "lower and upper are both zero", + capacity: &Capacity{lower: *resource.NewQuantity(0, resource.DecimalSI), upper: *resource.NewQuantity(0, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower is zero and upper is greater than zero", + capacity: &Capacity{lower: *resource.NewQuantity(0, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower is greater than upper", + capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(50, resource.DecimalSI)}, + wantErr: true, + }, + { + name: "lower and upper are equal", + capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower is greater than zero and upper is zero", + capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(0, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower and upper are both not zero and lower is less than upper", + capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(200, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower and upper are both not zero and lower is equal to upper", + capacity: &Capacity{lower: *resource.NewQuantity(100, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)}, + wantErr: false, + }, + { + name: "lower and upper are both not zero and lower is greater than upper", + capacity: &Capacity{lower: *resource.NewQuantity(200, resource.DecimalSI), upper: *resource.NewQuantity(100, resource.DecimalSI)}, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := &capacityCondition{capacity: *tc.capacity} + err := c.Validate() + + if (err != nil) != tc.wantErr { + t.Fatalf("Expected error %v, but got error %v", tc.wantErr, err) + } + }) + } +} + +func TestValidate(t *testing.T) { + testCases := []struct { + name string + res *resourcePolicies + wantErr bool + }{ + { + name: "unknown key in yaml", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "unknown": "", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "error format of capacity", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "10Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "error format of storageClass", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "storageClass": "ebs-sc", + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "error format of csi", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": "aws.efs.csi.driver", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "unsupported version", + res: &resourcePolicies{ + Version: "v2", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "unsupported action", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "unsupported"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "error format of nfs", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "nfs": "aws.efs.csi.driver", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "supported formart volume policies", + res: &resourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: "skip"}, + Conditions: map[string]interface{}{ + "capacity": "0,10Gi", + "storageClass": []string{"gp2", "ebs-sc"}, + "csi": interface{}( + map[string]interface{}{ + "driver": "aws.efs.csi.driver", + }), + "nfs": interface{}( + map[string]interface{}{ + "server": "192.168.20.90", + "path": "/mnt/data/", + }), + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + policies := &Policies{} + err1 := policies.buildPolicy(tc.res) + err2 := policies.Validate() + + if tc.wantErr { + if err1 == nil && err2 == nil { + t.Fatalf("Expected error %v, but not get error", tc.wantErr) + } + } else { + if err1 != nil || err2 != nil { + t.Fatalf("Expected error %v, but got error %v %v", tc.wantErr, err1, err2) + } + } + }) + } +} diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index 71c9814ba..190b5a953 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -159,6 +160,9 @@ type BackupSpec struct { // The default value is 1 hour. // +optional ItemOperationTimeout metav1.Duration `json:"itemOperationTimeout,omitempty"` + // ResourcePolicies specifies the referenced resource policies that backup should follow + // +optional + ResourcePolicies *v1.TypedLocalObjectReference `json:"resourcePolices,omitempty"` } // BackupHooks contains custom behaviors that should be executed at different phases of the backup. diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index 3ca121526..803adb2d6 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -371,6 +371,11 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { } out.CSISnapshotTimeout = in.CSISnapshotTimeout out.ItemOperationTimeout = in.ItemOperationTimeout + if in.ResourcePolicies != nil { + in, out := &in.ResourcePolicies, &out.ResourcePolicies + *out = new(corev1.TypedLocalObjectReference) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec. diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 1fcbdc13e..44b6ff3fc 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -290,6 +290,7 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, backupRequest: backupRequest, tarWriter: tw, dynamicFactory: kb.dynamicFactory, + kbClient: kb.kbClient, discoveryHelper: kb.discoveryHelper, podVolumeBackupper: podVolumeBackupper, podVolumeSnapshotTracker: newPVCSnapshotTracker(), @@ -581,6 +582,7 @@ func (kb *kubernetesBackupper) FinalizeBackup(log logrus.FieldLogger, backupRequest: backupRequest, tarWriter: tw, dynamicFactory: kb.dynamicFactory, + kbClient: kb.kbClient, discoveryHelper: kb.discoveryHelper, itemHookHandler: &hook.NoOpItemHookHandler{}, } diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index be96afc8b..d41f9506d 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -40,6 +40,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" @@ -2882,7 +2883,7 @@ type fakePodVolumeBackupper struct{} // BackupPodVolumes returns one pod volume backup per entry in volumes, with namespace "velero" // and name "pvb---". -func (b *fakePodVolumeBackupper) BackupPodVolumes(backup *velerov1.Backup, pod *corev1.Pod, volumes []string, _ logrus.FieldLogger) ([]*velerov1.PodVolumeBackup, []error) { +func (b *fakePodVolumeBackupper) BackupPodVolumes(backup *velerov1.Backup, pod *corev1.Pod, volumes []string, _ *resourcepolicies.Policies, _ logrus.FieldLogger) ([]*velerov1.PodVolumeBackup, []error) { var res []*velerov1.PodVolumeBackup anno := pod.GetAnnotations() diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index b220ace3b..fd22ea02c 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -18,6 +18,7 @@ package backup import ( "archive/tar" + "context" "encoding/json" "fmt" "strings" @@ -36,7 +37,10 @@ import ( kubeerrs "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" + kbClient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/vmware-tanzu/velero/internal/hook" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/archive" "github.com/vmware-tanzu/velero/pkg/client" @@ -61,6 +65,7 @@ type itemBackupper struct { backupRequest *Request tarWriter tarWriter dynamicFactory client.DynamicFactory + kbClient kbClient.Client discoveryHelper discovery.Helper podVolumeBackupper podvolume.Backupper podVolumeSnapshotTracker *pvcSnapshotTracker @@ -222,6 +227,7 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti } return false, itemFiles, kubeerrs.NewAggregate(backupErrs) } + itemFiles = append(itemFiles, additionalItemFiles...) obj = updatedObj if metadata, err = meta.Accessor(obj); err != nil { @@ -300,7 +306,7 @@ func (ib *itemBackupper) backupPodVolumes(log logrus.FieldLogger, pod *corev1api return nil, nil } - return ib.podVolumeBackupper.BackupPodVolumes(ib.backupRequest.Backup, pod, volumes, log) + return ib.podVolumeBackupper.BackupPodVolumes(ib.backupRequest.Backup, pod, volumes, ib.backupRequest.ResPolicies, log) } func (ib *itemBackupper) executeActions( @@ -318,7 +324,15 @@ func (ib *itemBackupper) executeActions( } log.Info("Executing custom action") + if act, err := ib.checkResourcePolicies(obj, &groupResource, action.Name()); err != nil { + return nil, itemFiles, errors.WithStack(err) + } else if act != nil && act.Type == resourcepolicies.Skip { + log.Infof("skip snapshot of pvc %s/%s bound pv for the matched resource policies", namespace, name) + continue + } + updatedItem, additionalItemIdentifiers, operationID, postOperationItems, err := action.Execute(obj, ib.backupRequest.Backup) + if err != nil { return nil, itemFiles, errors.Wrapf(err, "error executing custom action (groupResource=%s, namespace=%s, name=%s)", groupResource.String(), namespace, name) } @@ -469,6 +483,16 @@ func (ib *itemBackupper) takePVSnapshot(obj runtime.Unstructured, log logrus.Fie return nil } + if ib.backupRequest.ResPolicies != nil { + structuredVolume := &resourcepolicies.StructuredVolume{} + structuredVolume.ParsePV(pv) + action := ib.backupRequest.ResPolicies.Match(structuredVolume) + if action != nil && action.Type == resourcepolicies.Skip { + log.Infof("skip snapshot of pv %s for the matched resource policies", pv.Name) + return nil + } + } + // TODO: -- once failure-domain.beta.kubernetes.io/zone is no longer // supported in any velero-supported version of Kubernetes, remove fallback checking of it pvFailureDomainZone, labelFound := pv.Labels[zoneLabel] @@ -555,6 +579,30 @@ func (ib *itemBackupper) takePVSnapshot(obj runtime.Unstructured, log logrus.Fie return kubeerrs.NewAggregate(errs) } +func (ib *itemBackupper) checkResourcePolicies(obj runtime.Unstructured, groupResource *schema.GroupResource, actionName string) (*resourcepolicies.Action, error) { + if ib.backupRequest.ResPolicies != nil && groupResource.String() == "persistentvolumeclaims" && actionName == "velero.io/csi-pvc-backupper" { + pvc := corev1api.PersistentVolumeClaim{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil { + return nil, errors.WithStack(err) + } + + pvName := pvc.Spec.VolumeName + if pvName == "" { + return nil, errors.Errorf("PVC has no volume backing this claim") + } + + pv := &corev1api.PersistentVolume{} + if err := ib.kbClient.Get(context.Background(), kbClient.ObjectKey{Name: pvName}, pv); err != nil { + return nil, errors.WithStack(err) + } + + volume := resourcepolicies.StructuredVolume{} + volume.ParsePV(pv) + return ib.backupRequest.ResPolicies.Match(&volume), nil + } + return nil, nil +} + func volumeSnapshot(backup *velerov1api.Backup, volumeName, volumeID, volumeType, az, location string, iops *int64) *volume.Snapshot { return &volume.Snapshot{ Spec: volume.SnapshotSpec{ diff --git a/pkg/backup/request.go b/pkg/backup/request.go index 8b4f739e8..49b562756 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -23,6 +23,7 @@ import ( snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/vmware-tanzu/velero/internal/hook" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/itemoperation" "github.com/vmware-tanzu/velero/pkg/plugin/framework" @@ -52,6 +53,7 @@ type Request struct { BackedUpItems map[itemKey]struct{} CSISnapshots []snapshotv1api.VolumeSnapshot itemOperationsList *[]*itemoperation.BackupOperation + ResPolicies *resourcepolicies.Policies } // GetItemOperationsList returns ItemOperationsList, initializing it if necessary diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index af38f895d..d42eb7e50 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -20,8 +20,10 @@ import ( "fmt" "time" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/sirupsen/logrus" @@ -123,6 +125,10 @@ func (b *BackupBuilder) FromSchedule(schedule *velerov1api.Schedule) *BackupBuil }) } + if schedule.Spec.Template.ResourcePolicies != nil { + b.ResourcePolicies(schedule.Spec.Template.ResourcePolicies.Name) + } + return b } @@ -275,3 +281,9 @@ func (b *BackupBuilder) ItemOperationTimeout(timeout time.Duration) *BackupBuild b.object.Spec.ItemOperationTimeout.Duration = timeout return b } + +// resourcePolicies sets the Backup's resource polices. +func (b *BackupBuilder) ResourcePolicies(name string) *BackupBuilder { + b.object.Spec.ResourcePolicies = &v1.TypedLocalObjectReference{Kind: resourcepolicies.ConfigmapRefType, Name: name} + return b +} diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index c09f98e64..bb87ed748 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -104,6 +104,7 @@ type CreateOptions struct { OrderedResources string CSISnapshotTimeout time.Duration ItemOperationTimeout time.Duration + ResPoliciesConfigmap string client veleroclient.Interface } @@ -143,6 +144,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { f = flags.VarPF(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", "", "Use pod volume file system backup by default for volumes") f.NoOptDefVal = "true" + + flags.StringVar(&o.ResPoliciesConfigmap, "resource-policies-configmap", "", "Reference to the resource policies configmap that backup using") } // BindWait binds the wait flag separately so it is not called by other create @@ -374,6 +377,9 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro if o.DefaultVolumesToFsBackup.Value != nil { backupBuilder.DefaultVolumesToFsBackup(*o.DefaultVolumesToFsBackup.Value) } + if o.ResPoliciesConfigmap != "" { + backupBuilder.ResourcePolicies(o.ResPoliciesConfigmap) + } } backup := backupBuilder.ObjectMeta(builder.WithLabelsMap(o.Labels.Data())).Result() diff --git a/pkg/cmd/cli/schedule/create.go b/pkg/cmd/cli/schedule/create.go index 609e74470..1538f093a 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -23,8 +23,10 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" @@ -158,6 +160,10 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { }, } + if o.BackupOptions.ResPoliciesConfigmap != "" { + schedule.Spec.Template.ResourcePolicies = &v1.TypedLocalObjectReference{Kind: resourcepolicies.ConfigmapRefType, Name: o.BackupOptions.ResPoliciesConfigmap} + } + if printed, err := output.PrintWithFormat(c, schedule); printed || err != nil { return err } diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 71fcb55e0..cc532683c 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -43,6 +43,7 @@ import ( kbclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/storage" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" @@ -459,6 +460,21 @@ func (b *backupReconciler) prepareBackupRequest(backup *velerov1api.Backup, logg request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified")) } + if request.Spec.ResourcePolicies != nil && request.Spec.ResourcePolicies.Kind == resourcepolicies.ConfigmapRefType { + policiesConfigmap := &v1.ConfigMap{} + err := b.kbClient.Get(context.Background(), kbclient.ObjectKey{Namespace: request.Namespace, Name: request.Spec.ResourcePolicies.Name}, policiesConfigmap) + if err != nil { + request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("failed to get resource policies %s/%s configmap with err %v", request.Namespace, request.Spec.ResourcePolicies.Name, err)) + } + res, err := resourcepolicies.GetResourcePoliciesFromConfig(policiesConfigmap) + if err != nil { + request.Status.ValidationErrors = append(request.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("resource policies %s/%s", request.Namespace, request.Spec.ResourcePolicies.Name)).Error()) + } else if err = res.Validate(); err != nil { + request.Status.ValidationErrors = append(request.Status.ValidationErrors, errors.Wrapf(err, fmt.Sprintf("resource policies %s/%s", request.Namespace, request.Spec.ResourcePolicies.Name)).Error()) + } + request.ResPolicies = res + } + return request } diff --git a/pkg/controller/schedule_controller.go b/pkg/controller/schedule_controller.go index 86a0979b4..577d942e1 100644 --- a/pkg/controller/schedule_controller.go +++ b/pkg/controller/schedule_controller.go @@ -258,10 +258,8 @@ func getNextRunTime(schedule *velerov1.Schedule, cronSchedule cron.Schedule, asO func getBackup(item *velerov1.Schedule, timestamp time.Time) *velerov1.Backup { name := item.TimestampedName(timestamp) - backup := builder. + return builder. ForBackup(item.Namespace, name). FromSchedule(item). Result() - - return backup } diff --git a/pkg/podvolume/backupper.go b/pkg/podvolume/backupper.go index 3bd8e4866..31332f046 100644 --- a/pkg/podvolume/backupper.go +++ b/pkg/podvolume/backupper.go @@ -29,6 +29,7 @@ import ( corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" "github.com/vmware-tanzu/velero/pkg/label" @@ -41,7 +42,7 @@ import ( // Backupper can execute pod volume backups of volumes in a pod. type Backupper interface { // BackupPodVolumes backs up all specified volumes in a pod. - BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) + BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, resPolicies *resourcepolicies.Policies, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) } type backupper struct { @@ -110,7 +111,23 @@ func resultsKey(ns, name string) string { return fmt.Sprintf("%s/%s", ns, name) } -func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) { +func (b *backupper) checkResourcePolicies(resPolicies *resourcepolicies.Policies, pvc *corev1api.PersistentVolumeClaim, volume *corev1api.Volume) (*resourcepolicies.Action, error) { + structuredVolume := &resourcepolicies.StructuredVolume{} + if pvc != nil { + pv, err := b.pvClient.PersistentVolumes().Get(context.TODO(), pvc.Spec.VolumeName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrapf(err, "error getting pv for pvc %s", pvc.Spec.VolumeName) + } + structuredVolume.ParsePV(pv) + } else if volume != nil { + structuredVolume.ParsePodVolume(volume) + } else { + return nil, errors.Errorf("failed to check resource policies for empty volume") + } + return resPolicies.Match(structuredVolume), nil +} + +func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.Pod, volumesToBackup []string, resPolicies *resourcepolicies.Policies, log logrus.FieldLogger) ([]*velerov1api.PodVolumeBackup, []error) { if len(volumesToBackup) == 0 { return nil, nil } @@ -201,6 +218,16 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api. continue } + if resPolicies != nil { + if action, err := b.checkResourcePolicies(resPolicies, pvc, &volume); err != nil { + errs = append(errs, errors.Wrapf(err, "error getting pv for pvc %s", pvc.Spec.VolumeName)) + continue + } else if action != nil && action.Type == resourcepolicies.Skip { + log.Infof("skip backup of volume %s for the matched resource policies", volumeName) + continue + } + } + volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, b.uploaderType, pvc) if volumeBackup, err = b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil { errs = append(errs, err)