From 4d49b5971c167f2a275efe15d5ac7f287edf41f0 Mon Sep 17 00:00:00 2001 From: Samuel Lucidi Date: Tue, 24 Mar 2020 17:50:48 -0400 Subject: [PATCH] Support setting a custom CA bundle to use with a BackupStorageLocation (#2353) * Support setting a custom CA certificate for a BSL Signed-off-by: Sam Lucidi * update CRDS Signed-off-by: Sam Lucidi * Add changelog for #2353 Signed-off-by: Sam Lucidi * Clean up temp file from TestTempCACertFile Signed-off-by: Sam Lucidi --- changelogs/unreleased/2353-mansam | 1 + pkg/apis/velero/v1/backup_storage_location.go | 4 ++ pkg/apis/velero/v1/zz_generated.deepcopy.go | 7 ++- .../pod_volume_backup_controller.go | 24 ++++++++-- .../pod_volume_restore_controller.go | 20 ++++++++- pkg/generated/crds/crds.go | 2 +- .../velero.io_backupstoragelocations.yaml | 5 +++ pkg/persistence/object_store.go | 1 + pkg/plugin/framework/validation.go | 2 +- pkg/plugin/generated/RestoreItemAction.pb.go | 10 +++-- pkg/restic/command.go | 8 ++++ pkg/restic/common.go | 38 ++++++++++++++++ pkg/restic/common_test.go | 44 +++++++++++++++++++ pkg/restic/exec_commands.go | 3 +- pkg/restic/repository_manager.go | 16 +++++++ 15 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/2353-mansam diff --git a/changelogs/unreleased/2353-mansam b/changelogs/unreleased/2353-mansam new file mode 100644 index 000000000..8539c9884 --- /dev/null +++ b/changelogs/unreleased/2353-mansam @@ -0,0 +1 @@ +support setting a custom CA certificate on a BSL to use when verifying TLS connections diff --git a/pkg/apis/velero/v1/backup_storage_location.go b/pkg/apis/velero/v1/backup_storage_location.go index 332e90b27..3d74b647e 100644 --- a/pkg/apis/velero/v1/backup_storage_location.go +++ b/pkg/apis/velero/v1/backup_storage_location.go @@ -64,6 +64,10 @@ type ObjectStorageLocation struct { // Prefix is the path inside a bucket to use for Velero storage. Optional. // +optional Prefix string `json:"prefix,omitempty"` + + // CACert defines a CA bundle to use when verifying TLS connections to the provider. + // +optional + CACert []byte `json:"caCert,omitempty"` } // BackupStorageLocationSpec defines the specification for a Velero BackupStorageLocation. diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index d79863e15..e7f1aeaec 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -623,6 +623,11 @@ func (in *ExecHook) DeepCopy() *ExecHook { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectStorageLocation) DeepCopyInto(out *ObjectStorageLocation) { *out = *in + if in.CACert != nil { + in, out := &in.CACert, &out.CACert + *out = make([]byte, len(*in)) + copy(*out, *in) + } return } @@ -1330,7 +1335,7 @@ func (in *StorageType) DeepCopyInto(out *StorageType) { if in.ObjectStorage != nil { in, out := &in.ObjectStorage, &out.ObjectStorage *out = new(ObjectStorageLocation) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/pkg/controller/pod_volume_backup_controller.go b/pkg/controller/pod_volume_backup_controller.go index b6f3efe12..ae57546bd 100644 --- a/pkg/controller/pod_volume_backup_controller.go +++ b/pkg/controller/pod_volume_backup_controller.go @@ -212,21 +212,37 @@ func (c *podVolumeBackupController) processBackup(req *velerov1api.PodVolumeBack log.WithField("path", path).Debugf("Found path matching glob") // temp creds - file, err := restic.TempCredentialsFile(c.secretLister, req.Namespace, req.Spec.Pod.Namespace, c.fileSystem) + credentialsFile, err := restic.TempCredentialsFile(c.secretLister, req.Namespace, req.Spec.Pod.Namespace, c.fileSystem) if err != nil { log.WithError(err).Error("Error creating temp restic credentials file") return c.fail(req, errors.Wrap(err, "error creating temp restic credentials file").Error(), log) } // ignore error since there's nothing we can do and it's a temp file. - defer os.Remove(file) + defer os.Remove(credentialsFile) resticCmd := restic.BackupCommand( req.Spec.RepoIdentifier, - file, + credentialsFile, path, req.Spec.Tags, ) + // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic + caCert, err := restic.GetCACert(c.backupLocationLister, req.Namespace, req.Spec.BackupStorageLocation) + if err != nil { + log.WithError(err).Error("Error getting caCert") + } + var caCertFile string + if caCert != nil { + caCertFile, err = restic.TempCACertFile(caCert, req.Spec.BackupStorageLocation, c.fileSystem) + if err != nil { + log.WithError(err).Error("Error creating temp cacert file") + } + // ignore error since there's nothing we can do and it's a temp file. + defer os.Remove(caCertFile) + } + resticCmd.CACertFile = caCertFile + // Running restic command might need additional provider specific environment variables. Based on the provider, we // set resticCmd.Env appropriately (currently for Azure and S3 based backuplocations) var env []string @@ -272,7 +288,7 @@ func (c *podVolumeBackupController) processBackup(req *velerov1api.PodVolumeBack var snapshotID string if !emptySnapshot { - snapshotID, err = restic.GetSnapshotID(req.Spec.RepoIdentifier, file, req.Spec.Tags, env) + snapshotID, err = restic.GetSnapshotID(req.Spec.RepoIdentifier, credentialsFile, req.Spec.Tags, env, caCertFile) if err != nil { log.WithError(err).Error("Error getting SnapshotID") return c.fail(req, errors.Wrap(err, "error getting snapshot id").Error(), log) diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 6e72d5b08..37f5b6060 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -293,8 +293,23 @@ func (c *podVolumeRestoreController) processRestore(req *velerov1api.PodVolumeRe // ignore error since there's nothing we can do and it's a temp file. defer os.Remove(credsFile) + // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic + caCert, err := restic.GetCACert(c.backupLocationLister, req.Namespace, req.Spec.BackupStorageLocation) + if err != nil { + log.WithError(err).Error("Error getting caCert") + } + var caCertFile string + if caCert != nil { + caCertFile, err = restic.TempCACertFile(caCert, req.Spec.BackupStorageLocation, c.fileSystem) + if err != nil { + log.WithError(err).Error("Error creating temp cacert file") + } + // ignore error since there's nothing we can do and it's a temp file. + defer os.Remove(caCertFile) + } + // execute the restore process - if err := c.restorePodVolume(req, credsFile, volumeDir, log); err != nil { + if err := c.restorePodVolume(req, credsFile, caCertFile, volumeDir, log); err != nil { log.WithError(err).Error("Error restoring volume") return c.failRestore(req, errors.Wrap(err, "error restoring volume").Error(), log) } @@ -313,7 +328,7 @@ func (c *podVolumeRestoreController) processRestore(req *velerov1api.PodVolumeRe return nil } -func (c *podVolumeRestoreController) restorePodVolume(req *velerov1api.PodVolumeRestore, credsFile, volumeDir string, log logrus.FieldLogger) error { +func (c *podVolumeRestoreController) restorePodVolume(req *velerov1api.PodVolumeRestore, credsFile, caCertFile, volumeDir string, log logrus.FieldLogger) error { // Get the full path of the new volume's directory as mounted in the daemonset pod, which // will look like: /host_pods//volumes// volumePath, err := singlePathMatch(fmt.Sprintf("/host_pods/%s/volumes/*/%s", string(req.Spec.Pod.UID), volumeDir)) @@ -327,6 +342,7 @@ func (c *podVolumeRestoreController) restorePodVolume(req *velerov1api.PodVolume req.Spec.SnapshotID, volumePath, ) + resticCmd.CACertFile = caCertFile // Running restic command might need additional provider specific environment variables. Based on the provider, we // set resticCmd.Env appropriately (currently for Azure and S3 based backuplocations) diff --git a/pkg/generated/crds/crds.go b/pkg/generated/crds/crds.go index da7383a99..f7a96b1f7 100644 --- a/pkg/generated/crds/crds.go +++ b/pkg/generated/crds/crds.go @@ -30,7 +30,7 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xecej/\xf9\xbfj\xc8\x06\xac\xa2%\x05\xb3\x18d\x1f\x1b\x97\x16\xb5d\u0089\xac\xc2+bD\xc1N\xa0ѭ\x01\x95lA\xa3!f\r\u007fV\x1a\x81˝\xba\x86\x83\xb5\xa5\xb9~\xfbv\xcfm\xb4\xa5L\x15E%\xb9=\xbd%\x8b\xe0\xdb\xca*m\xde\xe6xD\xf1\xd6\xf0\xfd\x8a\xe9\xec\xc0-fNxoY\xc9W\x84\xb8$SZ\x17\xf9\xffD\xa9\x9b7-L\xed\xc9)\x99\xb1\x9a\xcb}\xddM\xaa>\xc9w\xa7\xf3^\x9d\xfc4\x8f\u007f\xc3^\xd7\xe5\xb8\xf2\xd3\xed\xfdC[ո\xe9\xf2\x9c\xb8\xddL3\r\xe3\x1d\xa3\xb8ܡ\xf6\x82\xdbiU\x10D\x94\xb9\xd75RS\xc1Qv\x99n\xaam\xc1\xad\x93\xf4?+4N\x9d\xd5\x1anȣ\xc0\x16\xa1*s\xa7\x85k\xd8H\xb8a\x05\x8a\x1bf\xf0\x17g\xbb\xe3\xb0Y9\x96.3\xbe\xed\b\xbb\x03=\xb7\xea\xee\xe8\xb2F%\xe4-\xfe\xbeĬc\x18n\x0e\xdf\xf1\x8c\xd4\x1fvJ7\x0e\xc1\xfb\xa4u\v\xe0\x98Q\xba\x86/\x99\xa8r\xcck\xb7\xd4\xfb\xbd\x87\xca\xed`8\xf9sƥ\xd3\x1f\xe7A\x9d\xed\xc9\xe6W\xf2HLc\x0f(\x80\x93!\x97\x1e\x1a\xf9\x9a\x03\x8e\xa0M\xa6g\xb1\x18`5\xc1\xf0\x00\xbb\x12\x82m\x05^\x83\xd5U\u007fi?\x8fi\xcdN\xa3\x9c\x88\xfbY\x1a#\xea\xd1\xc1\x82\x04\xcf\xc8\xd3\xd6vB\xbc\xf8\r\xb1\xe1\xa0\xd4\xd3<\xe9\u007fr#\x1a;\x87\x8c\xc2\x00\xd8\xe2\x81\x1d\xb9ҁ\xd8\xe0l\xb7\b\xf8\x82Yei\xbf\xeb6f!\xe7\xbb\x1dj\a\xa5<0\x83\xc6;\xf7)\x16L)\xb1kzJl\x03\xfc\x1b\x911\x8d\x9e\xde)\x94\xe1\xf9\x80\x92\x90\x19r\xd77\xb7\x13˜\x1fy^1\x01\\\x1a\xcbd\xe6\xe9`5N}:`Z\x9c\x03l\xbd\xf1G\x9c\x1d\xef;\x8e@I\x04\xa5\xa1p>o8Ԍ\u0087Ir\xb7\xcc`\x0eʫ\xa1\xae\x04\x9a\xb0PN\xfe\xa5\xb1\xeb\xab\t\xc0\xb5\x14\xfc\x0e)\xd8\x16\x05\x18\x14\x98Y\xa5\xc7\xd80/Tߖ}\xd4\x04\xefF\xbcUp\x9a\xc1\x85\xb6\x1d\x95\x9a\x84\t\xf0|\xe0\xd9\xc1o^N_\b\n\xe4\n\r\xd9/+Kq\x1a'\x0e\xe6%\xedی\t7m֘\xfb\xb0\x86fݴE?״\x05\x8f\xd7\xe5e-\xfa\xdf\x0f+\xa3\xe3>[17\x83\x89\xaf\xa9\x98\x8e\x89\xdc\x05\xa0\x9b\x1d`Q\xda\xd3\x15p\x1b{]\x8c\xcb\xe8\xf84ɞz\xedߜ \xce\xd5\xe9M\u007f\xde+\xea\xf4ϔB\xbd\xf4oF\b\xe4\xec\uf0efO\x14\xc0\xc7\xf6\x9c+\xe0\xbbZ\x00\xf9\x15츰\xa8{\x92\x98#W\xcdK\xe2\xe7\xb2`y\xa7r\xad`6;ܾ\xb8\xe8\xc84Y\x8f$n\xf4\xa7\xfa\x982F\xd5\xdd\xcdt\x16*Б\x89k,\xfcA\xec\x818\xd8\xf4P\xe4\xf3\xfe\xd3\a̧\x99\x02)\x1a6 \xe1}\x0f\xcd\xf6\xb2!DN# \x04)\xf5\xe9\xc2\x1f\xaa\xaf\x80\xc1\x13\x9e|t\xe1\x8e\xf8%j\xe6\x96q\x83\x17!j\xa4\x93=)\xd4\x13\x9e\bH8\xac/\xccM\x13\xbdoOxZ\x1e\xd4c\x9bÆ\x9b\x90|p\xfcs\x1d\xc4\x00:饲\f(\xd5\x12=\xcc\x12Q\x90\xea\"b\x8b\xdc>\x9b\xbcZLMv\xc0\v\xf2\x8d\xf1Bq\xda~\xe0e\x12\x81\xceu\x82A\xb2\x89\x98jyd\x82\xe7\xf52^\xbf7\xf2\n>)\xbb\x91S\xc1j\xb7ݾp\x132\\\x1f\x14\x9aO\xcaRϫ3ѣ|6\v\xfd42!\xe9ݰ\xa3\xbf\x9d\xb1YTb\xdf6\xfe\x84U\x8b\x84\x1b\xd8Hw\x86\xf0\xbc\xf297\xbf\u061c\xb7ﶢ2\x94\x92\x91J\xaeh\xb3[\x8f\xad\x13X\x9c\xa8\xc8m)\fѪ\x97\xf4\xcb%A|p\xfb\x82\x9f\xed\xf3\x87\x82e\x98C^\x11\x13)\xff\xc5,\xeey\x06\x05\xea\xfd\xf4F\xd0n\xa5\xf3\xd9)\xcb'\xf9R\xdf\xceҧ\x94\xad9\xb6\xe0\x8c\xf3%4V\xce6\x17\xc7D\xd1.\f\x1cMxM\x0f\\\xa2\x836I\x8a\x1b\x16\xb8\xc9\xf2\x9c\xae#\x98\xb8K\xf6\xdeɜ\x1f\xee\xdb\x1e%\xbf\xc7\x15\xact\xd6\xf9o\xb7U\x91\xd2\xfe\aJ\xc6\xf5\xa2\x85\xbe\xa7{\x05\x81\x9d\x99!+\xd4^\xc4\xc1\xe7\x06\x9c4\x8fL\xf4Ӧ#d)\xe75P\xf8mX\xed\x06\x91\xc6\x15<\x1f\x94\xf1\xbb⎣ȁ\xcfEZ\xae]>\xe1\xe9\xf2j`\xe3\x97\x1by\xe9\xb7\xe7\x81\xc5ƽ|\x01\xb0\x92\xe2\x04\x974\xf3\xf2\xf3C\x97$\xadK\x18D\x97Li\xc1\xac;\xcd\xc5]\xdcM\xabo*\\(:\x8dm\x82Ε\xca\xd8D$\ue531>C\xd7\t\x1eGrC\xf3g\x9a\x90\x13\x02\xb6\xf3\xb7CJ\xc7{\x00\xe7\xc8z\xa9J'%\x83\xa3\t\xce\x01\xc4<\x80dB\xc0ec\xa3\xde?^\xfa\xcb\x01Z\x82e\x14\x16\xcc@t\xaaPj\x95\xa11s\xea\xb0\xe8y\x17\x12nu\xb2\x8d\xf9C\x85O\xb5\xcf%\xf7bK\r\x1b\x1dk\xce\n\xb3o_Z9@g\xda\xee\xffy5;\x0f#\xa0\x9bڢ`rq\xb3\x18 w\xe3\xe7ES\b`|Ȯ\xf7\x15\x99qj\xa4\x17\x94\xe6\xcbn\xb0\x05\x97\x1b\x02\x0e\xef^u;\x86\xe8\x12\xf1\xfc\x90\xfa&\xcel\xd8\\wx\xdb,\xd50\xe5>֞\x0f\xa8\xb1#\xa9af\x98\xc29\xa9l\xebx\x9e\xc6h\x8f\xc7\x1b\x03;\xae\x8dm#i\xa0\x9a\xb5\xda\x16\x90\xb3\xce(\xf2V\xeb\xcf8\xa2\xfc\xe8\xe7\xb5\x12@\a\xf5\x1c\xef\xd3\xa0=\xa0\x8e\x85\xcf+\xaa\xcb\x1eʹ\xb9\xfao\xdc|]\xa8\xe7\x94?*\xaf/\xbf\x9c-\xddK(\xc4\xdb*%\x90\xc91\xfaSk3G\x8aJ\xba5\x89uaG,JTq\x89\x01\xf5\xb1\xdeۅ\x99\xed\n\x06&D\xbb6\x85\xe9\x86)_\xa8^q\xb1\xf4c\xa1\xe0c\xbels\x9aC\xbdо\xcb\"\xdd)1\xfc\xc2\x1c\x9a\xad˘\xae\xc6\b7\x19h\xd9\xf1ݺ\xfb\x8bU\xa16\x03\x9e\xb9=\f\b\xa0\xa2Iwd\x91\xfbvqdԩPd\xdf\xe7\x1c(\r\x92\x8b\xabѺ\x98\xfa\xfdA\x9b\x9d\xf0c\xe9\x0fEg\xd9\xdb\\h\x9fR\xbb\xf1\xd9\x15\x1bݚ\x8cQ'{\xdeeGj\tizMF\xb7\xe6bb\x93I\xa8\xc48\xbb\xd2b\xf9\xbc5[U\xf1\x19\xb5\x14\xb1NbnÝ\xa9\xa0H\x889\x96\xab%>\xabF\x82.\xf3f\xb0>\xab2\xa2U\xf50\x032\xad\x1e\"\x81%K\xb5\x0fgW<\xf4\xab\ff\x88X\xaas\x98\xaea\x98\x01:ZݐR\xb90\x03\xb3\xaeix\xc5z\x85\x85*\x85ש$\xfc\xb9\xb1\xe7T\xcd\xc1B\xa5\xc1Bd:\x87\xd5B-Az\x05\xc1\x02\u007f>\xb3Z\xa0\xae\a\x18]\xf3\xdc\x1a\x81n\x15\xc0(\xc8\xc4ʀ\x89\xbb\xffQ\x90\t\xf5\x00\v7\xfe\xa3`g7\xc6\x19\x8d\x98\xfc\xc9HV\x9a\x83\xb2\x8f\xf4\xa4q>\x8a\xbc\xef\x8e\x1d9\\\xb8\x18\x87=!dBUy\r{H\n=T<\xc1\xdd#9yz\n\x935\x0f\x81\x82+\x8f\xc1O\xff\x9d\xd0w\xafy\xd80Vi\xb6Ǐ*k\xbdG\x9d\xa2\xbf;\xb6\xf3x0\b5\x1e\xe9c\x1d\x04\x8b\xafغS\xc7bǐe\xf3\xf1a\xeb\xf4\xe50\x1c\xca{\xd2\xf2\xac\x15\xb3D<<|\xf4\x88[^\xe0\xfaC\xe5\x0fr\xab\x92i\x83\x8e\u007f\x91 ?i\xeb\xfe<\xa8\xe7\x01\xc2B\x05J\xbf\xeb㫑rxtZL\xc6ڿ\xa8\x8d\n\x16\xd94\xaf\x8e\x8f\xe3sZ\xb1hK(\xfe`\xa3vS\xb3\x06\x04\xb6\x9e\xfb\xbah\xdfW\xb4\xbc\xd6\x13\xb5q\xe7<\xfeD\xd22[\x99\xa5G\x924(>y\x0e\x19\xdfJ\xd3\v3\x0f\xc0+\xe3\xd9\xef$Cz\xab\xf3\x0e}N&7\xc3\xf1\xf4\xe0X\xe7\x1e)J\xab\xb1\xa8\xe4\xcf\xcc\xd4\t\xb4\x11\x8f\xd6\x00\xf3\xf3(\x18p\xb00\a<\xa2\x04%)_F/\xb8\xfck\xf8\xfe\x9c\xe1\xf9\xb5\x05#\xa4\xe3\xaaR(\x96G\xcb\r\xa8\xc5G\xd4\x0f\xe4\x8f\xf4\x11\xf5\x1b3\t\xb12!92B~_\xb3vJ\x17\xcc^C\xce,\xaeF\x00&\xf8\xb1\x11\x95\xa2\xe4\xf1\xc2\xd3M\x1a⭃\xf2Τ\x12B\x84\xc4s\x81ư}|\xb3\xf9\xec\xdc\xd1\x1e\xa5\xdb\xe4F\xb2D!\x14k\x12\x97\xdd\xf7\x8b\xfeD\xc72\xebο\x1e\xb5p\x84m\x8dz3\xb49\xa1\xf6\xee\x84M\x03û\xea\xe0\x9f\xc7\x1d\t\x97\x16\xf7\xd8\r\x8f\xf0\xa5\xe4zٗ\xdf\xd6\xc3\x1cG\xe8\xe8N\x16\xde|f\x00\x05\xdfs\xe7\x10\x9d`\xf7Lo\xd9\x1eW\x99\x12\xee\x1cŕ\xecc\xf4\xcbȕ\x9e\x87\xce\x12r\xe7FԷ<-\x9b\xc7(\x94\xf1\xfdr\xec6`\x05\x9f\xb0\xef\xea}\x82\x1f\xf3\xc7\xfa\x9b\r\x83\x01\x1by\xa7\xd5\xdeEN\x83\x9fn\xa2u\x0f~\xb9c\xdar&\xc4Ƀ\x9fXu\xd0\xfd\x01\x9d}M8\xd4\x11\x06\x1a˴M\xf3]\xf7\x9d\xa1\vn\x8b\xe0b\xbe\x86{,\x993\x92\x812\xd3U\xf5M\xff+\x1eW.\xb4\x8d\x9f\xac\xf0_\x1c\xc8\x0eL\xee\xe9M+h\xa4\xdd\xde?]\x19@\xec\xf8\xa1\x8e\xdf\xe9\xa2\xfe븜\xe6#\x1e\xb7\xcb\xce\xe7\xb17\xb8\x97\u007ftn\xa8\x81\x17]\xc67|7ܖ\xcbR\xf0\xcca\xfb\xed\x17\xca+\x1e\x87\x1f\xf4\x18\x92\x1b>\xe8\x11\xcc2荗C\x04\x90\xeeҺ\xc1\x91yo\xad;(\x0f\x0f\x943\xd1Q3)\xe2d\x95e\x02dUlQ\x93\b\xe2\x80\x01\x03\xe3\xd7N\"\xa8p\a6\x19\x0e%\x13R;\x87s\b\xa9'M\x11b\xaa,Ccv\x95\x10Óo\x1dm\xbc\"U\xcfL\xbb\x10s\xde\x00\xfe\x1a\x06\x8d\xec\xbfa\xfe\xeb\xee\xc0\xad\r8\xe2\xf7+m\xc1#Ql\xaf\xab\xf9\xd8ѻ\xe6?b\xdf*|\xdc\xe8\xe8+)\xc8\xe1\xe5-\xeb\f\xa8\x84\x9e&4fY\x86Nw?\xf5\xbfstyI\xff\xc4O\x19ѿ\x99\x92>\xbfa\xae\xe1o\u007f\xbf\x80p\xc2z\x8cx\xb8\xce\xff\x06\x00\x00\xff\xff\xd1v\x1cU\x18J\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcXQs\xdb6\f~\xf7\xaf\xc0e\x0fy\x99\xe5\xb6\xdb\xc3Noi\xba\xdd\xf5\x96\xb6\xb9\xb8\xcd\x1e\xb6ݕ&!\x9b\vEj\x04\xe9\xd4\xfb\xf5;P\x94lɲ\x93ޭ\xe3\x1bI\x10\xfc\x00|\x00!\xcd\xe6\xf3\xf9L4\xfa\x1e=igK\x10\x8d\xc6/\x01-Ϩx\xf8\x89\n\xed\x16ۗ+\f\xe2\xe5\xecA[U\xc2u\xa4\xe0\xea;$\x17\xbd\xc47Xi\xab\x83vvVc\x10J\x04Q\xce\x00\x84\xb5.\b^&\x9e\x02Hg\x83wƠ\x9f\xaf\xd1\x16\x0fq\x85\xab\xa8\x8dB\x9fn\xe8\xee߾(^\x15?\xce\x00\xa4\xc7t\xfc\xa3\xae\x91\x82\xa8\x9b\x12l4f\x06`E\x8d%\xac\x84|\x88\r\x05\xe7\xc5\x1a\x8d\x93\xed]\xc5\x16\rzWh7\xa3\x06%_\xbd\xf6.6%\xec7Z\r\x19Vk\xd2\xeb\xa4l\xd9*\xbb\xc9\xcaҾ\xd1\x14~=-s\xa3)$\xb9\xc6D/\xcc)XI\x84\xb4]G#\xfc\t\xa1\x19@\xe3\x91\xd0o\xf1\x93}\xb0\xee\xd1\xfe\xa2\xd1(*\xa1\x12\x86p\x06@\xd25X\xc2{F\xdf\b\x89j\x06\xb0\x15F\xabt\xbe\xb5\xc75h\xafn\xdf\xde\xff\xb0\x94\x1b\xacE\xbb\b\xa0\x90\xa4\xd7M\x92\x9b\xb6\x044\x81\x80\x0e\fX\xb3\x9b\xdc\xf8\xcd\xeb0\xbe`2`\x1b\xb2\x0f\x87\x92\xfd\xab\x97QtɄ\x81\xeb\x1e\x81E\xa6\xb3\xf0c^\x01GT:k9J\xc1\x81\xe8\xed\xb9\xa4q\xf0FGO%\x18\x8fU\x94\x0f\x18\x8e\xd7ǬKb\xecɔG\xed,8\x88\x84ɷ\xe7\x01<\x11\xb3\xd4~T\xfa˓(n\x93X\x87\xa2\x11a\x03ڒV\bb\x02\xd3D-\xeaF\xcf\xf2\x0fM˶\xafD\xcci\xa6=\x1ee\xea<\xc3x.\x87\xba\x10\x9e\xa5\xcfm\x16\xea\xed\xee\xe6\xa9/\x19W\xb5i\xfe\x1eY1e\xc1|\xc8\xe9\xc1Nw\xe9\x93\xefJ\x10!\xd2W\xbe,\xe9L\x16\\儐\xd1{\xb4!+\x04W\r߆\xaeA\xfc֯\xcb\xc5\xc1\xf3\xc2-\x8b\x85h#\xa1j\xabE\x01\u007fXx\xc3\r\x88\xe4Ơd\xe4\xdc\v\xd0\x11\x9d\xac{\xe4\xc3\aڒ\x02p6Y\x9b\x1eWn\xf1\xda~%m=jc\xb8\xeb\xf0X\xbbmj\xb9\x87\x83[\x04\x8ff\a\x82\xdd\x03\xdbWŋ\xe2\xe2\u007f~\xba\x8c\xa0\xc0o\x11\xaa;\xdc\xeaq\xbb}\xec͛#\xf9\x8e\xd5\xfdkÓ\xcf]\x1f\xb3\xf0Y\xec\xf3\x91\xf9\x956\xdcuM\xa4\xc0\xfe[\xa2\xed\xad)@\xd05\xa6\xd9\xeb\xe5\xcd%\xa5OB\xee\x18\x8f\x94>r\xf8(\x01\xe4\x0e\xdc\xe5F1R@?\x11\xec>V\x9a\xc0:0ή\a)Ҏ\xdc6\x82\xf3\xd0R\xc7yP\xc8\x1d\x1fw\xbar#\xec\x1a\xf7\x9f\x02\x19\xfb\x01J&\xc61\xd2!;\xf6l\xd0v\x9a\nψ!\u007f\xf1\x9e\x8d\xdf\xcd@\xb4\v\xdd\xd0\xc3=\xea\x1cKs\xf8Q\xfbl_\x8f\xa4+\xe7k\x11J`G\xce\xf9\xaa\xff\xa4\ai6\x82\xce\x1b|\xcb\x12\x9d\x9d\x87%\xa9\xa7\xea\x93\x05\bN\xa6\xe1\xd5V\xe8\x84\xfah\xe7\x93\x15'\xf6N\xd82Q\x8bGK\xfb\xff\x1c/\xf7\xb3T\x13\xe7\xf9\xbfF\xda\x00H\xff\x01ԁ#sV\xe5\x95}\x81\xe7\n\xda\x04T\xef\xc7\xff4..\x06?&\xd2T:\xdbvvT\xc2\xef\u007f\xceZ\xad\xa8\xee;\x1c\xbc\xf8o\x00\x00\x00\xff\xff\xa4&^\xf2\x13\x12\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcX\xcdr\xdb6\x10\xbe\xeb)v܃/\x15\x95\xa4=txs\x94v&S'\xf1X\x89{h;\x13\bXJ\xa8A\x80\xc5\x02rԧ\xef,\bR\"E\xd9\xceLS\xde\b,\x17\xdf~\xfb\v\xce\xe6\xf3\xf9L4\xfa\x0e=igK\x10\x8d\xc6/\x01-\xbfQq\xff\x13\x15\xda-v/\xd7\x18\xc4\xcbٽ\xb6\xaa\x84e\xa4\xe0\xea[$\x17\xbd\xc47Xi\xab\x83vvVc\x10J\x04Q\xce\x00\x84\xb5.\b^&~\x05\x90\xce\x06\xef\x8cA?ߠ-\xee\xe3\x1a\xd7Q\x1b\x85>\x9dН\xbf{Q\xbc*~\x9c\x01H\x8f\xe9\xf3\x8f\xbaF\n\xa2nJ\xb0ј\x19\x80\x155\x96\xb0\x16\xf2>6\x14\x9c\x17\x1b4N\xb6g\x15;4\xe8]\xa1\u074c\x1a\x94|\xf4ƻؔp\xd8h5dX\xadI\xaf\x93\xb2U\xab\xec:+K\xfbFS\xf8\xf5\xbc̵\xa6\x90\xe4\x1a\x13\xbd0\xe7`%\x11\xd2v\x13\x8d\xf0g\x84f\x00\x8dGB\xbf\xc3O\xf6\u07ba\a\xfb\x8bF\xa3\xa8\x84J\x18\xc2\x19\x00I\xd7`\t\xef\x19}#$\xaa\x19\xc0N\x18\xad\xd2\xf7\xad=\xaeA{u\xf3\xf6\ue1d5\xdcb-\xdaE\x00\x85$\xbdn\x92ܴ%\xa0\t\x04t`\xe0a\x8b\x1e\xe1.\x91\x06\x8c\x14)\xc3\xce\x1a\x01\xdc\xfa/\x94\x81\x8a\xbc\xd0xנ\x0f\xbac\x96\x9f\xa3\xc8\xea\xd7F`.\x19m+\x03\x8ac\t\t\xc2\x16!G\x04*\xa0d\t\xb8\n\xc2V\x13xL4\xd9ppR\x8f\xa8\x02a3\xae\x02VL\xa5'\xa0\xad\x8bFq\x00\xee\xd0\a\xf0(\xdd\xc6\xea\u007fz\xcd\x04\xc1\xa5#\x8d\b\x98\xdd\xd9=\xda\x06\xf4V\x18\xe69\xe2\xf7 \xac\x82Z\xec\xc1#\x9f\x01\xd1\x1eiK\"T\xc0;\xe7\x11\xb4\xad\\\t\xdb\x10\x1a*\x17\x8b\x8d\x0e].IW\xd7\xd1\xea\xb0_\xa4\x8c\xd0\xeb\x18\x9c\xa7\x85\xc2\x1d\x9a\x05\xe9\xcd\\x\xb9\xd5\x01e\x88\x1e\x17\xa2\xd1\xf3\x04ܶ\xe1]\xab\xef|N<\xba\xa3\x03\xf1L\x94\xb6\x15\xfa\xd6q\x95wu҈V5Nې^\xa4\xd1h\x87\xa4S\\\xd7:\xb0\xa7\xff\x8eH\x81\xfdS\xc02U\x14X#\xc4F\x89\x80\xaa\x80\xb7\x16\x96\xa2F\xb3\x14\x84ߜvf\x98\xe6L\xe9\xd3\xc4\x1f\x17¡`\xcbV\xbf\xdcըI\x0fM\xa6\xe9\xaaA9\xc8\x13V\xa1+\x9dӶr\x1eDN\xdb\x01\xa7\x93ʊ#\x91\xa9\xf4M),%\x12\xbds\n\x87\xeb#\xb0W\xbd\xd8\x00]\x83\xbe֔\x9aI\xc2\xc6km\x19\x81\\\xfeFJ\xa1/A\xc5h\am\xac\xc7\x10\xe6p\x8bB}\xb0f?\xb9\xf1\x9b\xd7a|\xc0\xa4\xc3\xf8ia\xad\xf6Vޠ\xd7N=j\xee\xeb\x91po\xf4\xd6=@\x95\x02\xd7\x06\xb3\xe7\xcaB{+ǥ\xb3{\xaen\xdeve\xb4M\x8f\x9cM\x99\x9b\x02\xaerV\xba\n^\x80\xd2$\xd6\x06)\xa9\x1c\xd3\xc3͑wK\b>>\xdbh\xe9l\xa57cS\x85R\xa9\xa3\vss&*\x1eU:\xe2j\x99\xce\xe0R\xc3\x11\xd0x\xb7\xd3\n\xfd\xbc\v܌!\xfa\x1c\xc1\xa9鍭\x9b\xcc\x1e\xe8\xcbO\x8e\xebG]\xf6\xe1X\xb2\xefz\x19E\x97L\x18\xb8\xee\x11X\xe4p\x16~\x1cW\xc0\x1e\x95\xceZ\xf6Rp z{.i\xec\xbcѧ\xe7\x12\x8c\x9fu\x94\xf7\x18N\xd7\xc7Q\x97ĘɔG\xed[p\x10\t\x13\xb7\x8f\x03x\xc2g\x00R,\xd1?\x8dby\xc5b}\xc4\vX^\xc1:Ze\xb0\xc3\xf2\xb0E\xcb\r\\W{\xee\"\x1f\xafW\x13:\xa1\xe31\x15\x87܂;6\xa7\xb0W\xce\xd7\"\x94\xb0ޟ$\xf5\x93\xa65\x1e+\xfd\xe5I\xd3n\x92XGp#\xc2\x16\xb4%\xad\x10\xc4\x04\xdd\x13e\xb6{\xfa\x04\xfeд\x89\xf4\x95\xce\xe0\n\xa2=\x9e\x14\xa1y\x86\xf1\xdc\xf4\xe8\xf8|43n\xb2Pow\xf7\x9eF\xaeq\xc1\x9eN\xcd\x13+\xa6,\x98\x0f\xd3u\xb0\xd3\x1d\xfad\xcb\f\"D\xfaʦ\x99\xbeɂ\xeb\x9c\xeb2z\x8f6d\x85\xe0\xaaa\xdb\xebf\xdfo\xdd8/\x8e:'Oc\x16\xa2\x8d\x84\xaa-\x84\x05\xfca\xe1\r\xcfV\x92g\x9e\x92\x91\xf3\x98C'\xe1d\xdd\x03\u007f|\xa4-)\x00g\x93\xb5in\xe0\xe9\xb5\x1d\xc5\xd2փ6\x86\a*\x8f\xb5ۥ\xdb\xc4\xf0\xe1\xe9ǣك`z`\xf7\xaaxQ\\\xfc\xcf]\xd9\b\n\xdcfQ\xdd\xe2N\x8fo\x12\xa7l^\x9f\xc8wQ\xdd7R~\xf9܍h\v\x9f\xc5>\x9f\x98_i\xc3\x03\xe5D\n\x1c\xaeI\xed\xb5\x81\x02\x04]cz{\xbd\xba\xbe\xa4t\xdb\xe5a\xf8D\xe9\x03\xbb\x8f\x12@\xbe\\\xb8<\x03G\n\xe8'\x9c\xdd\xfbJ\x13X\a\xc6\xd9\xcd E\xda'O\xc4\xe0<\xb4\xa1\xe3<(\xe4a\x96˯\xdc\n\xbb\xc1\xc3-'c?BɁq\x8at\x18\x1d\x87h\xd0v:\x14\x9e\xe1C\xbe\xcc?\xea\xbf\xeb\x81h\xe7\xba!\xc3=\xea\xecKs|_\u007f6\xd7#鮹0\x91s>\xea?\x19\xaf\x9a\xad\xa0\xc7\r\xbea\x89\xce\xce\xe3\x92ԇ\xea\x93\x05\bΦ\xe1\xd5N\xe8\x84\xfad\xe7\x93\x15g\xf6\xce\xd82Q\x8bGK\x87_8/\x0fo\xa9&\xce\xf3/\x9b\xb4\x01\x90~q\xa8#\"sV\xe5\x95C\x81\xe7\n\xda\x04T\xefǿk..\x06\xff\\ҫt\xb6\x1dZ\xa9\x84\xdf\xff\x9c\xb5ZQ\xddu8x\xf1\xdf\x00\x00\x00\xff\xff\xc1e\xcb@\xee\x12\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4\x96\xcdr$5\f\x80\xef\xfd\x14\xaa\xe5\xb0\x17\xa6\x87\x00\a\xaao\x90]\xaaR@*\x95,\xb9P\x1c<\xb6fZ\xc4m\x1bI\x9e\x10\x9e\x9e\xb2\xbb;\xf3\x93Ne9l\xdf,\xcb\xfa\xf9$\xb9ݬV\xab\xc6$\xbaG\x16\x8a\xa1\x03\x93\b\xffQ\fe%\xed\xc3\x0f\xd2R\\\xef/6\xa8\xe6\xa2y\xa0\xe0:\xb8̢q\xb8E\x89\x99-~\xc0-\x05R\x8a\xa1\x19P\x8d3j\xba\x06\xc0\x84\x10\xd5\x14\xb1\x94%\x80\x8dA9z\x8f\xbc\xdaah\x1f\xf2\x067\x99\xbcC\xae\x1ef\xff\xfbo\xdao\xdb\xef\x1b\x00\xcbX\x8f\u007f\xa2\x01E͐:\b\xd9\xfb\x06 \x98\x01;p\xe8Qqc\xecCN\x8c\u007fg\x14\x95v\x8f\x1e9\xb6\x14\x1bIh\x8b\xe3\x1dǜ:8l\x8c秠Ƅ>TS?US\xb7\xa3\xa9\xba\xebI\xf4\x97\xd74~\xa5I+\xf9\xcc\xc6/\aT\x15\x84\xc2.{Ë*\r@b\x14\xe4=\xfe\x1e\x1eB|\f?\x13z'\x1dl\x8d\x17l\x00\xc4Ƅ\x1d\\\x97\xa8\x93\xb1\xe8\x1a\x80\xbd\xf1\xe4*\x9e1\x8f\x980\xfcxsu\xffݝ\xedq0\xa3\x10\xc0\xa1X\xa6T\xf5\x96r\x00\x1200E\x02\x1a\xa7\x00!\x06\x84\xc80DF\x18\xa3\x95v2\x998&d\xa5\x99`\xf9\x8e\xfa\xe7Yv\xe6\xfc}\x89n\xd4\x01W:\x06\x05\xb4G\x98\xea\x8e\x0e\xa4F\x0eq\vړ\x00c\xc5\x12\xc6\x1e:2\vE\xc5\x04\x88\x9b\xbf\xd0j\vw\x05\x1d\vH\x1f\xb3w\xa5\xcd\xf6\xc8\n\x8c6\xee\x02\xfd\xfblYJ~ť7:\x17x\xfe((r0\xbep\xcd\xf85\x98\xe0`0O\xc0X|@\x0eG֪\x8a\xb4\xf0[\x81Ca\x1b;\xe8U\x93t\xeb\xf5\x8et\x9e\x18\x1b\x87!\aҧu\xed{\xdad\x8d,k\x87{\xf4k\xa1\xddʰ\xedI\xd1jf\\\x9bD\xab\x1ax\xa8\x03\xd3\x0e\xee+\x9e\xc6K\xde\x1fE\xaaO\xa5\x13D\x99\xc2\xeeY\\{\xf8U\xee\xa5\u007f\xc72\x8f\xc7\xc6\xf8\x0fx\x8b\xa8P\xb9\xfdx\xf7\tf\xa7\xb5\x04\xa7\xcc+\xed\xc319\x80/\xa0(l\x91\xc7\xc2m9\x0e\xd5\"\x06\x97\"\x05\xad\v\xeb\t\xc3)tɛ\x81T\xe6\xf6+\xf5i\xe1\xb2\xde\x1b\xb0A\xc8\xc9\x19E\xd7\xc2U\x80K3\xa0\xbf4\x82_\x1c{!,\xab\x82\xf4m\xf0\xc7\xd7ݩ\xe2H\xebY<\xdfE\x8b\x15Z\x18˻\x84\xb6Ԭ\x80+giK\xb6\x8e\x01l#\xc3cO\xb6\x9f\xc7\xf2\x84\xe8\xf3\x00\xb7G⥁-\xdfh\xa0\xdc*\xa7\xf2W\x92\x85Z'b<\xe9\xb5Ց\x997)\xa8\xd1,\xff\x8bC=1\x93\xb0\x99\x19\x83Nv\xea-\xb0t\xe8srG\xe6\xc8r\x9e\xf7I8\x1f\xabJ\xfdk\x19\n\x02&t\xb1\xb1\x16\x93\xa2\xbb>\u007fN\xbc{w\xf2.\xa8K\x1b\x83\xa3\xf15\x04\u007f\xfcٌV\xd1\xdd\xcfq\x14\xe1\u007f\x01\x00\x00\xff\xff\xcb0\x9b\f\x8c\t\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xb4WOoܶ\x13\xbd\xebS\f\xf2;\xe4W \xd26m\x0f\x85n\xad\x93\x02A\xd3 \xb0\x13_\x8a\x1e\xb8\xe4\xacĚ\"Y\xcep\x1d\xf7\xd3\x17CI\xfbG+\xdb\xe9\xa1{\xd3p8|||oȭ꺮T\xb4\xb7\x98\xc8\x06߂\x8a\x16\xbf0z\xf9\xa2\xe6\xeeGjl\xd8\xec_o\x91\xd5\xeb\xea\xcez\xd3\xc2U&\x0e\xc35R\xc8I\xe3\x1b\xdcYo\xd9\x06_\r\xc8\xca(Vm\x05\xa0\xbc\x0f\xac$L\xf2\t\xa0\x83\xe7\x14\x9c\xc3Tw蛻\xbc\xc5m\xb6\xce`*+\xcc\xeb\xef\xbfm\xbek~\xa8\x00t\xc22\xfd\x93\x1d\x90X\r\xb1\x05\x9f\x9d\xab\x00\xbc\x1a\xb0\x05\x13\xee\xbd\v\xca$\xfc+#15{t\x98BcCE\x11\xb5,ڥ\x90c\vǁq\xee\x04h\xdc̛\xa9\xcc\xf5X\xa6\x8c8K\xfc\xeb\xda\xe8{;eD\x97\x93r\x97 \xca Y\xdfe\xa7\xd2\xc5p\x05\x10\x13\x12\xa6=~\xf6w>\xdc\xfb_,:C-\xec\x94#\xac\x00H\x87\x88-|\x10\x94Qi4\x15\xc0^9k\n\x15#\xee\x10\xd1\xff\xf4\xf1\xdd\xed\xf77\xba\xc7A\x8dA\x00\x83\xa4\x93\x8d%o\x89\x1b,\x81\x82\t\x05p8\x00\x03\xe5A%\xb6;\xa5\x19v)\f\xb0U\xfa.ǩ&@\xd8\xfe\x89\x9a\x818$\xd5\xe1+\xa0\xac{PRmL\x04\x17:\xd8Y\x87\xcd4%\xa6\x101\xb1\x9dY\x96߉\xbe\x0e\xb1\x05\xe0\x97\xb2\xa31\a\x8c(\n\t\xb8G\x98t\x81\x06\xa8\xec\x16\xc2\x0e\xb8\xb7\x04\t\v\x95~\xd4\xd8IY\x90\x14\xe5'\xe4\r\xdc\b݉\x80\xfa\x90\x9d\x11\x19\xee11$ԡ\xf3\xf6\xefCe\x12^dI\xa7x\x16\xc2\xfc\xb3\x9e1y\xe5\xe4,2\xbe\x02\xe5\r\f\xea\x01\x12\x16v\xb2?\xa9VR\xa8\x81\xdfBB\xb0~\x17Z\xe8\x99#\xb5\x9bMgyv\x94\x0eÐ\xbd\xe5\x87M\xf1\x85\xddf\x0e\x896\x06\xf7\xe86d\xbbZ%\xdd[F\xcd9\xe1FE[\x17\xe0\xbe\x18\xaa\x19\xcc\xff\xd2d?zy\x82\x94\x1fD=\xc4\xc9\xfa\xee\x10.:\u007f\x94w\xd1\xf9(\x8fqڈ\xffH\xaf\x84\x84\x95\xeb\xb77\x9f`^\xb4\x1c\xc19\xe7\xa3N\x0e\xd3\xe8H\xbc\x10e\xfd\x0e\xd3xpEeR\x11\xbd\x89\xc1z.\x1f\xdaY\xf4\xe7\xa4S\xde\x0e\x96i\x96\xad\x9cO\x03W\xa5\xaf\xc0\x16!G\xa3\x18M\x03\xef<\\\xa9\x01ݕ\"\xfc\xcfi\x17\x86\xa9\x16J\x9f'\xfe\xb4\x1d\x9e'\x8el\x1d\xc2s\xbfZ=\xa1\x85\x95o\"j9/!M\xe6ٝ\xd5\xc5\x02\xb0\v\t\xd4\xd1\xd9\x13m\xcdI\xdd5o\x16P*u\xc8\xe7\xb1\x05\x8aO%E\x16\xbe\xef\xd5y\v\xf9?6]#}\x80&\bcg\xf8\xa6Y\xd4{l\xf55\x8d\xaeb\x98\xa5*[\x17\x1e\xc5\xe8\xd2zN\xd1,\x17\x95\x1f\xfa<\xac\x15\xaf\xe1\xe7\x82\xf4}\xe8\x9e\x18\xbd\n\x9eE\xd0O\xa4\xdc\x06\x97\a\xbc\xf1*R\x1f\x9e̜/\xcd\xc3E\xb2L\xbbFi\xb5\xf8\x18\xa4i\xf8\x1a)\xbbՅV\x858\xff\xca\xc5\xf9\x1c\xcbr\xf7\xcc,˄\xb1\xe3\"ȅ\x9d<2ұ\r\xdc[\xeeᾷ\xba_\xa9\neZ9 \xe9/DA\xdb\xe2\xd8\u007f\a[tl\x13^ȣ.\xa2\xb9\b\n\xe4j\xad\xf8\xc2s\xeb\x85\xeb\xc9\v\xcf:\x96\x15g\xfajϖ\xec\x99T\x9dSB\xcfS\x8dr[-'|\x8dig\xc5\u007f\xbe~\xff\xa4s\xdf\x1c\xf3\xca\x1bLY?\xe2\x88\tk\xb2\x9dܭ2&\xde-\xceZ\x120\xfeN\xef\xf8gO\r\xbfD\x9bN\x9e,\x8f@{{H\x1b\x1b\v\xfa\xf1\x8aX\xbe^J9\xa4r\xedj\xe5/\xb0m\x11\f:d4\xb0}\x18;\xe3\x031\x0eK\xbc\xbb\x90\x06\xc5-\xc8\xc5Q\xb3\xbd\x10\x8a\xbc/\xd5\xd6a\v\x9c\xf2\xba\x8aV6\x1b{E\x17\xb6:\xdb\xe7G\xc9X;\xfe\x83\xb9\x9e8\u007fx\xa4\x83\xd5\xf0\x01\xef/b\x1fS\xd0H\x84Kc<\x82~E܋\xd0\xf1a\xfe\xfa\xf8U\xa4XO\x0f\xf12\x00P\x9e\xb5愺\xe9\xcd8E\x8e\x8eQZcd4\x1f\x96O\xf1\x17/\xce\xde\xd6\xe5S\ao\xec\xf8/\x02~\xff\xa3\x1a\xab\xa2\xb9\x9dqH\xf0\x9f\x00\x00\x00\xff\xff\xbbظ3\xc4\f\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Y_s۸\x11\u007fק\xd8\xc9=\xb87\x13RMz\xd3\xe9\xe8\xedb7\x1d\xb7w\x8e'\xf2\xe5%\x93\x87\x15\xb1\x12Q\x83\x00\x8a\x05\xa5\xa8\x9d~\xf7\xce\x02\xa4$J\xb4,_{)_l\x82\x8b\xc5\xfe\xdf\xdfB\x93\xa2(&\xe8\xf5'\n\xac\x9d\x9d\x01zM_#Yy\xe3\xf2\xf1O\\j7]\xbfYP\xc47\x93Gm\xd5\f\xae[\x8e\xae\xf9H\xec\xdaP\xd1\r-\xb5\xd5Q;;i(\xa2\u0088\xb3\t\x00Z\xeb\"\xca2\xcb+@\xe5l\f\xce\x18\nŊl\xf9\xd8.h\xd1j\xa3(\xa4\x13\xfa\xf3\u05ff/ߖ?L\x00\xaa@i\xfb\x83n\x88#6~\x06\xb65f\x02`\xb1\xa1\x19x\xa7\xd6δ\r-\xb0zl=\x97k2\x14\\\xa9݄=Ur\xe8*\xb8\xd6\xcf`\xff!\xef\xed\x04\xca\xca\xdc;\xf5)\xb1y\x97ؤ/Fs\xfc\xdb\xd8ן4\xc7D\xe1M\x1bМ\n\x91>\xb2\xb6\xab\xd6`8\xf9<\x01\xf0\x81\x98\u009a~\xb1\x8f\xd6m\xec{MF\xf1\f\x96h\x98&\x00\\9O3\xb8\x13)=V\xa4&\x00k4Z%Sd\xb9\x9d'\xfb\xe3\xfd\xed\xa7?̫\x9a\x1a̋\xc2\xd9y\nQ\xf7\xea\xc9s\xe0\xd8\xdd\x1a\x80\"\xae\x82\xf6\x89#\\\t\xabL\x03J\\I\f\xb1&\xe8\x1cB\n8\x1d\x03n\t\xb1\xd6\f\x81\x92\x0e6;\xf7\x80-\b\tZp\x8b\xbfS\x15K\x98\x8b\x9e\x81\x81k\xd7\x1a%\xfe_S\x88\x10\xa8r+\xab\xff\xb9\xe3\xcc\x10]:\xd2`\xa4ξ\xfd\xa3m\xa4`ш\x11Zz\rh\x154\xb8\x85@r\x06\xb4\xf6\x80[\"\xe1\x12~v\x81@ۥ\x9bA\x1d\xa3\xe7\xd9t\xbaұ\x0f\xe5\xca5Mku\xdcNS@\xeaE\x1b]\u0a625\x99)\xebU\x81\xa1\xaau\xa4*\xb6\x81\xa6\xe8u\x91\x04\xb7)\x92\xcbF}\x17\xba\xb8\xe7\xab\x03I\xe3V\xdc\xc61h\xbb\xda-\xa7\x00{\xd2\xee\x12`\xa0\x19\xb0ۖ\xe5ߛW\x96\xc4*\x1f\xff<\u007f\x80\xfe\xd0䂡͓\xb5\xf7\xdbxox1\x94\xb6K\n\xd9q\xcb\xe0\x9ađ\xac\xf2Nۘ^*\xa3\xc9\x0e\x8d\xce\xed\xa2\xd1Q<\xfd\x8f\x968\x8a\u007fJ\xb8N\t\r\v\x82\xd6+\x8c\xa4J\xb8\xb5p\x8d\r\x99kd\xfa\xcd\xcd.\x16\xe6BL\xfa\xbc\xe1\x0f\xebА0[k\xb7\xdc\x17\x8aQ\x0f\x1d\xe5\xfe\xdcS%\xfe\x12\xa3\xc9>\xbd\xd4UJ\x01X\xba\x00xL^\x1e\xb0\x1dKMyrU\x98G\x17pE?\xb9\xea ɟ\x90\xe9\xdd؎^*\xa9m9M\xa9c\r\x9c)\x8fX\x02\x98~릦@iG \x8e\xba\x92@r\xac\xa3\v[a+\xfbI\x95G\xfbG\x8d.\x8fu\x8a\xce\xca\u007f\xe7\x14\x8d\x89+\x1b!֘c\xf2ޥ\xcc\b\xad\xb5\x92\x05\xce^,\x80w\xea\xec\xf9\x1dg\x84@K\nd%\xa3r\xf1\xf1.\x95\xa8\x88\xda\xf6\x99\x97K7Dwb\xbeE60)\x18:\xfa\x9c\xb3\xe1\xc9z<*\xe9\x8f\xf7\xb7}\r\xee\x8d\xd4\xc9\x1c\x8fO|\xb8\xf90\xcbRI\b\xadR%\x95.\xb7Ԃ9\x04l\xe4\xce)1\x99\xcc\xd1\xe6\xe0\x88\x0e\xaa\x1a\xedHa\x85\x04Z\x92u\x97\xad\xf4\xb2\xf2\xea\xa5\xd9z\f\x1b\xfag\x04>\x1c\x17\x86\xffS\x13\xbeH\xad\x84ڟU\xeb\xee \x9eϪ%\xf3C\xb0\x14)i\xa6\\ŢTE>\xf2ԭ)\xac5m\xa6\x1b\x17\x1e\xb5]\x15\x12\x88E\x8e\x04\x9e\xa6\x11`\xfa]\xfa\xf3\xab\xb4H\xc8\xfc2U\x12\xe9\xb7\xd0G\xce\xe1\xe9\x8b\xd5\xe9q\xe5\xa5]\xe9j\xde!\x9f㝒\x12\x9bZWu?$\xec\xab\xe7h\x8e4\xa8r\xc9E\xbb\xfd\xcd\xc3V\f\xd9\x06\x91g[tch\x81V\xc9\xff\xac9\xca\xfa\x8b-\xd7\xea\v\x92\xf4\x97ۛo\x13̭~qF\x8e\x02\xe2\x1c\x13\xde\xdd*1\xdfRS8\v\xa7>\x0eH{`7\x82$w4\x17#\xb9\x88\xab\x13\x00\x85J\xa5\x8b\x064\xf7g@\xd6\x19\x9d\a\xc2?\xe0\x8a\x01\x03\x01B\x83^\xfc\xf4H\xdb\"7i\x8fZz\xac\xb4\xd1\x0e\xaf,\b\xd0{\xa3G\xdai\u05ca;\xb8\xd8!o\x19kq\xc5\xe3\xfa\x8eX=\xef>k\xed<^\x8c\xc1\xe7\xee\xe8\x8cKv\x10:\xba=P=\x8d\xdf\x13\xe0\xfa\x84\xddd\n\x14tu(Z1>\xba\f(\x04\xd2\x0f\x16\xbcS\x83\xf7a\x9c\r>e}\x9e\x9d\xde\"Ɩ/\x9e\xdf\x12uo\xbd\\\x0fb\xc7#a\x85_3\xc1UN\xb0\xe3\xf0\x9a\xea\x9c\v\xafO\xe9ӅHPY\xac\xa8\x1b\x89\xc7.\x866\xc8\xfd\t\xa7C\x18\x1c0\xcb\xfbR\xdd\x15^\xa4\x12\xb4\x13ԹDmHA\u007fGv\xbc\xe7\x84\xe7!\x8f\x05-\xa5T\xb5\xde8T\xfdPԉ\xd6_\xf2<\xc84\x9c\xee\x1b\xae\xf8I\x8e-\x93JS\xf2\x88\xfa\xc7\xeda\xe9B\x83q\x06\n#\x15#\fmk\f.\f\xcd \x86\xf6\xf8㓩\xdf\x103\xaeΧ\xd7ϙ&χ\xdd\x06\xc0\x85k\xe3n@\x1c\xa4\xf8\x15w\xd1s\xf9t:2\x82\rC\x16\x050s\a\x1f\x8dI;\x0e\xd3z\u007f\x89\x9a\xe4Y\x90\xb8\xe5\xbf\xcdp\x00_#\x9f7νP\x8c%Ϯ\x06\x9d\xc9\x1eH\x13a\xdb\x1c\x9fP\xc0\x1dmN\xd6n\xed}p\xab@|\x1c\x1aE\x1f?'\xca\x16\xf0>\xc5\xf9\xc5\xfav\a\x9cW\xb9#\x82ڙ>=]D\x03\xb6m\x16\x14D\xef\xc56\x12\x0f\x8b\xf0\xe9̟\xa6\x88\xbd\xd1\x0ev\xf7W\b\x99O7\x14Uh\xd3-\x9b\xe4Lt\xa04{\x83\xa7SQ\xafBB\x12\x922\x92\xd2\xfbh\xed\xd3\xd4SH\x9f^rK\x91\xa4\xb9qv\x14\xe3\xf6\xf9\xa9m\xfc\xe3\x0fO\"\x0em#\xad\x06E\xbd\xfb*\x06|'\xfc\xff\u05fc\x9fl\xacl\xd1s\xed\xe2\xed\xcdYo\xcfwd}\x94\xefAK\xaa]\xe9ޯ#\xea]>li\xf9\xc9apq\xeaq\xc4\x10/k\x1e\xf3\x01\xe93}#\xf1%U\u009c<\x06\x8c\xa7\x81\x99\ue0ef\x8f\u007fey\r\xacӵ\x98`\x9f\f\x86\xf2\xa8\xcb\xd2N\x04ڹ\x90c\xf5\x94\xe3\xa0\x11\f\n\xffP\xf4oQ\xf3G\xe2\xe1hi\xff\x93ӛ\xfd[\x8aˢ\xfb\x89)}\xe8\xd4R\a\x87w\xb7\xaa\xdd\xca\x1e\x86`%\x90\x9d\xd4\xdd\xf1\x8fL\xaf\xf2UI\xff\xabQz\xad\x9c\xcdh\x96g\xf0\xf9\xcb\x04\xba\xbb\xd6O\xbd\x1c\xb2\xf8\x9f\x00\x00\x00\xff\xff\x80\xb6\xf7)\x9e\x1b\x00\x00"), diff --git a/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml b/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml index 45cf32ae3..fe503368e 100644 --- a/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml +++ b/pkg/generated/crds/manifests/velero.io_backupstoragelocations.yaml @@ -61,6 +61,11 @@ spec: bucket: description: Bucket is the bucket to use for object storage. type: string + caCert: + description: CACert defines a CA bundle to use when verifying TLS + connections to the provider. + format: byte + type: string prefix: description: Prefix is the path inside a bucket to use for Velero storage. Optional. diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 45dcca034..6cebbf80e 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -114,6 +114,7 @@ func NewObjectBackupStore(location *velerov1api.BackupStorageLocation, objectSto } location.Spec.Config["bucket"] = bucket location.Spec.Config["prefix"] = prefix + location.Spec.Config["caCert"] = string(location.Spec.ObjectStorage.CACert) } objectStore, err := objectStoreGetter.GetObjectStore(location.Spec.Provider) diff --git a/pkg/plugin/framework/validation.go b/pkg/plugin/framework/validation.go index a7a443e5f..ba8f39be1 100644 --- a/pkg/plugin/framework/validation.go +++ b/pkg/plugin/framework/validation.go @@ -27,7 +27,7 @@ import ( func ValidateObjectStoreConfigKeys(config map[string]string, validKeys ...string) error { // `bucket` and `prefix` are automatically added to all object // store config by velero, so add them as valid keys. - return validateConfigKeys(config, append(validKeys, "bucket", "prefix")...) + return validateConfigKeys(config, append(validKeys, "bucket", "prefix", "caCert")...) } // ValidateVolumeSnapshotterConfigKeys ensures that a volume snapshotter's diff --git a/pkg/plugin/generated/RestoreItemAction.pb.go b/pkg/plugin/generated/RestoreItemAction.pb.go index b4aa4c14d..a736defda 100644 --- a/pkg/plugin/generated/RestoreItemAction.pb.go +++ b/pkg/plugin/generated/RestoreItemAction.pb.go @@ -24,10 +24,12 @@ type RestoreItemActionExecuteRequest struct { ItemFromBackup []byte `protobuf:"bytes,4,opt,name=itemFromBackup,proto3" json:"itemFromBackup,omitempty"` } -func (m *RestoreItemActionExecuteRequest) Reset() { *m = RestoreItemActionExecuteRequest{} } -func (m *RestoreItemActionExecuteRequest) String() string { return proto.CompactTextString(m) } -func (*RestoreItemActionExecuteRequest) ProtoMessage() {} -func (*RestoreItemActionExecuteRequest) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } +func (m *RestoreItemActionExecuteRequest) Reset() { *m = RestoreItemActionExecuteRequest{} } +func (m *RestoreItemActionExecuteRequest) String() string { return proto.CompactTextString(m) } +func (*RestoreItemActionExecuteRequest) ProtoMessage() {} +func (*RestoreItemActionExecuteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor3, []int{0} +} func (m *RestoreItemActionExecuteRequest) GetPlugin() string { if m != nil { diff --git a/pkg/restic/command.go b/pkg/restic/command.go index 650ed055d..ab6f0aedc 100644 --- a/pkg/restic/command.go +++ b/pkg/restic/command.go @@ -29,6 +29,7 @@ type Command struct { Command string RepoIdentifier string PasswordFile string + CACertFile string Dir string Args []string ExtraFlags []string @@ -51,6 +52,9 @@ func (c *Command) StringSlice() []string { if c.PasswordFile != "" { res = append(res, passwordFlag(c.PasswordFile)) } + if c.CACertFile != "" { + res = append(res, cacertFlag(c.CACertFile)) + } // If VELERO_SCRATCH_DIR is defined, put the restic cache within it. If not, // allow restic to choose the location. This makes running either in-cluster @@ -94,3 +98,7 @@ func passwordFlag(file string) string { func cacheDirFlag(dir string) string { return fmt.Sprintf("--cache-dir=%s", dir) } + +func cacertFlag(path string) string { + return fmt.Sprintf("--cacert=%s", path) +} diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 3e4112d83..75e510337 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -204,6 +204,44 @@ func TempCredentialsFile(secretLister corev1listers.SecretLister, veleroNamespac return name, nil } +// TempCACertFile creates a temp file containing a CA bundle +// and returns its path. The caller should generally call os.Remove() +// to remove the file when done with it. +func TempCACertFile(caCert []byte, bsl string, fs filesystem.Interface) (string, error) { + file, err := fs.TempFile("", fmt.Sprintf("cacert-%s", bsl)) + if err != nil { + return "", errors.WithStack(err) + } + + if _, err := file.Write(caCert); err != nil { + // nothing we can do about an error closing the file here, and we're + // already returning an error about the write failing. + file.Close() + return "", errors.WithStack(err) + } + + name := file.Name() + + if err := file.Close(); err != nil { + return "", errors.WithStack(err) + } + + return name, nil +} + +func GetCACert(backupLocationLister velerov1listers.BackupStorageLocationLister, namespace, bsl string) ([]byte, error) { + location, err := backupLocationLister.BackupStorageLocations(namespace).Get(bsl) + if err != nil { + return nil, errors.Wrap(err, "error getting backup storage location") + } + + if location.Spec.ObjectStorage != nil { + return location.Spec.ObjectStorage.CACert, nil + } + + return nil, nil +} + // NewPodVolumeBackupListOptions creates a ListOptions with a label selector configured to // find PodVolumeBackups for the backup identified by name. func NewPodVolumeBackupListOptions(name string) metav1.ListOptions { diff --git a/pkg/restic/common_test.go b/pkg/restic/common_test.go index 9b551a4b2..2a73c6039 100644 --- a/pkg/restic/common_test.go +++ b/pkg/restic/common_test.go @@ -17,9 +17,12 @@ limitations under the License. package restic import ( + "os" "sort" "testing" + velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" @@ -375,3 +378,44 @@ func TestTempCredentialsFile(t *testing.T) { assert.Equal(t, "passw0rd", string(contents)) } + +func TestTempCACertFile(t *testing.T) { + var ( + bslInformer = cache.NewSharedIndexInformer(nil, new(velerov1api.BackupStorageLocation), 0, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) + bslLister = velerov1listers.NewBackupStorageLocationLister(bslInformer.GetIndexer()) + fs = velerotest.NewFakeFileSystem() + bsl = &velerov1api.BackupStorageLocation{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "default", + }, + Spec: velerov1api.BackupStorageLocationSpec{ + StorageType: velerov1api.StorageType{ + ObjectStorage: &velerov1api.ObjectStorageLocation{CACert: []byte("cacert")}, + }, + }, + } + ) + + // bsl not in lister: expect an error + caCert, err := GetCACert(bslLister, "velero", "default") + assert.Error(t, err) + + // now add bsl to lister + require.NoError(t, bslInformer.GetStore().Add(bsl)) + + // bsl in lister: expect temp file to be created with cacert value + caCert, err = GetCACert(bslLister, "velero", "default") + require.NoError(t, err) + + fileName, err := TempCACertFile(caCert, "default", fs) + require.NoError(t, err) + + contents, err := fs.ReadFile(fileName) + require.NoError(t, err) + + assert.Equal(t, "cacert", string(contents)) + + os.Remove(fileName) +} diff --git a/pkg/restic/exec_commands.go b/pkg/restic/exec_commands.go index 9d640ac04..230886aaf 100644 --- a/pkg/restic/exec_commands.go +++ b/pkg/restic/exec_commands.go @@ -47,11 +47,12 @@ type backupStatusLine struct { // GetSnapshotID runs a 'restic snapshots' command to get the ID of the snapshot // in the specified repo matching the set of provided tags, or an error if a // unique snapshot cannot be identified. -func GetSnapshotID(repoIdentifier, passwordFile string, tags map[string]string, env []string) (string, error) { +func GetSnapshotID(repoIdentifier, passwordFile string, tags map[string]string, env []string, caCertFile string) (string, error) { cmd := GetSnapshotCommand(repoIdentifier, passwordFile, tags) if len(env) > 0 { cmd.Env = env } + cmd.CACertFile = caCertFile stdout, stderr, err := exec.RunCommand(cmd.Cmd()) if err != nil { diff --git a/pkg/restic/repository_manager.go b/pkg/restic/repository_manager.go index 233d3e5e2..6ae87f022 100644 --- a/pkg/restic/repository_manager.go +++ b/pkg/restic/repository_manager.go @@ -244,6 +244,22 @@ func (rm *repositoryManager) exec(cmd *Command, backupLocation string) error { cmd.PasswordFile = file + // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic + caCert, err := GetCACert(rm.backupLocationLister, rm.namespace, backupLocation) + if err != nil { + return err + } + var caCertFile string + if caCert != nil { + caCertFile, err = TempCACertFile(caCert, backupLocation, rm.fileSystem) + if err != nil { + return err + } + // ignore error since there's nothing we can do and it's a temp file. + defer os.Remove(caCertFile) + } + cmd.CACertFile = caCertFile + if strings.HasPrefix(cmd.RepoIdentifier, "azure") { if !cache.WaitForCacheSync(rm.ctx.Done(), rm.backupLocationInformerSynced) { return errors.New("timed out waiting for cache to sync")