Skip to content

Commit

Permalink
Add Functionality to use RestoreData using Kopia Server (#1914)
Browse files Browse the repository at this point in the history
* Add RestoreDataUsingKopiaServer function

* Imports sorted

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Change config file and log directory

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Get server address using function

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Addressed Comments

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Addressed Comments

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Add Documentation for RestoreDataUsingKopiaServer

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* clear namespace confusion

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Improvements

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* fix typo

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Add PodOverride functionality

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update pkg/function/restore_data_using_kopia_server.go

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update pkg/function/restore_data_using_kopia_server.go

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Sort Imports in kopia/utils.go

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Remove userCredentials and certData from Blueprint

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Remove image field from Blueprint

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update Documentation

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update Documentation

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Remove repositoryServerAddress

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Add Image Field

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* RepositoryServer support in kando using Interface

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update pkg/function/restore_data_using_kopia_server.go

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update Documentation

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Address Comment

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Address Comment

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Removed Code which is not needed

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Removed Unused constants

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update pkg/function/restore_data_using_kopia_server.go

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Add License

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update docs/functions.rst

Co-authored-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* Address comments

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Fix Lint Problem

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

* Update docs/functions.rst

Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>

---------

Signed-off-by: Rajat Gupta <rajat.gupta@veeam.com>
Co-authored-by: Vivek Singh <vsingh.ggits.2010@gmail.com>
Co-authored-by: Prasad Ghangal <prasad.ghangal@gmail.com>
  • Loading branch information
3 people committed May 3, 2023
1 parent 496f88a commit a3ec049
Show file tree
Hide file tree
Showing 2 changed files with 348 additions and 0 deletions.
76 changes: 76 additions & 0 deletions docs/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,82 @@ Example:
container: kanister-tools
includePath: /mnt/data
.. _restoredatausingkopiaserver:

RestoreDataUsingKopiaServer
---------------------------

This function restores data backed up by the ``BackupDataUsingKopiaServer`` function.
It creates a new Pod that mounts the PVCs referenced by the Pod specified in the
function argument and restores data to the specified path.

.. note::
It is extremely important that, the PVCs are not currently
in use by an active application container, as they are required
to be mounted to the new Pod (ensure by using
``ScaleWorkload`` with replicas=0 first).
For advanced use cases, it is possible to have concurrent access but
the PV needs to have ``RWX`` access mode and the volume needs to use a
clustered file system that supports concurrent access.

.. csv-table::
:header: "Argument", "Required", "Type", "Description"
:align: left
:widths: 5,5,5,15

`namespace`, Yes, `string`, namespace of the application that you want to restore the data in
`image`, Yes, `string`, image to be used for running restore job (should contain kopia binary)
`backupIdentifier`, Yes, `string`, unique snapshot id generated during backup
`restorePath`, Yes, `string`, path where data to be restored
`pod`, No, `string`, pod to which the volumes are attached
`volumes`, No, `map[string]string`, mapping of `pvcName` to `mountPath` under which the volume will be available
`podOverride`, No, `map[string]interface{}`, specs to override default pod specs with

.. note::
The ``image`` argument requires the use of ``ghcr.io/kanisterio/kanister-tools``
image since it includes the required tools to restore data from
the object store.

Either ``pod`` or the ``volumes`` arguments must be specified to this function
based on the function that was used to backup the data.
If `BackupDataUsingKopiaServer` is used to backup the data we should specify `pod` and for
`CopyVolumeDataUsingKopiaServer`, `Volumes` should be specified.

Additionally, in order to use this function, a RepositoryServer CR is required.

Example:

Consider a scenario where you wish to restore the data backed up by the
:ref:`backupdatausingkopiaserver` function. We will first scale down the application,
restore the data and then scale it back up.
For this phase, we will use the ``backupIdentifier`` Artifact provided by
backup function.

.. substitution-code-block:: yaml
:linenos:

- func: ScaleWorkload
name: shutdownPod
args:
namespace: "{{ .Deployment.Namespace }}"
name: "{{ .Deployment.Name }}"
kind: Deployment
replicas: 0
- func: RestoreDataUsingKopiaServer
name: restoreFromS3
args:
namespace: "{{ .Deployment.Namespace }}"
pod: "{{ index .Deployment.Pods 0 }}"
backupIdentifier: "{{ .ArtifactsIn.backupIdentifier.KeyValue.id }}"
restorePath: /mnt/data
- func: ScaleWorkload
name: bringupPod
args:
namespace: "{{ .Deployment.Namespace }}"
name: "{{ .Deployment.Name }}"
kind: Deployment
replicas: 1

Registering Functions
---------------------

Expand Down
272 changes: 272 additions & 0 deletions pkg/function/restore_data_using_kopia_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// Copyright 2023 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"
"fmt"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

kanister "github.com/kanisterio/kanister/pkg"
crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1"
"github.com/kanisterio/kanister/pkg/format"
kankopia "github.com/kanisterio/kanister/pkg/kopia"
kopiacmd "github.com/kanisterio/kanister/pkg/kopia/command"
"github.com/kanisterio/kanister/pkg/kube"
"github.com/kanisterio/kanister/pkg/param"
)

