Skip to content

Commit

Permalink
Add DescribeBackups Function (#333)
Browse files Browse the repository at this point in the history
* Add/Update restic func for stats

* Add get_backups func

* Update output for getBackups

* Add test for snapshotIDs parsing

* Add more info to BackupInfo

* Change file name

* Fix merge conflicts

* Update func name:

* Update file name

* Update error check

* Add tests for backupsInfo

* Address review comments

* Address review comments

* Update switch case

* Address review comments

* Address review comments

* Update test

* Fix tests

* Address review comments

* Update tests

* Update stats parsing

* Update test to avoid locking old repo
  • Loading branch information
DeepikaDixit authored and mergify[bot] committed Oct 10, 2019
1 parent 3baeb82 commit 5b743d9
Show file tree
Hide file tree
Showing 5 changed files with 371 additions and 10 deletions.
3 changes: 2 additions & 1 deletion pkg/function/backup_data_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
BackupDataStatsOutputFileCount = "fileCount"
BackupDataStatsOutputSize = "size"
BackupDataStatsOutputMode = "mode"
defaultStatsMode = "restore-size"
)

func init() {
Expand Down Expand Up @@ -116,7 +117,7 @@ func (*BackupDataStatsFunc) Exec(ctx context.Context, tp param.TemplateParams, a
if err = Arg(args, BackupDataStatsBackupIdentifierArg, &backupID); err != nil {
return nil, err
}
if err = OptArg(args, BackupDataStatsMode, &mode, "restore-size"); err != nil {
if err = OptArg(args, BackupDataStatsMode, &mode, defaultStatsMode); err != nil {
return nil, err
}
if err = OptArg(args, BackupDataStatsEncryptionKeyArg, &encryptionKey, restic.GeneratePassword()); err != nil {
Expand Down
77 changes: 77 additions & 0 deletions pkg/function/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"

. "gopkg.in/check.v1"
v1 "k8s.io/api/core/v1"
Expand All @@ -34,6 +35,7 @@ import (
"github.com/kanisterio/kanister/pkg/objectstore"
"github.com/kanisterio/kanister/pkg/param"
"github.com/kanisterio/kanister/pkg/resource"
"github.com/kanisterio/kanister/pkg/restic"
"github.com/kanisterio/kanister/pkg/testutil"
)

Expand Down Expand Up @@ -98,6 +100,9 @@ func (s *DataSuite) SetUpSuite(c *C) {
location.Prefix = "testBackupRestoreLocDelete"
location.Bucket = testBucketName
s.profile = testutil.ObjectStoreProfileOrSkip(c, s.providerType, location)

os.Setenv("POD_NAMESPACE", s.namespace)
os.Setenv("POD_SERVICE_ACCOUNT", "default")
}

func (s *DataSuite) TearDownSuite(c *C) {
Expand Down Expand Up @@ -165,6 +170,26 @@ func newBackupDataBlueprint() *crv1alpha1.Blueprint {
}
}

func newDescribeBackupsBlueprint() *crv1alpha1.Blueprint {
return &crv1alpha1.Blueprint{
Actions: map[string]*crv1alpha1.BlueprintAction{
"describeBackups": &crv1alpha1.BlueprintAction{
Kind: param.StatefulSetKind,
Phases: []crv1alpha1.BlueprintPhase{
crv1alpha1.BlueprintPhase{
Name: "testDescribeBackups",
Func: "DescribeBackups",
Args: map[string]interface{}{
DescribeBackupsArtifactPrefixArg: "{{ .Profile.Location.Bucket }}/{{ .Profile.Location.Prefix }}",
DescribeBackupsEncryptionKeyArg: "{{ .Secrets.backupKey.Data.password | toString }}",
},
},
},
},
},
}
}

func newLocationDeleteBlueprint() *crv1alpha1.Blueprint {
return &crv1alpha1.Blueprint{
Actions: map[string]*crv1alpha1.BlueprintAction{
Expand Down Expand Up @@ -539,3 +564,55 @@ func (s *DataSuite) initPVCTemplateParams(c *C, pvc *v1.PersistentVolumeClaim, o
tp.Profile = s.profile
return tp
}
func (s *DataSuite) TestDescribeBackups(c *C) {
tp, _ := s.getTemplateParamsAndPVCName(c, 1)

// Test backup
bp := *newBackupDataBlueprint()
out := runAction(c, bp, "backup", tp)
c.Assert(out[BackupDataOutputBackupID].(string), Not(Equals), "")
c.Assert(out[BackupDataOutputBackupTag].(string), Not(Equals), "")

// Test DescribeBackups
bp2 := *newDescribeBackupsBlueprint()
out2 := runAction(c, bp2, "describeBackups", tp)
c.Assert(out2[DescribeBackupsSnapshotIDs], NotNil)
c.Assert(out2[DescribeBackupsFileCount].(string), Not(Equals), "")
c.Assert(out2[DescribeBackupsSize].(string), Not(Equals), "")
c.Assert(out2[DescribeBackupsPasswordIncorrect].(string), Not(Equals), "")
c.Assert(out2[DescribeBackupsRepoDoesNotExist].(string), Not(Equals), "")
}

func (s *DataSuite) TestDescribeBackupsWrongPassword(c *C) {
tp, _ := s.getTemplateParamsAndPVCName(c, 1)

// Test backup
bp := *newBackupDataBlueprint()
bp.Actions["backup"].Phases[0].Args[BackupDataBackupArtifactPrefixArg] = "abc-foo-bar"
bp.Actions["backup"].Phases[0].Args[BackupDataEncryptionKeyArg] = restic.GeneratePassword()
out := runAction(c, bp, "backup", tp)
c.Assert(out[BackupDataOutputBackupID].(string), Not(Equals), "")
c.Assert(out[BackupDataOutputBackupTag].(string), Not(Equals), "")

// Test DescribeBackups
bp2 := *newDescribeBackupsBlueprint()
bp2.Actions["describeBackups"].Phases[0].Args[DescribeBackupsArtifactPrefixArg] = "abc-foo-bar"
out2 := runAction(c, bp2, "describeBackups", tp)
c.Assert(out2[DescribeBackupsPasswordIncorrect].(string), Equals, "true")
}

func (s *DataSuite) TestDescribeBackupsRepoNotAvailable(c *C) {
tp, _ := s.getTemplateParamsAndPVCName(c, 1)

// Test backup
bp := *newBackupDataBlueprint()
out := runAction(c, bp, "backup", tp)
c.Assert(out[BackupDataOutputBackupID].(string), Not(Equals), "")
c.Assert(out[BackupDataOutputBackupTag].(string), Not(Equals), "")

// Test DescribeBackups
bp2 := *newDescribeBackupsBlueprint()
bp2.Actions["describeBackups"].Phases[0].Args[DescribeBackupsArtifactPrefixArg] = "foobar"
out2 := runAction(c, bp2, "describeBackups", tp)
c.Assert(out2[DescribeBackupsRepoDoesNotExist].(string), Equals, "true")
}
161 changes: 161 additions & 0 deletions pkg/function/describe_backups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2019 The Kanister Authors.
//
// 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 function

import (
"context"
"strings"

"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
sp "k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes"

kanister "github.com/kanisterio/kanister/pkg"
"github.com/kanisterio/kanister/pkg/format"
"github.com/kanisterio/kanister/pkg/kube"
"github.com/kanisterio/kanister/pkg/param"
"github.com/kanisterio/kanister/pkg/restic"
)

const (
// DescribeBackupsArtifactPrefixArg provides the path to restore backed up data
DescribeBackupsArtifactPrefixArg = "backupArtifactPrefix"
// DescribeBackupsEncryptionKeyArg provides the encryption key to be used for deletes
DescribeBackupsEncryptionKeyArg = "encryptionKey"
// DescribeBackupsPodOverrideArg contains pod specs to override default pod specs
DescribeBackupsPodOverrideArg = "podOverride"
DescribeBackupsJobPrefix = "describe-backups-"
DescribeBackupsFileCount = "fileCount"
DescribeBackupsSize = "size"
DescribeBackupsSnapshotIDs = "snapshotIDs"
DescribeBackupsPasswordIncorrect = "passwordIncorrect"
DescribeBackupsRepoDoesNotExist = "repoUnavailable"
rawDataStatsMode = "raw-data"
)

func init() {
kanister.Register(&DescribeBackupsFunc{})
}

var _ kanister.Func = (*DescribeBackupsFunc)(nil)

type DescribeBackupsFunc struct{}

func (*DescribeBackupsFunc) Name() string {
return "DescribeBackups"
}

func describeBackups(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, encryptionKey, targetPaths, jobPrefix string, podOverride sp.JSONMap) (map[string]interface{}, error) {
namespace, err := kube.GetControllerNamespace()
if err != nil {
return nil, errors.Wrapf(err, "Failed to get controller namespace")
}
options := &kube.PodOptions{
Namespace: namespace,
GenerateName: jobPrefix,
Image: kanisterToolsImage,
Command: []string{"sh", "-c", "tail -f /dev/null"},
PodOverride: podOverride,
}
pr := kube.NewPodRunner(cli, options)
podFunc := describeBackupsPodFunc(cli, tp, namespace, encryptionKey, targetPaths)
return pr.Run(ctx, podFunc)
}

func describeBackupsPodFunc(cli kubernetes.Interface, tp param.TemplateParams, namespace, encryptionKey, targetPath string) func(ctx context.Context, pod *v1.Pod) (map[string]interface{}, error) {
return func(ctx context.Context, pod *v1.Pod) (map[string]interface{}, error) {
// Wait for pod to reach running state
if err := kube.WaitForPodReady(ctx, cli, pod.Namespace, pod.Name); err != nil {
return nil, errors.Wrapf(err, "Failed while waiting for Pod %s to be ready", pod.Name)
}
pw, err := getPodWriter(cli, ctx, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name, tp.Profile)
if err != nil {
return nil, err
}
defer cleanUpCredsFile(ctx, pw, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)
snapshotIDs, err := restic.GetSnapshotIDs(tp.Profile, cli, targetPath, encryptionKey, namespace, pod.Name, pod.Spec.Containers[0].Name)
switch {
case err == nil:
break
case strings.Contains(err.Error(), restic.PasswordIncorrect):
return map[string]interface{}{
DescribeBackupsPasswordIncorrect: "true",
},
nil

case strings.Contains(err.Error(), restic.RepoDoesNotExist):
return map[string]interface{}{
DescribeBackupsRepoDoesNotExist: "true",
},
nil
default:
return nil, err

}
cmd, err := restic.StatsCommandByID(tp.Profile, targetPath, "" /* get all snapshot stats */, rawDataStatsMode, encryptionKey)
if err != nil {
return nil, err
}
stdout, stderr, err := kube.Exec(cli, namespace, pod.Name, pod.Spec.Containers[0].Name, cmd, nil)
format.Log(pod.Name, pod.Spec.Containers[0].Name, stdout)
format.Log(pod.Name, pod.Spec.Containers[0].Name, stderr)
if err != nil {
return nil, errors.Wrapf(err, "Failed to get backup stats")
}
// Get File Count and Size from Stats
_, fc, size := restic.SnapshotStatsFromStatsLog(stdout)
if fc == "" || size == "" {
return nil, errors.New("Failed to parse snapshot stats from logs")
}
return map[string]interface{}{
DescribeBackupsSnapshotIDs: snapshotIDs,
DescribeBackupsFileCount: fc,
DescribeBackupsSize: size,
DescribeBackupsPasswordIncorrect: "false",
DescribeBackupsRepoDoesNotExist: "false",
},
nil
}
}

func (*DescribeBackupsFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) {
var getDescribeBackupsArtifactPrefix, encryptionKey string
var err error
if err = Arg(args, DescribeBackupsArtifactPrefixArg, &getDescribeBackupsArtifactPrefix); err != nil {
return nil, err
}
if err = OptArg(args, DescribeBackupsEncryptionKeyArg, &encryptionKey, restic.GeneratePassword()); err != nil {
return nil, err
}
podOverride, err := GetPodSpecOverride(tp, args, DescribeBackupsPodOverrideArg)
if err != nil {
return nil, err
}

// Validate profile
if err = validateProfile(tp.Profile); err != nil {
return nil, err
}
cli, err := kube.NewClient()
if err != nil {
return nil, errors.Wrapf(err, "Failed to create Kubernetes client")
}
return describeBackups(ctx, cli, tp, encryptionKey, getDescribeBackupsArtifactPrefix, DescribeBackupsJobPrefix, podOverride)
}

func (*DescribeBackupsFunc) RequiredArgs() []string {
return []string{DescribeBackupsArtifactPrefixArg}
}
Loading

0 comments on commit 5b743d9

Please sign in to comment.