2017-08-02 17:27:17 +00:00
|
|
|
/*
|
|
|
|
Copyright 2017 Heptio Inc.
|
|
|
|
|
|
|
|
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 (
|
|
|
|
"archive/tar"
|
|
|
|
"compress/gzip"
|
2017-08-24 23:44:01 +00:00
|
|
|
"fmt"
|
2017-08-02 17:27:17 +00:00
|
|
|
"io"
|
|
|
|
|
2017-09-14 21:27:31 +00:00
|
|
|
"github.com/pkg/errors"
|
2017-10-05 23:36:04 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2017-09-14 21:27:31 +00:00
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
|
|
|
|
|
|
|
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
|
|
|
"github.com/heptio/ark/pkg/client"
|
|
|
|
"github.com/heptio/ark/pkg/discovery"
|
|
|
|
"github.com/heptio/ark/pkg/util/collections"
|
2017-10-02 20:53:08 +00:00
|
|
|
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
2017-11-14 18:17:20 +00:00
|
|
|
"github.com/heptio/ark/pkg/util/logging"
|
2017-08-02 17:27:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Backupper performs backups.
|
|
|
|
type Backupper interface {
|
2017-09-11 17:59:04 +00:00
|
|
|
// Backup takes a backup using the specification in the api.Backup and writes backup and log data
|
|
|
|
// to the given writers.
|
|
|
|
Backup(backup *api.Backup, backupFile, logFile io.Writer) error
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// kubernetesBackupper implements Backupper.
|
|
|
|
type kubernetesBackupper struct {
|
2017-10-02 20:53:08 +00:00
|
|
|
dynamicFactory client.DynamicFactory
|
|
|
|
discoveryHelper discovery.Helper
|
|
|
|
actions map[schema.GroupResource]Action
|
|
|
|
podCommandExecutor podCommandExecutor
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
groupBackupperFactory groupBackupperFactory
|
2017-09-07 18:09:22 +00:00
|
|
|
}
|
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
// ResourceIdentifier describes a single item by its group, resource, namespace, and name.
|
|
|
|
type ResourceIdentifier struct {
|
|
|
|
schema.GroupResource
|
|
|
|
Namespace string
|
|
|
|
Name string
|
2017-09-07 18:09:22 +00:00
|
|
|
}
|
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
// Action is an actor that performs an operation on an individual item being backed up.
|
|
|
|
type Action interface {
|
2017-10-02 20:53:08 +00:00
|
|
|
// Execute allows the Action to perform arbitrary logic with the item being backed up and the
|
|
|
|
// backup itself. Implementations may return additional ResourceIdentifiers that indicate specific
|
|
|
|
// items that also need to be backed up.
|
|
|
|
Execute(log *logrus.Entry, item runtime.Unstructured, backup *api.Backup) ([]ResourceIdentifier, error)
|
2017-08-24 23:44:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type itemKey struct {
|
|
|
|
resource string
|
|
|
|
namespace string
|
|
|
|
name string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *itemKey) String() string {
|
|
|
|
return fmt.Sprintf("resource=%s,namespace=%s,name=%s", i.resource, i.namespace, i.name)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewKubernetesBackupper creates a new kubernetesBackupper.
|
|
|
|
func NewKubernetesBackupper(
|
|
|
|
discoveryHelper discovery.Helper,
|
|
|
|
dynamicFactory client.DynamicFactory,
|
|
|
|
actions map[string]Action,
|
2017-10-02 20:53:08 +00:00
|
|
|
podCommandExecutor podCommandExecutor,
|
2017-08-02 17:27:17 +00:00
|
|
|
) (Backupper, error) {
|
2017-09-01 21:39:04 +00:00
|
|
|
resolvedActions, err := resolveActions(discoveryHelper, actions)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &kubernetesBackupper{
|
2017-10-02 20:53:08 +00:00
|
|
|
discoveryHelper: discoveryHelper,
|
|
|
|
dynamicFactory: dynamicFactory,
|
|
|
|
actions: resolvedActions,
|
|
|
|
podCommandExecutor: podCommandExecutor,
|
|
|
|
|
|
|
|
groupBackupperFactory: &defaultGroupBackupperFactory{},
|
2017-08-02 17:27:17 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// resolveActions resolves the string-based map of group-resources to actions and returns a map of
|
|
|
|
// schema.GroupResources to actions.
|
2017-09-01 21:39:04 +00:00
|
|
|
func resolveActions(helper discovery.Helper, actions map[string]Action) (map[schema.GroupResource]Action, error) {
|
2017-08-02 17:27:17 +00:00
|
|
|
ret := make(map[schema.GroupResource]Action)
|
|
|
|
|
|
|
|
for resource, action := range actions {
|
2017-08-24 23:44:01 +00:00
|
|
|
gvr, _, err := helper.ResourceFor(schema.ParseGroupResource(resource).WithVersion(""))
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-08-24 23:44:01 +00:00
|
|
|
ret[gvr.GroupResource()] = action
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2017-08-24 23:44:01 +00:00
|
|
|
// getResourceIncludesExcludes takes the lists of resources to include and exclude, uses the
|
|
|
|
// discovery helper to resolve them to fully-qualified group-resource names, and returns an
|
|
|
|
// IncludesExcludes list.
|
2017-10-02 20:53:08 +00:00
|
|
|
func getResourceIncludesExcludes(helper discovery.Helper, includes, excludes []string) *collections.IncludesExcludes {
|
2017-08-24 23:44:01 +00:00
|
|
|
resources := collections.GenerateIncludesExcludes(
|
2017-09-01 21:39:04 +00:00
|
|
|
includes,
|
|
|
|
excludes,
|
2017-09-14 21:27:31 +00:00
|
|
|
func(item string) string {
|
2017-08-24 23:44:01 +00:00
|
|
|
gvr, _, err := helper.ResourceFor(schema.ParseGroupResource(item).WithVersion(""))
|
2017-09-01 21:39:04 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return ""
|
2017-09-01 21:39:04 +00:00
|
|
|
}
|
|
|
|
|
2017-08-24 23:44:01 +00:00
|
|
|
gr := gvr.GroupResource()
|
2017-09-14 21:27:31 +00:00
|
|
|
return gr.String()
|
2017-09-01 21:39:04 +00:00
|
|
|
},
|
|
|
|
)
|
2017-08-24 23:44:01 +00:00
|
|
|
|
|
|
|
return resources
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// getNamespaceIncludesExcludes returns an IncludesExcludes list containing which namespaces to
|
|
|
|
// include and exclude from the backup.
|
|
|
|
func getNamespaceIncludesExcludes(backup *api.Backup) *collections.IncludesExcludes {
|
|
|
|
return collections.NewIncludesExcludes().Includes(backup.Spec.IncludedNamespaces...).Excludes(backup.Spec.ExcludedNamespaces...)
|
|
|
|
}
|
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
func getResourceHooks(hookSpecs []api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]resourceHook, error) {
|
|
|
|
resourceHooks := make([]resourceHook, 0, len(hookSpecs))
|
|
|
|
|
|
|
|
for _, r := range hookSpecs {
|
|
|
|
h := resourceHook{
|
|
|
|
name: r.Name,
|
|
|
|
namespaces: collections.NewIncludesExcludes().Includes(r.IncludedNamespaces...).Excludes(r.ExcludedNamespaces...),
|
|
|
|
resources: getResourceIncludesExcludes(discoveryHelper, r.IncludedResources, r.ExcludedResources),
|
|
|
|
hooks: r.Hooks,
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.LabelSelector != nil {
|
|
|
|
labelSelector, err := metav1.LabelSelectorAsSelector(r.LabelSelector)
|
|
|
|
if err != nil {
|
|
|
|
return []resourceHook{}, errors.WithStack(err)
|
|
|
|
}
|
|
|
|
h.labelSelector = labelSelector
|
|
|
|
}
|
|
|
|
|
|
|
|
resourceHooks = append(resourceHooks, h)
|
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
return resourceHooks, nil
|
2017-08-11 15:05:56 +00:00
|
|
|
}
|
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
// Backup backs up the items specified in the Backup, placing them in a gzip-compressed tar file
|
2017-09-11 17:59:04 +00:00
|
|
|
// written to backupFile. The finalized api.Backup is written to metadata.
|
|
|
|
func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io.Writer) error {
|
|
|
|
gzippedData := gzip.NewWriter(backupFile)
|
2017-08-11 15:05:56 +00:00
|
|
|
defer gzippedData.Close()
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2017-08-11 15:05:56 +00:00
|
|
|
tw := tar.NewWriter(gzippedData)
|
2017-08-02 17:27:17 +00:00
|
|
|
defer tw.Close()
|
|
|
|
|
2017-09-11 17:59:04 +00:00
|
|
|
gzippedLog := gzip.NewWriter(logFile)
|
2017-08-11 15:05:56 +00:00
|
|
|
defer gzippedLog.Close()
|
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
logger := logrus.New()
|
|
|
|
logger.Out = gzippedLog
|
2017-11-14 18:17:20 +00:00
|
|
|
logger.Hooks.Add(&logging.ErrorLocationHook{})
|
|
|
|
logger.Hooks.Add(&logging.LogLocationHook{})
|
2017-10-02 20:53:08 +00:00
|
|
|
log := logger.WithField("backup", kubeutil.NamespaceAndName(backup))
|
|
|
|
log.Info("Starting backup")
|
|
|
|
|
|
|
|
namespaceIncludesExcludes := getNamespaceIncludesExcludes(backup)
|
|
|
|
log.Infof("Including namespaces: %s", namespaceIncludesExcludes.IncludesString())
|
|
|
|
log.Infof("Excluding namespaces: %s", namespaceIncludesExcludes.ExcludesString())
|
|
|
|
|
|
|
|
resourceIncludesExcludes := getResourceIncludesExcludes(kb.discoveryHelper, backup.Spec.IncludedResources, backup.Spec.ExcludedResources)
|
|
|
|
log.Infof("Including resources: %s", resourceIncludesExcludes.IncludesString())
|
|
|
|
log.Infof("Excluding resources: %s", resourceIncludesExcludes.ExcludesString())
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
resourceHooks, err := getResourceHooks(backup.Spec.Hooks.Resources, kb.discoveryHelper)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var labelSelector string
|
|
|
|
if backup.Spec.LabelSelector != nil {
|
|
|
|
labelSelector = metav1.FormatLabelSelector(backup.Spec.LabelSelector)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
backedUpItems := make(map[itemKey]struct{})
|
|
|
|
var errs []error
|
2017-08-11 15:05:56 +00:00
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
cohabitatingResources := map[string]*cohabitatingResource{
|
|
|
|
"deployments": newCohabitatingResource("deployments", "extensions", "apps"),
|
|
|
|
"networkpolicies": newCohabitatingResource("networkpolicies", "extensions", "networking.k8s.io"),
|
|
|
|
}
|
|
|
|
|
|
|
|
gb := kb.groupBackupperFactory.newGroupBackupper(
|
|
|
|
log,
|
|
|
|
backup,
|
|
|
|
namespaceIncludesExcludes,
|
|
|
|
resourceIncludesExcludes,
|
|
|
|
labelSelector,
|
|
|
|
kb.dynamicFactory,
|
|
|
|
kb.discoveryHelper,
|
|
|
|
backedUpItems,
|
|
|
|
cohabitatingResources,
|
|
|
|
kb.actions,
|
|
|
|
kb.podCommandExecutor,
|
|
|
|
tw,
|
|
|
|
resourceHooks,
|
|
|
|
)
|
2017-08-11 15:05:56 +00:00
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
for _, group := range kb.discoveryHelper.Resources() {
|
2017-10-02 20:53:08 +00:00
|
|
|
if err := gb.backupGroup(group); err != nil {
|
2017-08-02 17:27:17 +00:00
|
|
|
errs = append(errs, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-02 20:53:08 +00:00
|
|
|
err = kuberrs.NewAggregate(errs)
|
2017-08-11 15:05:56 +00:00
|
|
|
if err == nil {
|
2017-10-02 20:53:08 +00:00
|
|
|
log.Infof("Backup completed successfully")
|
2017-08-11 15:05:56 +00:00
|
|
|
} else {
|
2017-10-02 20:53:08 +00:00
|
|
|
log.Infof("Backup completed with errors: %v", err)
|
2017-08-11 15:05:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type tarWriter interface {
|
|
|
|
io.Closer
|
|
|
|
Write([]byte) (int, error)
|
|
|
|
WriteHeader(*tar.Header) error
|
|
|
|
}
|