diff --git a/changelogs/unreleased/4397-reasonerjt b/changelogs/unreleased/4397-reasonerjt new file mode 100644 index 000000000..243350200 --- /dev/null +++ b/changelogs/unreleased/4397-reasonerjt @@ -0,0 +1 @@ +Add restoreactionitem plugin to handle admission webhook configurations \ No newline at end of file diff --git a/pkg/cmd/server/plugin/plugin.go b/pkg/cmd/server/plugin/plugin.go index 1a79b6dd6..507200266 100644 --- a/pkg/cmd/server/plugin/plugin.go +++ b/pkg/cmd/server/plugin/plugin.go @@ -55,6 +55,7 @@ func NewCommand(f client.Factory) *cobra.Command { RegisterRestoreItemAction("velero.io/crd-preserve-fields", newCRDV1PreserveUnknownFieldsItemAction). RegisterRestoreItemAction("velero.io/change-pvc-node-selector", newChangePVCNodeSelectorItemAction(f)). RegisterRestoreItemAction("velero.io/apiservice", newAPIServiceRestoreItemAction). + RegisterRestoreItemAction("velero.io/admission-webhook-configuration", newAdmissionWebhookConfigurationAction). Serve() }, } @@ -202,3 +203,7 @@ func newChangePVCNodeSelectorItemAction(f client.Factory) veleroplugin.HandlerIn func newAPIServiceRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) { return restore.NewAPIServiceAction(logger), nil } + +func newAdmissionWebhookConfigurationAction(logger logrus.FieldLogger) (interface{}, error) { + return restore.NewAdmissionWebhookConfigurationAction(logger), nil +} diff --git a/pkg/restore/admissionwebhook_config_action.go b/pkg/restore/admissionwebhook_config_action.go new file mode 100644 index 000000000..8fd5c1693 --- /dev/null +++ b/pkg/restore/admissionwebhook_config_action.go @@ -0,0 +1,89 @@ +/* +Copyright 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 ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/vmware-tanzu/velero/pkg/plugin/velero" +) + +// AdmissionWebhookConfigurationAction is a RestoreItemAction plugin applicable to mutatingwebhookconfiguration and +// validatingwebhookconfiguration to reset the invalid value for "sideEffects" of the webhooks. +// More background please refer to https://github.com/vmware-tanzu/velero/issues/3516 +type AdmissionWebhookConfigurationAction struct { + logger logrus.FieldLogger +} + +// NewAdmissionWebhookConfigurationAction creates a new instance of AdmissionWebhookConfigurationAction +func NewAdmissionWebhookConfigurationAction(logger logrus.FieldLogger) *AdmissionWebhookConfigurationAction { + return &AdmissionWebhookConfigurationAction{logger: logger} +} + +// AppliesTo implements the RestoreItemAction plugin interface method. +func (a *AdmissionWebhookConfigurationAction) AppliesTo() (velero.ResourceSelector, error) { + return velero.ResourceSelector{ + IncludedResources: []string{"mutatingwebhookconfigurations", "validatingwebhookconfigurations"}, + }, nil +} + +// Execute will reset the value of "sideEffects" attribute of each item in the "webhooks" list to "None" if they are invalid values for +// v1, such as "Unknown" or "Some" +func (a *AdmissionWebhookConfigurationAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + a.logger.Info("Executing ChangeStorageClassAction") + defer a.logger.Info("Done executing ChangeStorageClassAction") + + item := input.Item + apiVersion, _, err := unstructured.NestedString(item.UnstructuredContent(), "apiVersion") + if err != nil { + return nil, errors.Wrap(err, "failed to get the apiVersion from input item") + } + name, _, _ := unstructured.NestedString(item.UnstructuredContent(), "metadata", "name") + logger := a.logger.WithField("resource_name", name) + if apiVersion != "admissionregistration.k8s.io/v1" { + logger.Infof("unable to handle api version: %s, skip", apiVersion) + return velero.NewRestoreItemActionExecuteOutput(input.Item), nil + } + webhooks, ok, err := unstructured.NestedSlice(item.UnstructuredContent(), "webhooks") + if err != nil { + return nil, errors.Wrap(err, "failed to get webhooks slice from input item") + } + if !ok { + logger.Info("webhooks is not set, skip") + return velero.NewRestoreItemActionExecuteOutput(input.Item), nil + } + newWebhooks := make([]interface{}, 0) + for i, entry := range webhooks { + logger2 := logger.WithField("index", i) + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&entry) + if err != nil { + logger2.Errorf("failed to convert the webhook entry, error: %v, it will be dropped", err) + continue + } + s, _, _ := unstructured.NestedString(obj, "sideEffects") + if s != "None" && s != "NoneOnDryRun" { + logger2.Infof("reset the invalid sideEffects value '%s' to 'None'", s) + obj["sideEffects"] = "None" + } + newWebhooks = append(newWebhooks, obj) + } + item.UnstructuredContent()["webhooks"] = newWebhooks + return velero.NewRestoreItemActionExecuteOutput(item), nil +} diff --git a/pkg/restore/admissionwebhook_config_action_test.go b/pkg/restore/admissionwebhook_config_action_test.go new file mode 100644 index 000000000..c6c31d221 --- /dev/null +++ b/pkg/restore/admissionwebhook_config_action_test.go @@ -0,0 +1,199 @@ +package restore + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestNewAdmissionWebhookConfigurationActionExecute(t *testing.T) { + action := NewAdmissionWebhookConfigurationAction(velerotest.NewLogger()) + cases := []struct { + name string + itemJSON string + wantErr bool + NoneSideEffectsIndex []int // the indexes with sideEffects that arereset to None + NotNoneSideEffectsIndex []int // the indexes with sideEffects that are not reset to None + }{ + { + name: "v1 mutatingwebhookconfiguration with sideEffects as Unknown", + itemJSON: `{ + "apiVersion": "admissionregistration.k8s.io/v1", + "kind": "MutatingWebhookConfiguration", + "metadata": { + "name": "my-test-mutating" + }, + "webhooks": [ + { + "clientConfig": { + "url": "https://mytest.org" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "apiVersions": [ + "v1" + ], + "operations": [ + "CREATE" + ], + "resources": [ + "pods" + ], + "scope": "Namespaced" + } + ], + "sideEffects": "Unknown" + } + ] + }`, + wantErr: false, + NoneSideEffectsIndex: []int{0}, + }, + { + name: "v1 validatingwebhookconfiguration with sideEffects as Some", + itemJSON: `{ + "apiVersion": "admissionregistration.k8s.io/v1", + "kind": "ValidatingWebhookConfiguration", + "metadata": { + "name": "my-test-validating" + }, + "webhooks": [ + { + "clientConfig": { + "url": "https://mytest.org" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "apiVersions": [ + "v1" + ], + "operations": [ + "CREATE" + ], + "resources": [ + "pods" + ], + "scope": "Namespaced" + } + ], + "sideEffects": "Some" + } + ] + }`, + wantErr: false, + NoneSideEffectsIndex: []int{0}, + }, + { + name: "v1beta1 validatingwebhookconfiguration with sideEffects as Some, nothing should change", + itemJSON: `{ + "apiVersion": "admissionregistration.k8s.io/v1beta1", + "kind": "ValidatingWebhookConfiguration", + "metadata": { + "name": "my-test-validating" + }, + "webhooks": [ + { + "clientConfig": { + "url": "https://mytest.org" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "apiVersions": [ + "v1" + ], + "operations": [ + "CREATE" + ], + "resources": [ + "pods" + ], + "scope": "Namespaced" + } + ], + "sideEffects": "Some" + } + ] + }`, + wantErr: false, + NotNoneSideEffectsIndex: []int{0}, + }, + { + name: "v1 validatingwebhookconfiguration with multiple invalid sideEffects", + itemJSON: `{ + "apiVersion": "admissionregistration.k8s.io/v1", + "kind": "ValidatingWebhookConfiguration", + "metadata": { + "name": "my-test-validating" + }, + "webhooks": [ + { + "clientConfig": { + "url": "https://mytest.org" + }, + "sideEffects": "Some" + }, + { + "clientConfig": { + "url": "https://mytest2.org" + }, + "sideEffects": "Some" + } + ] + }`, + wantErr: false, + NoneSideEffectsIndex: []int{0, 1}, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + o := map[string]interface{}{} + json.Unmarshal([]byte(tt.itemJSON), &o) + input := &velero.RestoreItemActionExecuteInput{ + Item: &unstructured.Unstructured{ + Object: o, + }, + } + output, err := action.Execute(input) + if tt.wantErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + if tt.NoneSideEffectsIndex != nil { + wb, _, err := unstructured.NestedSlice(output.UpdatedItem.UnstructuredContent(), "webhooks") + assert.Nil(t, err) + for _, i := range tt.NoneSideEffectsIndex { + it, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&wb[i]) + assert.Nil(t, err) + s := it["sideEffects"].(string) + assert.Equal(t, "None", s) + } + } + if tt.NotNoneSideEffectsIndex != nil { + wb, _, err := unstructured.NestedSlice(output.UpdatedItem.UnstructuredContent(), "webhooks") + assert.Nil(t, err) + for _, i := range tt.NotNoneSideEffectsIndex { + it, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&wb[i]) + assert.Nil(t, err) + s := it["sideEffects"].(string) + assert.NotEqual(t, "None", s) + } + } + }) + } +} diff --git a/pkg/restore/job_action.go b/pkg/restore/job_action.go index ecbea69da..fbaf30b24 100644 --- a/pkg/restore/job_action.go +++ b/pkg/restore/job_action.go @@ -1,5 +1,5 @@ /* -Copyright 2017 the Velero contributors. +Copyright 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. diff --git a/pkg/restore/merge_service_account.go b/pkg/restore/merge_service_account.go index 279ce9979..dd210f10f 100644 --- a/pkg/restore/merge_service_account.go +++ b/pkg/restore/merge_service_account.go @@ -1,5 +1,5 @@ /* -Copyright 2018 the Velero contributors. +Copyright 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. @@ -13,6 +13,7 @@ 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 (