183 lines
7.2 KiB
Go
183 lines
7.2 KiB
Go
/*
|
|
Copyright 2020 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 backup
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
|
|
|
apiextv1beta1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
velerodiscovery "github.com/vmware-tanzu/velero/pkg/discovery"
|
|
|
|
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
|
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
|
)
|
|
|
|
// RemapCRDVersionAction inspects CustomResourceDefinition and decides if it is a v1
|
|
// CRD that needs to be backed up as v1beta1.
|
|
type RemapCRDVersionAction struct {
|
|
logger logrus.FieldLogger
|
|
betaCRDClient apiextv1beta1client.CustomResourceDefinitionInterface
|
|
discoveryHelper velerodiscovery.Helper
|
|
}
|
|
|
|
// NewRemapCRDVersionAction instantiates a new RemapCRDVersionAction plugin.
|
|
func NewRemapCRDVersionAction(logger logrus.FieldLogger, betaCRDClient apiextv1beta1client.CustomResourceDefinitionInterface, discoveryHelper velerodiscovery.Helper) *RemapCRDVersionAction {
|
|
return &RemapCRDVersionAction{logger: logger, betaCRDClient: betaCRDClient, discoveryHelper: discoveryHelper}
|
|
}
|
|
|
|
// AppliesTo selects the resources the plugin should run against. In this case, CustomResourceDefinitions.
|
|
func (a *RemapCRDVersionAction) AppliesTo() (velero.ResourceSelector, error) {
|
|
return velero.ResourceSelector{
|
|
IncludedResources: []string{"customresourcedefinition.apiextensions.k8s.io"},
|
|
}, nil
|
|
}
|
|
|
|
// Execute executes logic necessary to check a CustomResourceDefinition and inspect it for characteristics that necessitate saving it as v1beta1 instead of v1.
|
|
func (a *RemapCRDVersionAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
|
|
a.logger.Info("Executing RemapCRDVersionAction")
|
|
|
|
// This plugin is only relevant for CRDs retrieved from the v1 endpoint that were installed via the v1beta1
|
|
// endpoint, so we can exit immediately if the resource in question isn't v1.
|
|
apiVersion, ok, err := unstructured.NestedString(item.UnstructuredContent(), "apiVersion")
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to read apiVersion from CRD")
|
|
}
|
|
if ok && apiVersion != "apiextensions.k8s.io/v1" {
|
|
a.logger.Info("Exiting RemapCRDVersionAction, CRD is not v1")
|
|
return item, nil, nil
|
|
}
|
|
|
|
// This plugin will exit if the CRD was installed via v1beta1 but the cluster does not support v1beta1 CRD
|
|
supportv1b1 := false
|
|
CheckVersion:
|
|
for _, g := range a.discoveryHelper.APIGroups() {
|
|
if g.Name == apiextv1.GroupName {
|
|
for _, v := range g.Versions {
|
|
if v.Version == apiextv1beta1.SchemeGroupVersion.Version {
|
|
supportv1b1 = true
|
|
break CheckVersion
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
if !supportv1b1 {
|
|
a.logger.Info("Exiting RemapCRDVersionAction, the cluster does not support v1beta1 CRD")
|
|
return item, nil, nil
|
|
}
|
|
|
|
// We've got a v1 CRD and the cluster supports v1beta1 CRD, so proceed.
|
|
var crd apiextv1.CustomResourceDefinition
|
|
|
|
// Do not use runtime.DefaultUnstructuredConverter.FromUnstructured here because it has a bug when converting integers/whole
|
|
// numbers in float fields (https://github.com/kubernetes/kubernetes/issues/87675).
|
|
// Using JSON as a go-between avoids this issue, without adding a bunch of type conversion by using unstructured helper functions
|
|
// to inspect the fields we want to look at.
|
|
js, err := json.Marshal(item.UnstructuredContent())
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to convert unstructured item to JSON")
|
|
}
|
|
|
|
if err = json.Unmarshal(js, &crd); err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to convert JSON to CRD Go type")
|
|
}
|
|
|
|
log := a.logger.WithField("plugin", "RemapCRDVersionAction").WithField("CRD", crd.Name)
|
|
|
|
switch {
|
|
case hasSingleVersion(crd), hasNonStructuralSchema(crd), hasPreserveUnknownFields(crd):
|
|
log.Infof("CustomResourceDefinition %s appears to be v1beta1, fetching the v1beta version", crd.Name)
|
|
item, err = fetchV1beta1CRD(crd.Name, a.betaCRDClient)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
default:
|
|
log.Infof("CustomResourceDefinition %s does not appear to be v1beta1, backing up as v1", crd.Name)
|
|
}
|
|
|
|
return item, nil, nil
|
|
}
|
|
|
|
func fetchV1beta1CRD(name string, betaCRDClient apiextv1beta1client.CustomResourceDefinitionInterface) (*unstructured.Unstructured, error) {
|
|
betaCRD, err := betaCRDClient.Get(context.TODO(), name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error fetching v1beta1 version of %s", name)
|
|
}
|
|
|
|
// Individual items fetched from the API don't always have the kind/API version set
|
|
// See https://github.com/kubernetes/kubernetes/issues/3030. Unsure why this is happening here and not in main Velero;
|
|
// probably has to do with List calls and Dynamic client vs typed client
|
|
// Set these all the time, since they shouldn't ever be different, anyway
|
|
betaCRD.Kind = "CustomResourceDefinition"
|
|
betaCRD.APIVersion = apiextv1beta1.SchemeGroupVersion.String()
|
|
|
|
m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&betaCRD)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error converting v1beta1 version of %s to unstructured", name)
|
|
}
|
|
item := &unstructured.Unstructured{Object: m}
|
|
|
|
return item, nil
|
|
|
|
}
|
|
|
|
// hasPreserveUnknownFields determines whether or not a CRD is set to preserve unknown fields or not.
|
|
func hasPreserveUnknownFields(crd apiextv1.CustomResourceDefinition) bool {
|
|
return crd.Spec.PreserveUnknownFields
|
|
}
|
|
|
|
// hasNonStructuralSchema determines whether or not a CRD has had a nonstructural schema condition applied.
|
|
func hasNonStructuralSchema(crd apiextv1.CustomResourceDefinition) bool {
|
|
var ret bool
|
|
for _, c := range crd.Status.Conditions {
|
|
if c.Type == apiextv1.NonStructuralSchema {
|
|
ret = true
|
|
break
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// hasSingleVersion checks a CRD to see if it has a single version with no schema information.
|
|
func hasSingleVersion(crd apiextv1.CustomResourceDefinition) bool {
|
|
// Looking for 1 version should be enough to tell if it's a v1beta1 CRD, as all v1beta1 CRD versions share the same schema.
|
|
// v1 CRDs can have different schemas per version
|
|
// The silently upgraded versions will often have a `versions` entry that looks like this:
|
|
// versions:
|
|
// - name: v1
|
|
// served: true
|
|
// storage: true
|
|
// This is acceptable when re-submitted to a v1beta1 endpoint on restore.
|
|
var ret bool
|
|
if len(crd.Spec.Versions) > 0 {
|
|
if crd.Spec.Versions[0].Schema == nil || crd.Spec.Versions[0].Schema.OpenAPIV3Schema == nil {
|
|
ret = true
|
|
}
|
|
}
|
|
return ret
|
|
}
|