const (
RestoreDataUsingKopiaServerFuncName = "RestoreDataUsingKopiaServer"
// SparseRestoreOption is the key for specifiying whether to do a sparse restore
SparseRestoreOption = "sparseRestore"
)

type restoreDataUsingKopiaServerFunc struct{}

func init() {
_ = kanister.Register(&restoreDataUsingKopiaServerFunc{})
}

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

func (*restoreDataUsingKopiaServerFunc) Name() string {
return RestoreDataUsingKopiaServerFuncName
}

func (*restoreDataUsingKopiaServerFunc) RequiredArgs() []string {
return []string{
RestoreDataBackupIdentifierArg,
RestoreDataNamespaceArg,
RestoreDataRestorePathArg,
RestoreDataImageArg,
}
}

func (*restoreDataUsingKopiaServerFunc) Arguments() []string {
return []string{
RestoreDataBackupIdentifierArg,
RestoreDataNamespaceArg,
RestoreDataRestorePathArg,
RestoreDataPodArg,
RestoreDataVolsArg,
RestoreDataPodOverrideArg,
RestoreDataImageArg,
}
}

func (*restoreDataUsingKopiaServerFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]any) (map[string]any, error) {
var (
err error
image string
namespace string
restorePath string
snapID string
)
if err = Arg(args, RestoreDataBackupIdentifierArg, &snapID); err != nil {
return nil, err
}
if err = Arg(args, RestoreDataNamespaceArg, &namespace); err != nil {
return nil, err
}
if err = Arg(args, RestoreDataRestorePathArg, &restorePath); err != nil {
return nil, err
}
if err = Arg(args, RestoreDataImageArg, &image); err != nil {
return nil, err
}

userPassphrase, cert, err := userCredentialsAndServerTLS(&tp)
if err != nil {
return nil, errors.Wrap(err, "Failed to fetch User Credentials/Certificate Data from Template Params")
}

fingerprint, err := kankopia.ExtractFingerprintFromCertificateJSON(cert)
if err != nil {
return nil, errors.Wrap(err, "Failed to fetch Kopia API Server Certificate Secret Data from Certificate")
}

// Validate and get optional arguments
pod, vols, podOverride, err := validateAndGetOptArgsForRestore(tp, args)
if err != nil {
return nil, err
}

if len(vols) == 0 {
vols, err = FetchPodVolumes(pod, tp)
if err != nil {
return nil, err
}
}

username := tp.RepositoryServer.Username
hostname, userAccessPassphrase, err := hostNameAndUserPassPhraseFromRepoServer(userPassphrase)
if err != nil {
return nil, errors.Wrap(err, "Failed to get hostname/user passphrase from Options")
}

cli, err := kube.NewClient()
if err != nil {
return nil, errors.Wrap(err, "Failed to create Kubernetes client")
}

_, sparseRestore := tp.Options[SparseRestoreOption]

return restoreDataFromServer(
ctx,
cli,
hostname,
image,
restoreDataJobPrefix,
namespace,
restorePath,
tp.RepositoryServer.Address,
fingerprint,
snapID,
username,
userAccessPassphrase,
sparseRestore,
vols,
podOverride,
)
}

