Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/fix restore single file backup command #911

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ coverage/
.cache
_public/
_archive/
tmp/

.github/release-notes.md

Expand Down
27 changes: 27 additions & 0 deletions e2e/definitions/restore/restore-backupcommand.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: k8up.io/v1
kind: Restore
metadata:
name: k8up-restore-backupcommand
namespace: k8up-e2e-subject
spec:
snapshot: $SNAPSHOT_ID
failedJobsHistoryLimit: 1
successfulJobsHistoryLimit: 1
restoreMethod:
folder:
claimName: subject-pvc
backend:
repoPasswordSecretRef:
name: backup-repo
key: password
s3:
endpoint: http://minio.minio.svc.cluster.local:9000
bucket: backup
accessKeyIDSecretRef:
name: backup-credentials
key: username
secretAccessKeySecretRef:
name: backup-credentials
key: password
podSecurityContext:
runAsUser: $ID
2 changes: 1 addition & 1 deletion e2e/kind.mk
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ $(KIND_KUBECONFIG): $(KIND)
# Applies local-path-config.yaml to kind cluster and forces restart of provisioner - can be simplified once https://github.com/kubernetes-sigs/kind/pull/3090 is merged.
# This is necessary due to the multi node cluster. Classic k8s hostPath provisioner doesn't permit multi node and sharedFileSystemPath support is only in local-path-provisioner v0.0.23.
@kubectl apply -n local-path-storage -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.23/deploy/local-path-storage.yaml
@kubectl get cm -n local-path-storage local-path-config -o yaml|yq e '.data."config.json"="{\"nodePathMap\":[],\"sharedFileSystemPath\": \"/tmp/e2e/local-path-provisioner\"}"'|kubectl apply -f -
@kubectl get cm -n local-path-storage local-path-config -o yaml|yq $(yq --help | grep -q eval && echo e) '.data."config.json"="{\"nodePathMap\":[],\"sharedFileSystemPath\": \"/tmp/e2e/local-path-provisioner\"}"'|kubectl apply -f -
@kubectl delete po -n local-path-storage --all

$(KIND): export GOBIN = $(go_bin)
Expand Down
47 changes: 47 additions & 0 deletions e2e/test-08-restore-backupcommand.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bats

load "lib/utils"
load "lib/detik"
load "lib/k8up"

# shellcheck disable=SC2034
DETIK_CLIENT_NAME="kubectl"
# shellcheck disable=SC2034
DETIK_CLIENT_NAMESPACE="k8up-e2e-subject"
# shellcheck disable=SC2034
DEBUG_DETIK="true"

@test "Given a PVC, When creating a Backup of an annotated app, Then Restore stdout dump to PVC" {
expected_content="expected content: $(timestamp)"
expected_filename="expected_filename.txt"

given_a_running_operator
given_a_clean_ns
given_s3_storage
given_an_annotated_subject "${expected_filename}" "${expected_content}"

kubectl apply -f definitions/secrets
yq e '.spec.podSecurityContext.runAsUser='$(id -u)'' definitions/backup/backup.yaml | kubectl apply -f -

try "at most 10 times every 5s to get backup named 'k8up-backup' and verify that '.status.started' is 'true'"
verify_object_value_by_label job 'k8up.io/owned-by=backup_k8up-backup' '.status.active' 1 true

wait_until backup/k8up-backup completed

run restic snapshots

echo "---BEGIN restic snapshots output---"
echo "${output}" | jq .
echo "---END---"

echo -n "Number of Snapshots >= 1? "
jq -e 'length >= 1' <<< "${output}" # Ensure that there was actually a backup created

run get_latest_snap_by_path /k8up-e2e-subject-subject-container.txt

yq e '.spec.snapshot="'${output}'" | .spec.podSecurityContext.runAsUser='$(id -u)'' definitions/restore/restore-backupcommand.yaml | kubectl apply -f -
wait_until restore/k8up-restore-backupcommand completed
verify "'.status.conditions[?(@.type==\"Completed\")].reason' is 'Succeeded' for Restore named 'k8up-restore-backupcommand'"

expect_file_in_container 'deploy/annotated-subject-deployment' 'subject-container' "/data/k8up-e2e-subject-subject-container.txt" "${expected_content}"
}
64 changes: 63 additions & 1 deletion restic/cli/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,13 @@ func (r *Restic) getLatestSnapshot(snapshotID string, log logr.Logger) (dto.Snap

func (r *Restic) folderRestore(restoreDir string, snapshot dto.Snapshot, restoreFilter string, verify bool, log logr.Logger) error {
var linkedDir string
if cfg.Config.RestoreTrimPath {

singleFile, err := r.isRestoreSingleFile(log, snapshot)
if err != nil {
return err
}

if !singleFile && cfg.Config.RestoreTrimPath {
restoreRoot, err := r.linkRestorePaths(snapshot, restoreDir)
if err != nil {
return err
Expand Down Expand Up @@ -221,6 +227,62 @@ func (r *Restic) linkRestorePaths(snapshot dto.Snapshot, restoreDir string) (str
return restoreRoot, nil
}

func (r *Restic) isRestoreSingleFile(log logr.Logger, snapshot dto.Snapshot) (bool, error) {
buf := bytes.Buffer{}

opts := CommandOptions{
Path: r.resticPath,
Args: r.globalFlags.ApplyToCommand("ls", "--json", snapshot.ID),
StdOut: &buf,
}

cmd := NewCommand(r.ctx, log, opts)
cmd.Run()
capturedStdOut := buf.String()

stdOutLines := strings.Split(capturedStdOut, "\n")

if len(stdOutLines) == 0 {
err := fmt.Errorf("no list exist for snapshot %v", snapshot.ID)
log.Error(err, "the snapshot list is empty")
return false, err
}

err := json.Unmarshal([]byte(stdOutLines[0]), &dto.Snapshot{})
if err != nil {
return false, err
}

count := 0
for i := 1; i < len(stdOutLines); i++ {
if len(stdOutLines[i]) == 0 {
continue
}

node := &fileNode{}
err := json.Unmarshal([]byte(stdOutLines[i]), node)
if err != nil {
return false, err
}
if node.Type == "file" {
count++
}
if node.Type == "dir" {
count = 0
break
}
if count >= 2 {
break
}
}

if count == 1 {
return true, nil
}

return false, nil
}

func (r *Restic) parsePath(paths []string) string {
return path.Base(paths[len(paths)-1])
}
Expand Down
Loading