Files
velero/pkg/archive/parser.go
codegold79 6bdd4ac192 Restore API group version by priority (#3133)
* Restore API group version by priority

Signed-off-by: F. Gold <fgold@vmware.com>

* Add changelog

Signed-off-by: F. Gold <fgold@vmware.com>

* Correct spelling

Signed-off-by: F. Gold <fgold@vmware.com>

* Refactor userResourceGroupVersionPriorities(...) to accept config map, adjust unit test

Signed-off-by: F. Gold <fgold@vmware.com>

* Move some unit tests into e2e

Signed-off-by: F. Gold <fgold@vmware.com>

* Add three e2e tests using Testify Suites

Summary of changes

Makefile - add testify e2e test target
go.sum - changed with go mod tidy
pkg/install/install.go - increased polling timeout
test/e2e/restore_priority_group_test.go - deleted
test/e2e/restore_test.go - deleted
test/e2e/velero_utils.go - made restic optional in velero install
test/e2e_testify/Makefile - makefile for testify e2e tests
test/e2e_testify/README.md - example command for running tests
test/e2e_testify/common_test.go - helper functions
test/e2e_testify/e2e_suite_test.go - prepare for tests and run
test/e2e_testify/restore_priority_apigv_test.go - test cases

Signed-off-by: F. Gold <fgold@vmware.com>

* Make changes per @nrb code review

Signed-off-by: F. Gold <fgold@vmware.com>

* Wait for pods in e2e tests

Signed-off-by: F. Gold <fgold@vmware.com>

* Remove testify suites e2e scaffolding moved to PR #3354

Signed-off-by: F. Gold <fgold@vmware.com>

* Make changes per @brito-rafa and Velero maintainers code reviews

- Made changes suggested by @brito-rafa in GitHub.
- We had a code review meeting with @carlisia, @dsu-igeek, @zubron, and @nrb
- and changes were made based on their suggetions:
  - pull in logic from 'meetsAPIGVResotreReqs()' to restore.go.
  - add TODO to remove APIGroupVersionFeatureFlag check
  - have feature flag and backup version format checks in separate `if` statements.
  - rename variables to be sourceGVs, targetGVs, and userGVs.

Signed-off-by: F. Gold <fgold@vmware.com>

* Convert Testify Suites e2e tests to existing Ginkgo framework

Signed-off-by: F. Gold <fgold@vmware.com>

* Made changes per @zubron PR review

Signed-off-by: F. Gold <fgold@vmware.com>

* Run go mod tidy after resolving go.sum merge conflict

Signed-off-by: F. Gold <fgold@vmware.com>

* Add feature documentation to velero.io site

Signed-off-by: F. Gold <fgold@vmware.com>

* Add config map e2e test; rename e2e test file and name

Signed-off-by: F. Gold <fgold@vmware.com>

* Update go.{mod,sum} files

Signed-off-by: F. Gold <fgold@vmware.com>

* Move CRDs and CRs to testdata folder

Signed-off-by: F. Gold <fgold@vmware.com>

* Fix typos in cert-manager to pass codespell CICD check

Signed-off-by: F. Gold <fgold@vmware.com>

* Make changes per @nrb code review round 2

- make checkAndReadDir function private
- add info level messages when priorties 1-3 API group versions can not be used

Signed-off-by: F. Gold <fgold@vmware.com>

* Make user config map rules less strict

Signed-off-by: F. Gold <fgold@vmware.com>

* Update e2e test image version in example

Signed-off-by: F. Gold <fgold@vmware.com>

* Update case A music-system controller code

Signed-off-by: F. Gold <fgold@vmware.com>

* Documentation updates

Signed-off-by: F. Gold <fgold@vmware.com>

* Update migration case documentation

Signed-off-by: F. Gold <fgold@vmware.com>
2021-02-16 12:36:17 -05:00

269 lines
8.5 KiB
Go

/*
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 archive
import (
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
)
// Parser traverses an extracted archive on disk to validate
// it and provide a helpful representation of it to consumers.
type Parser struct {
log logrus.FieldLogger
fs filesystem.Interface
}
// ResourceItems contains the collection of items of a given resource type
// within a backup, grouped by namespace (or empty string for cluster-scoped
// resources).
type ResourceItems struct {
// GroupResource is API group and resource name,
// formatted as "resource.group". For the "core"
// API group, the ".group" suffix is omitted.
GroupResource string
// ItemsByNamespace is a map from namespace (or empty string
// for cluster-scoped resources) to a list of individual item
// names contained in the archive. Item names **do not** include
// the file extension.
ItemsByNamespace map[string][]string
}
// NewParser constructs a Parser.
func NewParser(log logrus.FieldLogger, fs filesystem.Interface) *Parser {
return &Parser{
log: log,
fs: fs,
}
}
// Parse reads an extracted backup on the file system and returns
// a structured catalog of the resources and items contained within it.
func (p *Parser) Parse(dir string) (map[string]*ResourceItems, error) {
// ensure top-level "resources" directory exists, and read subdirectories
// of it, where each one is expected to correspond to a resource.
resourcesDir := filepath.Join(dir, velerov1api.ResourcesDir)
exists, err := p.fs.DirExists(resourcesDir)
if err != nil {
return nil, errors.Wrapf(err, "error checking for existence of directory %q", strings.TrimPrefix(resourcesDir, dir+"/"))
}
if !exists {
return nil, errors.Errorf("directory %q does not exist", strings.TrimPrefix(resourcesDir, dir+"/"))
}
resourceDirs, err := p.fs.ReadDir(resourcesDir)
if err != nil {
return nil, errors.Wrapf(err, "error reading contents of directory %q", strings.TrimPrefix(resourcesDir, dir+"/"))
}
// loop through each subdirectory (one per resource) and assemble
// catalog of items within it.
resources := map[string]*ResourceItems{}
for _, resourceDir := range resourceDirs {
if !resourceDir.IsDir() {
p.log.Warnf("Ignoring unexpected file %q in directory %q", resourceDir.Name(), strings.TrimPrefix(resourcesDir, dir+"/"))
continue
}
resourceItems := &ResourceItems{
GroupResource: resourceDir.Name(),
ItemsByNamespace: map[string][]string{},
}
// check for existence of a "cluster" subdirectory containing cluster-scoped
// instances of this resource, and read its contents if it exists.
clusterScopedDir := filepath.Join(resourcesDir, resourceDir.Name(), velerov1api.ClusterScopedDir)
exists, err := p.fs.DirExists(clusterScopedDir)
if err != nil {
return nil, errors.Wrapf(err, "error checking for existence of directory %q", strings.TrimPrefix(clusterScopedDir, dir+"/"))
}
if exists {
items, err := p.getResourceItemsForScope(clusterScopedDir, dir)
if err != nil {
return nil, err
}
if len(items) > 0 {
resourceItems.ItemsByNamespace[""] = items
}
}
// check for existence of a "namespaces" subdirectory containing further subdirectories,
// one per namespace, and read its contents if it exists.
namespaceScopedDir := filepath.Join(resourcesDir, resourceDir.Name(), velerov1api.NamespaceScopedDir)
exists, err = p.fs.DirExists(namespaceScopedDir)
if err != nil {
return nil, errors.Wrapf(err, "error checking for existence of directory %q", strings.TrimPrefix(namespaceScopedDir, dir+"/"))
}
if exists {
namespaceDirs, err := p.fs.ReadDir(namespaceScopedDir)
if err != nil {
return nil, errors.Wrapf(err, "error reading contents of directory %q", strings.TrimPrefix(namespaceScopedDir, dir+"/"))
}
for _, namespaceDir := range namespaceDirs {
if !namespaceDir.IsDir() {
p.log.Warnf("Ignoring unexpected file %q in directory %q", namespaceDir.Name(), strings.TrimPrefix(namespaceScopedDir, dir+"/"))
continue
}
items, err := p.getResourceItemsForScope(filepath.Join(namespaceScopedDir, namespaceDir.Name()), dir)
if err != nil {
return nil, err
}
if len(items) > 0 {
resourceItems.ItemsByNamespace[namespaceDir.Name()] = items
}
}
}
resources[resourceDir.Name()] = resourceItems
}
return resources, nil
}
// getResourceItemsForScope returns the list of items with a namespace or
// cluster-scoped subdirectory for a specific resource.
func (p *Parser) getResourceItemsForScope(dir, archiveRootDir string) ([]string, error) {
files, err := p.fs.ReadDir(dir)
if err != nil {
return nil, errors.Wrapf(err, "error reading contents of directory %q", strings.TrimPrefix(dir, archiveRootDir+"/"))
}
var items []string
for _, file := range files {
if file.IsDir() {
p.log.Warnf("Ignoring unexpected subdirectory %q in directory %q", file.Name(), strings.TrimPrefix(dir, archiveRootDir+"/"))
continue
}
items = append(items, strings.TrimSuffix(file.Name(), ".json"))
}
return items, nil
}
// checkAndReadDir is a wrapper around fs.DirExists and fs.ReadDir that does checks
// and returns errors if directory cannot be read.
func (p *Parser) checkAndReadDir(dir string) ([]os.FileInfo, error) {
exists, err := p.fs.DirExists(dir)
if err != nil {
return []os.FileInfo{}, errors.Wrapf(err, "finding %q", dir)
}
if !exists {
return []os.FileInfo{}, errors.Errorf("%q not found", dir)
}
contents, err := p.fs.ReadDir(dir)
if err != nil {
return []os.FileInfo{}, errors.Wrapf(err, "reading contents of %q", dir)
}
return contents, nil
}
// ParseGroupVersions extracts the versions for each API Group from the backup
// directory names and stores them in a metav1 APIGroup object.
func (p *Parser) ParseGroupVersions(dir string) (map[string]metav1.APIGroup, error) {
resourcesDir := filepath.Join(dir, velerov1api.ResourcesDir)
// Get the subdirectories inside the "resources" directory. The subdirectories
// will have resource.group names like "horizontalpodautoscalers.autoscaling".
rgDirs, err := p.checkAndReadDir(resourcesDir)
if err != nil {
return nil, err
}
resourceAGs := make(map[string]metav1.APIGroup)
// Loop through the resource.group directory names.
for _, rgd := range rgDirs {
group := metav1.APIGroup{
Name: extractGroupName(rgd.Name()),
}
rgdPath := filepath.Join(resourcesDir, rgd.Name())
// Inside each of the resource.group directories are directories whose
// names are API Group versions like "v1" or "v1-preferredversion"
gvDirs, err := p.checkAndReadDir(rgdPath)
if err != nil {
return nil, err
}
var supportedVersions []metav1.GroupVersionForDiscovery
for _, gvd := range gvDirs {
gvdName := gvd.Name()
// Don't save the namespaces or clusters directories in list of
// supported API Group Versions.
if gvdName == "namespaces" || gvdName == "cluster" {
continue
}
version := metav1.GroupVersionForDiscovery{
GroupVersion: strings.TrimPrefix(group.Name+"/"+gvdName, "/"),
Version: gvdName,
}
if strings.Contains(gvdName, velerov1api.PreferredVersionDir) {
gvdName = strings.TrimSuffix(gvdName, velerov1api.PreferredVersionDir)
// Update version and group version to be without suffix.
version.Version = gvdName
version.GroupVersion = strings.TrimPrefix(group.Name+"/"+gvdName, "/")
group.PreferredVersion = version
}
supportedVersions = append(supportedVersions, version)
}
group.Versions = supportedVersions
resourceAGs[rgd.Name()] = group
}
return resourceAGs, nil
}
// extractGroupName will take a concatenated resource.group and extract the group,
// if there is one. Resources like "pods" which has no group and will return an
// empty string.
func extractGroupName(resourceGroupDir string) string {
parts := strings.SplitN(resourceGroupDir, ".", 2)
var group string
if len(parts) == 2 {
group = parts[1]
}
return group
}