func restoreDataFromServer(
ctx context.Context,
cli kubernetes.Interface,
hostname,
image,
jobPrefix,
namespace,
restorePath,
serverAddress,
fingerprint,
snapID,
username,
userPassphrase string,
sparseRestore bool,
vols map[string]string,
podOverride crv1alpha1.JSONMap,
) (map[string]any, error) {
for pvc := range vols {
if _, err := cli.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvc, metav1.GetOptions{}); err != nil {
return nil, errors.Wrap(err, "Failed to retrieve PVC from namespace: "+namespace+" name: "+pvc)
}
}

options := &kube.PodOptions{
Namespace: namespace,
GenerateName: jobPrefix,
Image: image,
Command: []string{"bash", "-c", "tail -f /dev/null"},
Volumes: vols,
PodOverride: podOverride,
}

pr := kube.NewPodRunner(cli, options)
podFunc := restoreDataFromServerPodFunc(
cli,
hostname,
namespace,
restorePath,
serverAddress,
fingerprint,
snapID,
username,
userPassphrase,
sparseRestore,
)
return pr.Run(ctx, podFunc)
}

func restoreDataFromServerPodFunc(
cli kubernetes.Interface,
hostname,
namespace,
restorePath,
serverAddress,
fingerprint,
snapID,
username,
userPassphrase string,
sparseRestore bool,
) func(ctx context.Context, pod *corev1.Pod) (map[string]any, error) {
return func(ctx context.Context, pod *corev1.Pod) (map[string]any, error) {
if err := kube.WaitForPodReady(ctx, cli, pod.Namespace, pod.Name); err != nil {
return nil, errors.Wrap(err, "Failed while waiting for Pod: "+pod.Name+" to be ready")
}

contentCacheMB, metadataCacheMB := kopiacmd.GetCacheSizeSettingsForSnapshot()
configFile, logDirectory := kankopia.CustomConfigFileAndLogDirectory(hostname)

cmd := kopiacmd.RepositoryConnectServerCommand(
kopiacmd.RepositoryServerCommandArgs{
UserPassword: userPassphrase,
ConfigFilePath: configFile,
LogDirectory: logDirectory,
CacheDirectory: kopiacmd.DefaultCacheDirectory,
Hostname: hostname,
ServerURL: serverAddress,
Fingerprint: fingerprint,
Username: username,
ContentCacheMB: contentCacheMB,
MetadataCacheMB: metadataCacheMB,
})
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.Wrap(err, "Failed to connect to Kopia API server")
}

cmd = kopiacmd.SnapshotRestore(
kopiacmd.SnapshotRestoreCommandArgs{
CommandArgs: &kopiacmd.CommandArgs{
RepoPassword: "",
ConfigFilePath: configFile,
LogDirectory: logDirectory,
},
SnapID: snapID,
TargetPath: restorePath,
SparseRestore: sparseRestore,
IgnorePermissionErrors: true,
})
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)
return nil, errors.Wrap(err, "Failed to restore backup from Kopia API server")
}
}

func validateAndGetOptArgsForRestore(tp param.TemplateParams, args map[string]any) (pod string, vols map[string]string, podOverride crv1alpha1.JSONMap, err error) {
if err = OptArg(args, RestoreDataPodArg, &pod, ""); err != nil {
return pod, vols, podOverride, err
}
if err = OptArg(args, RestoreDataVolsArg, &vols, nil); err != nil {
return pod, vols, podOverride, err
}
if (pod != "") && (len(vols) > 0) {
return pod, vols, podOverride, errors.New(fmt.Sprintf("Exactly one of the %s or %s arguments are required, but both are provided", RestoreDataPodArg, RestoreDataVolsArg))
}
podOverride, err = GetPodSpecOverride(tp, args, RestoreDataPodOverrideArg)
if err != nil {
return pod, vols, podOverride, errors.Wrap(err, "Failed to get Pod Override Specs")
}
return pod, vols, podOverride, nil
}

0 comments on commit a3ec049

Please sign in to comment.