mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-15 11:46:06 +00:00
allow restic restore helper image to be specified via ConfigMap (#1311)
* allow restic restore helper image to be specified via ConfigMap Signed-off-by: Steve Kriss <krisss@vmware.com>
This commit is contained in:
committed by
Nolan Brubaker
parent
2a36cdcbf6
commit
7298a4eda0
@@ -0,0 +1 @@
|
||||
Allow restic restore helper image name to be optionally specified via ConfigMap
|
||||
+33
-1
@@ -126,7 +126,39 @@ You're now ready to use Velero with restic.
|
||||
common encryption key for all restic repositories created by Velero. **This means that anyone who has access to your
|
||||
bucket can decrypt your restic backup data**. Make sure that you limit access to the restic bucket
|
||||
appropriately. We plan to implement full Velero backup encryption, including securing the restic encryption keys, in
|
||||
a future release.
|
||||
a future release.
|
||||
|
||||
## Customize Restore Helper Image
|
||||
|
||||
Velero uses a helper init container when performing a restic restore. By default, the image for this container is `gcr.io/heptio-images/velero-restic-restore-helper:<VERSION>`,
|
||||
where `VERSION` matches the version/tag of the main Velero image. You can customize the image that is used for this helper by creating a ConfigMap in the Velero namespace with
|
||||
the alternate image. The ConfigMap must look like the following:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# any name can be used; Velero uses the labels (below)
|
||||
# to identify it rather than the name
|
||||
name: restic-restore-action-config
|
||||
# must be in the velero namespace
|
||||
namespace: velero
|
||||
# the below labels should be used verbatim in your
|
||||
# ConfigMap.
|
||||
labels:
|
||||
# this value-less label identifies the ConfigMap as
|
||||
# config for a plugin (i.e. the built-in restic restore
|
||||
# item action plugin)
|
||||
velero.io/plugin-config: ""
|
||||
# this label identifies the name and kind of plugin
|
||||
# that this ConfigMap is for.
|
||||
velero.io/restic: RestoreItemAction
|
||||
data:
|
||||
# "image" is the only configurable key. The value can either
|
||||
# include a tag or not; if the tag is *not* included, the
|
||||
# tag from the main Velero image will automatically be used.
|
||||
image: myregistry.io/my-custom-helper-image[:OPTIONAL_TAG]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
RegisterBackupItemAction("serviceaccount", newServiceAccountBackupItemAction(f)).
|
||||
RegisterRestoreItemAction("job", newJobRestoreItemAction).
|
||||
RegisterRestoreItemAction("pod", newPodRestoreItemAction).
|
||||
RegisterRestoreItemAction("restic", newResticRestoreItemAction).
|
||||
RegisterRestoreItemAction("restic", newResticRestoreItemAction(f)).
|
||||
RegisterRestoreItemAction("service", newServiceRestoreItemAction).
|
||||
RegisterRestoreItemAction("serviceaccount", newServiceAccountRestoreItemAction).
|
||||
RegisterRestoreItemAction("addPVCFromPod", newAddPVCFromPodRestoreItemAction).
|
||||
@@ -128,8 +128,15 @@ func newPodRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
return restore.NewPodAction(logger), nil
|
||||
}
|
||||
|
||||
func newResticRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
return restore.NewResticRestoreAction(logger), nil
|
||||
func newResticRestoreItemAction(f client.Factory) veleroplugin.HandlerInitializer {
|
||||
return func(logger logrus.FieldLogger) (interface{}, error) {
|
||||
client, err := f.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return restore.NewResticRestoreAction(logger, client.CoreV1().ConfigMaps(f.Namespace())), nil
|
||||
}
|
||||
}
|
||||
|
||||
func newServiceRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 the Velero contributors.
|
||||
Copyright 2018, 2019 the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,41 +18,37 @@ package restore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"github.com/heptio/velero/pkg/buildinfo"
|
||||
"github.com/heptio/velero/pkg/plugin/framework"
|
||||
"github.com/heptio/velero/pkg/plugin/velero"
|
||||
"github.com/heptio/velero/pkg/restic"
|
||||
"github.com/heptio/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
const defaultImageBase = "gcr.io/heptio-images/velero-restic-restore-helper"
|
||||
|
||||
type ResticRestoreAction struct {
|
||||
logger logrus.FieldLogger
|
||||
initContainerImage string
|
||||
logger logrus.FieldLogger
|
||||
client corev1client.ConfigMapInterface
|
||||
}
|
||||
|
||||
func NewResticRestoreAction(logger logrus.FieldLogger) *ResticRestoreAction {
|
||||
func NewResticRestoreAction(logger logrus.FieldLogger, client corev1client.ConfigMapInterface) *ResticRestoreAction {
|
||||
return &ResticRestoreAction{
|
||||
logger: logger,
|
||||
initContainerImage: initContainerImage(),
|
||||
logger: logger,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func initContainerImage() string {
|
||||
tag := buildinfo.Version
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
|
||||
// TODO allow full image URL to be overriden via CLI flag.
|
||||
return fmt.Sprintf("gcr.io/heptio-images/velero-restic-restore-helper:%s", tag)
|
||||
}
|
||||
|
||||
func (a *ResticRestoreAction) AppliesTo() (velero.ResourceSelector, error) {
|
||||
return velero.ResourceSelector{
|
||||
IncludedResources: []string{"pods"},
|
||||
@@ -78,9 +74,20 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu
|
||||
|
||||
log.Info("Restic snapshot ID annotations found")
|
||||
|
||||
// TODO we might want/need to get plugin config at the top of this method at some point; for now, wait
|
||||
// until we know we're doing a restore before getting config.
|
||||
log.Debugf("Getting plugin config")
|
||||
config, err := getPluginConfig(framework.PluginKindRestoreItemAction, "velero.io/restic", a.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image := getImage(log, config)
|
||||
log.Infof("Using image %q", image)
|
||||
|
||||
initContainer := corev1.Container{
|
||||
Name: restic.InitContainer,
|
||||
Image: a.initContainerImage,
|
||||
Image: image,
|
||||
Args: []string{string(input.Restore.UID)},
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
@@ -110,7 +117,7 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu
|
||||
initContainer.VolumeMounts = append(initContainer.VolumeMounts, mount)
|
||||
}
|
||||
|
||||
if len(pod.Spec.InitContainers) == 0 || pod.Spec.InitContainers[0].Name != "restic-wait" {
|
||||
if len(pod.Spec.InitContainers) == 0 || pod.Spec.InitContainers[0].Name != restic.InitContainer {
|
||||
pod.Spec.InitContainers = append([]corev1.Container{initContainer}, pod.Spec.InitContainers...)
|
||||
} else {
|
||||
pod.Spec.InitContainers[0] = initContainer
|
||||
@@ -123,3 +130,72 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu
|
||||
|
||||
return velero.NewRestoreItemActionExecuteOutput(&unstructured.Unstructured{Object: res}), nil
|
||||
}
|
||||
|
||||
func getImage(log logrus.FieldLogger, config *corev1.ConfigMap) string {
|
||||
if config == nil {
|
||||
log.Debug("No config found for plugin")
|
||||
return initContainerImage(defaultImageBase)
|
||||
}
|
||||
|
||||
image := config.Data["image"]
|
||||
if image == "" {
|
||||
log.Debugf("No custom image configured")
|
||||
return initContainerImage(defaultImageBase)
|
||||
}
|
||||
|
||||
log = log.WithField("image", image)
|
||||
|
||||
parts := strings.Split(image, ":")
|
||||
switch {
|
||||
case len(parts) == 1:
|
||||
// tag-less image name: add tag
|
||||
log.Debugf("Plugin config contains image name without tag. Adding tag.")
|
||||
return initContainerImage(image)
|
||||
case len(parts) == 2:
|
||||
// tagged image name
|
||||
log.Debugf("Plugin config contains image name with tag")
|
||||
return image
|
||||
default:
|
||||
// unrecognized
|
||||
log.Warnf("Plugin config contains unparseable image name")
|
||||
return initContainerImage(defaultImageBase)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO eventually this can move to pkg/plugin/framework since it'll be used across multiple
|
||||
// plugins.
|
||||
func getPluginConfig(kind framework.PluginKind, name string, client corev1client.ConfigMapInterface) (*corev1.ConfigMap, error) {
|
||||
opts := metav1.ListOptions{
|
||||
// velero.io/plugin-config: true
|
||||
// velero.io/restic: RestoreItemAction
|
||||
LabelSelector: fmt.Sprintf("velero.io/plugin-config,%s=%s", name, kind),
|
||||
}
|
||||
|
||||
list, err := client.List(opts)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(list.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(list.Items) > 1 {
|
||||
var items []string
|
||||
for _, item := range list.Items {
|
||||
items = append(items, item.Name)
|
||||
}
|
||||
return nil, errors.Errorf("found more than one ConfigMap matching label selector %q: %v", opts.LabelSelector, items)
|
||||
}
|
||||
|
||||
return &list.Items[0], nil
|
||||
}
|
||||
|
||||
func initContainerImage(imageBase string) string {
|
||||
tag := buildinfo.Version
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s", imageBase, tag)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2019 the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/heptio/velero/pkg/buildinfo"
|
||||
velerotest "github.com/heptio/velero/pkg/util/test"
|
||||
)
|
||||
|
||||
func TestGetImage(t *testing.T) {
|
||||
configMapWithData := func(key, val string) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
key: val,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
originalVersion := buildinfo.Version
|
||||
buildinfo.Version = "buildinfo-version"
|
||||
defer func() {
|
||||
buildinfo.Version = originalVersion
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configMap *corev1.ConfigMap
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil config map returns default image with buildinfo.Version as tag",
|
||||
configMap: nil,
|
||||
want: fmt.Sprintf("%s:%s", defaultImageBase, buildinfo.Version),
|
||||
},
|
||||
{
|
||||
name: "config map without 'image' key returns default image with buildinfo.Version as tag",
|
||||
configMap: configMapWithData("non-matching-key", "val"),
|
||||
want: fmt.Sprintf("%s:%s", defaultImageBase, buildinfo.Version),
|
||||
},
|
||||
{
|
||||
name: "config map with invalid data in 'image' key returns default image with buildinfo.Version as tag",
|
||||
configMap: configMapWithData("image", "not:valid:image"),
|
||||
want: fmt.Sprintf("%s:%s", defaultImageBase, buildinfo.Version),
|
||||
},
|
||||
{
|
||||
name: "config map with untagged image returns image with buildinfo.Version as tag",
|
||||
configMap: configMapWithData("image", "myregistry.io/my-image"),
|
||||
want: fmt.Sprintf("%s:%s", "myregistry.io/my-image", buildinfo.Version),
|
||||
},
|
||||
{
|
||||
name: "config map with tagged image returns tagged image",
|
||||
configMap: configMapWithData("image", "myregistry.io/my-image:my-tag"),
|
||||
want: "myregistry.io/my-image:my-tag",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, getImage(velerotest.NewLogger(), test.configMap))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user