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:
Steve Kriss
2019-03-29 17:11:34 -04:00
committed by Nolan Brubaker
parent 2a36cdcbf6
commit 7298a4eda0
5 changed files with 220 additions and 22 deletions
+1
View File
@@ -0,0 +1 @@
Allow restic restore helper image name to be optionally specified via ConfigMap
+33 -1
View File
@@ -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
+10 -3
View File
@@ -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) {
+94 -18
View File
@@ -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)
}
+82
View File
@@ -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))
})
}
}