mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-06 21:36:30 +00:00
Merge pull request #5540 from wenterjoy/main
change image repository by configmap
This commit is contained in:
4
changelogs/unreleased/5540-wenterjoy
Normal file
4
changelogs/unreleased/5540-wenterjoy
Normal file
@@ -0,0 +1,4 @@
|
||||
add new RestoreItemAction of "velero.io/change-image-name" to handle the issue mentioned at #5519
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
RegisterRestoreItemAction("velero.io/add-pvc-from-pod", newAddPVCFromPodRestoreItemAction).
|
||||
RegisterRestoreItemAction("velero.io/add-pv-from-pvc", newAddPVFromPVCRestoreItemAction).
|
||||
RegisterRestoreItemAction("velero.io/change-storage-class", newChangeStorageClassRestoreItemAction(f)).
|
||||
RegisterRestoreItemAction("velero.io/change-image-name", newChangeImageNameRestoreItemAction(f)).
|
||||
RegisterRestoreItemAction("velero.io/role-bindings", newRoleBindingItemAction).
|
||||
RegisterRestoreItemAction("velero.io/cluster-role-bindings", newClusterRoleBindingItemAction).
|
||||
RegisterRestoreItemAction("velero.io/crd-preserve-fields", newCRDV1PreserveUnknownFieldsItemAction).
|
||||
@@ -190,6 +191,19 @@ func newChangeStorageClassRestoreItemAction(f client.Factory) plugincommon.Handl
|
||||
}
|
||||
}
|
||||
|
||||
func newChangeImageNameRestoreItemAction(f client.Factory) plugincommon.HandlerInitializer {
|
||||
return func(logger logrus.FieldLogger) (interface{}, error) {
|
||||
client, err := f.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return restore.NewChangeImageNameAction(
|
||||
logger,
|
||||
client.CoreV1().ConfigMaps(f.Namespace()),
|
||||
), nil
|
||||
}
|
||||
}
|
||||
func newRoleBindingItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
return restore.NewRoleBindingAction(logger), nil
|
||||
}
|
||||
|
||||
213
pkg/restore/change_image_name_action.go
Normal file
213
pkg/restore/change_image_name_action.go
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
Copyright 2022 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 (
|
||||
"context"
|
||||
"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"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
)
|
||||
|
||||
const (
|
||||
DELIMITER_VALUE = ","
|
||||
)
|
||||
|
||||
// ChangeImageNameAction updates a deployment or Pod's image name
|
||||
// if a mapping is found in the plugin's config map.
|
||||
type ChangeImageNameAction struct {
|
||||
logger logrus.FieldLogger
|
||||
configMapClient corev1client.ConfigMapInterface
|
||||
}
|
||||
|
||||
// NewChangeImageNameAction is the constructor for ChangeImageNameAction.
|
||||
func NewChangeImageNameAction(
|
||||
logger logrus.FieldLogger,
|
||||
configMapClient corev1client.ConfigMapInterface,
|
||||
) *ChangeImageNameAction {
|
||||
return &ChangeImageNameAction{
|
||||
logger: logger,
|
||||
configMapClient: configMapClient,
|
||||
}
|
||||
}
|
||||
|
||||
// AppliesTo returns the resources that ChangeImageNameAction should
|
||||
// be run for.
|
||||
func (a *ChangeImageNameAction) AppliesTo() (velero.ResourceSelector, error) {
|
||||
return velero.ResourceSelector{
|
||||
IncludedResources: []string{"deployments", "statefulsets", "daemonsets", "replicasets", "replicationcontrollers", "jobs", "cronjobs", "pods"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute updates the item's spec.containers' image if a mapping is found
|
||||
// in the config map for the plugin.
|
||||
func (a *ChangeImageNameAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
a.logger.Info("Executing ChangeImageNameAction")
|
||||
defer a.logger.Info("Done executing ChangeImageNameAction")
|
||||
|
||||
opts := metav1.ListOptions{
|
||||
LabelSelector: fmt.Sprintf("velero.io/plugin-config,%s=%s", "velero.io/change-image-name", common.PluginKindRestoreItemAction),
|
||||
}
|
||||
|
||||
list, err := a.configMapClient.List(context.TODO(), opts)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(list.Items) == 0 {
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: input.Item,
|
||||
}, 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)
|
||||
}
|
||||
|
||||
config := &list.Items[0]
|
||||
if len(config.Data) == 0 {
|
||||
a.logger.Info("No image name mappings found")
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
}
|
||||
|
||||
obj, ok := input.Item.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("object was of unexpected type %T", input.Item)
|
||||
}
|
||||
if obj.GetKind() == "Pod" {
|
||||
|
||||
err = a.replaceImageName(obj, config, "spec", "containers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
|
||||
err = a.replaceImageName(obj, config, "spec", "initContainers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
|
||||
} else if obj.GetKind() == "CronJob" {
|
||||
//handle containers
|
||||
err = a.replaceImageName(obj, config, "spec", "jobTemplate", "spec", "template", "spec", "containers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
//handle initContainers
|
||||
err = a.replaceImageName(obj, config, "spec", "jobTemplate", "spec", "template", "spec", "initContainers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
|
||||
} else {
|
||||
//handle containers
|
||||
err = a.replaceImageName(obj, config, "spec", "template", "spec", "containers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
|
||||
//handle initContainers
|
||||
err = a.replaceImageName(obj, config, "spec", "template", "spec", "initContainers")
|
||||
if err != nil {
|
||||
a.logger.Infof("replace image name meet error: %v", err)
|
||||
return nil, errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
}
|
||||
return velero.NewRestoreItemActionExecuteOutput(obj), nil
|
||||
}
|
||||
|
||||
func (a *ChangeImageNameAction) replaceImageName(obj *unstructured.Unstructured, config *corev1.ConfigMap, filed ...string) error {
|
||||
|
||||
log := a.logger.WithFields(map[string]interface{}{
|
||||
"kind": obj.GetKind(),
|
||||
"namespace": obj.GetNamespace(),
|
||||
"name": obj.GetName(),
|
||||
})
|
||||
needUpdateObj := false
|
||||
containers, _, err := unstructured.NestedSlice(obj.UnstructuredContent(), filed...)
|
||||
if err != nil {
|
||||
a.logger.Infof("UnstructuredConverter meet error: %v", err)
|
||||
return errors.Wrap(err, "error getting item's spec.containers")
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i, container := range containers {
|
||||
a.logger.Infoln("container:", container)
|
||||
if image, ok := container.(map[string]interface{})["image"]; ok {
|
||||
imageName := image.(string)
|
||||
if exists, newImageName, err := a.isImageReplaceRuleExist(log, imageName, config); exists && err == nil {
|
||||
needUpdateObj = true
|
||||
a.logger.Infof("Updating item's image from %s to %s", imageName, newImageName)
|
||||
container.(map[string]interface{})["image"] = newImageName
|
||||
containers[i] = container
|
||||
}
|
||||
}
|
||||
}
|
||||
if needUpdateObj {
|
||||
if err := unstructured.SetNestedField(obj.UnstructuredContent(), containers, filed...); err != nil {
|
||||
return errors.Wrap(err, "unable to set item's initContainer image")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ChangeImageNameAction) isImageReplaceRuleExist(log *logrus.Entry, oldImageName string, cm *corev1.ConfigMap) (exists bool, newImageName string, err error) {
|
||||
if oldImageName == "" {
|
||||
log.Infoln("Item has no old image name specified")
|
||||
return false, "", nil
|
||||
}
|
||||
log.Debug("oldImageName: ", oldImageName)
|
||||
|
||||
//how to use: "<old_image_name_sub_part><delimiter><new_image_name_sub_part>"
|
||||
//for current implementation the <delimiter> value can only be ","
|
||||
//e.x: in case your old image name is 1.1.1.1:5000/abc:test
|
||||
//"case1":"1.1.1.1:5000,2.2.2.2:3000"
|
||||
//"case2":"5000,3000"
|
||||
//"case3":"abc:test,edf:test"
|
||||
//"case4":"1.1.1.1:5000/abc:test,2.2.2.2:3000/edf:test"
|
||||
for _, row := range cm.Data {
|
||||
if !strings.Contains(row, DELIMITER_VALUE) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(oldImageName, strings.TrimSpace(row[0:strings.Index(row, DELIMITER_VALUE)])) && len(row[strings.Index(row, DELIMITER_VALUE):]) > len(DELIMITER_VALUE) {
|
||||
log.Infoln("match specific case:", row)
|
||||
oldImagePart := strings.TrimSpace(row[0:strings.Index(row, DELIMITER_VALUE)])
|
||||
newImagePart := strings.TrimSpace(row[strings.Index(row, DELIMITER_VALUE)+len(DELIMITER_VALUE):])
|
||||
newImageName = strings.Replace(oldImageName, oldImagePart, newImagePart, -1)
|
||||
return true, newImageName, nil
|
||||
}
|
||||
}
|
||||
return false, "", errors.Errorf("No mapping rule found for image: %s", oldImageName)
|
||||
}
|
||||
182
pkg/restore/change_image_name_action_test.go
Normal file
182
pkg/restore/change_image_name_action_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
v1 "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"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
)
|
||||
|
||||
// TestChangeImageNameActionExecute runs the ChangeImageNameAction's Execute
|
||||
// method and validates that the item's image name is modified (or not) as expected.
|
||||
// Validation is done by comparing the result of the Execute method to the test case's
|
||||
// desired result.
|
||||
func TestChangeImageRepositoryActionExecute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
podOrObj interface{}
|
||||
configMap *corev1api.ConfigMap
|
||||
freshedImageName string
|
||||
imageNameSlice []string
|
||||
want interface{}
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "a valid mapping with spaces for a new image repository is applied correctly",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container1",
|
||||
Image: "1.1.1.1:5000/abc:test",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("case1", "1.1.1.1:5000 , 2.2.2.2:3000").
|
||||
Result(),
|
||||
freshedImageName: "2.2.2.2:3000/abc:test",
|
||||
want: "2.2.2.2:3000/abc:test",
|
||||
},
|
||||
|
||||
{
|
||||
name: "a valid mapping for a new image repository is applied correctly",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container2",
|
||||
Image: "1.1.1.1:5000/abc:test",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("specific", "1.1.1.1:5000,2.2.2.2:3000").
|
||||
Result(),
|
||||
freshedImageName: "2.2.2.2:3000/abc:test",
|
||||
want: "2.2.2.2:3000/abc:test",
|
||||
},
|
||||
|
||||
{
|
||||
name: "a valid mapping for a new image name is applied correctly",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container3",
|
||||
Image: "1.1.1.1:5000/abc:test",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("specific", "abc:test,myproject:latest").
|
||||
Result(),
|
||||
freshedImageName: "1.1.1.1:5000/myproject:latest",
|
||||
want: "1.1.1.1:5000/myproject:latest",
|
||||
},
|
||||
|
||||
{
|
||||
name: "a valid mapping for a new image repository port is applied correctly",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container4",
|
||||
Image: "1.1.1.1:5000/abc:test",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("specific", "5000,3333").
|
||||
Result(),
|
||||
freshedImageName: "1.1.1.1:5000/abc:test",
|
||||
want: "1.1.1.1:3333/abc:test",
|
||||
},
|
||||
|
||||
{
|
||||
name: "a valid mapping for a new image tag is applied correctly",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container5",
|
||||
Image: "1.1.1.1:5000/abc:test",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("specific", "test,latest").
|
||||
Result(),
|
||||
freshedImageName: "1.1.1.1:5000/abc:test",
|
||||
want: "1.1.1.1:5000/abc:latest",
|
||||
},
|
||||
|
||||
{
|
||||
name: "image name contains more than one part that matching the replacing words.",
|
||||
podOrObj: builder.ForPod("default", "pod1").ObjectMeta().
|
||||
Containers(&v1.Container{
|
||||
Name: "container6",
|
||||
Image: "dev/image1:dev",
|
||||
}).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-image-name").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-image-name", "RestoreItemAction")).
|
||||
Data("specific", "dev/,test/").
|
||||
Result(),
|
||||
freshedImageName: "dev/image1:dev",
|
||||
want: "test/image1:dev",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset()
|
||||
a := NewChangeImageNameAction(
|
||||
logrus.StandardLogger(),
|
||||
clientset.CoreV1().ConfigMaps("velero"),
|
||||
)
|
||||
|
||||
// set up test data
|
||||
if tc.configMap != nil {
|
||||
_, err := clientset.CoreV1().ConfigMaps(tc.configMap.Namespace).Create(context.TODO(), tc.configMap, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.podOrObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
input := &velero.RestoreItemActionExecuteInput{
|
||||
Item: &unstructured.Unstructured{
|
||||
Object: unstructuredMap,
|
||||
},
|
||||
}
|
||||
|
||||
// execute method under test
|
||||
res, err := a.Execute(input)
|
||||
|
||||
// validate for both error and non-error cases
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
default:
|
||||
assert.NoError(t, err)
|
||||
pod := new(corev1.Pod)
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(res.UpdatedItem.UnstructuredContent(), pod)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pod.Spec.Containers[0].Image, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,49 @@ data:
|
||||
# class name.
|
||||
<old-storage-class>: <new-storage-class>
|
||||
```
|
||||
### Changing Pod/Deployment/StatefulSet/DaemonSet/ReplicaSet/ReplicationController/Job/CronJob Image Repositories
|
||||
Velero can change the image name of pod/deployment/statefulsets/daemonset/replicaset/replicationcontroller/job/cronjob during restores. To configure a image name mapping, create a config map in the Velero namespace 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: change-image-name-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 restore item action plugin)
|
||||
velero.io/plugin-config: ""
|
||||
# this label identifies the name and kind of plugin
|
||||
# that this ConfigMap is for.
|
||||
velero.io/change-image-name: RestoreItemAction
|
||||
data:
|
||||
# add 1+ key-value pairs here, where the key can be any
|
||||
# words that ConfigMap accepts.
|
||||
# the value should be:
|
||||
# "<old_image_name_sub_part><delimiter><new_image_name_sub_part>"
|
||||
# for current implementation the <delimiter> can only be ","
|
||||
# e.x: in case your old image name is 1.1.1.1:5000/abc:test
|
||||
"case1":"1.1.1.1:5000,2.2.2.2:3000"
|
||||
"case2":"5000,3000"
|
||||
"case3":"abc:test,edf:test"
|
||||
"case5":"test,latest"
|
||||
"case4":"1.1.1.1:5000/abc:test,2.2.2.2:3000/edf:test"
|
||||
# Please note that image name may contain more than one part that
|
||||
# matching the replacing words.
|
||||
# e.x:in case your old image names are:
|
||||
# dev/image1:dev and dev/image2:dev
|
||||
# you want change to:
|
||||
# test/image1:dev and test/image2:dev
|
||||
# the suggested replacing rule is:
|
||||
"case5":"dev/,test/"
|
||||
# this will avoid unexpected replacement to the second "dev".
|
||||
```
|
||||
|
||||
### Changing PVC selected-node
|
||||
|
||||
|
||||
Reference in New Issue
Block a user