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

feat(snapshots): GetBackup details endpoint should support new dr #5058

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
4 changes: 3 additions & 1 deletion .github/actions/build-custom-image-with-apko/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ runs:
export GIT_TAG=${{ inputs.git-tag }}
envsubst '${GIT_TAG}' < ${{ inputs.context }}/apko.yaml.tmpl > ${{ inputs.context }}/apko.yaml

- uses: chainguard-images/actions/apko-publish@main
# pin to work around https://github.com/chainguard-images/actions/issues/160
- uses: chainguard-images/actions/apko-publish@49e3bc2feb790da6c3a7f749b38c769174c4ad54
with:
apko-image: ghcr.io/wolfi-dev/sdk@sha256:b3c690e2aff7a6e6259632047b5d6133169204f7011b9936731cf3f403d5a8c1
config: ${{ inputs.context }}/apko.yaml
archs: amd64,arm64
tag: ${{ inputs.image-name }}
Expand Down
4 changes: 3 additions & 1 deletion .github/actions/build-dep-image-with-apko/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ runs:
echo "image-exists=false" >> "$GITHUB_OUTPUT"
fi

- uses: chainguard-images/actions/apko-publish@main
# pin to work around https://github.com/chainguard-images/actions/issues/160
- uses: chainguard-images/actions/apko-publish@49e3bc2feb790da6c3a7f749b38c769174c4ad54
if: ${{ inputs.overwrite == 'true' || steps.check-image-exists.outputs.image-exists == 'false' }}
with:
apko-image: ghcr.io/wolfi-dev/sdk@sha256:b3c690e2aff7a6e6259632047b5d6133169204f7011b9936731cf3f403d5a8c1
config: ${{ inputs.apko-config }}
archs: amd64,arm64
tag: ${{ inputs.image-name }}
Expand Down
10 changes: 5 additions & 5 deletions pkg/handlers/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,22 @@ func (h *Handler) ListInstanceBackups(w http.ResponseWriter, r *http.Request) {
}

type GetBackupResponse struct {
BackupDetail *snapshottypes.BackupDetail `json:"backupDetail"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
BackupDetails []snapshottypes.BackupDetail `json:"backupDetails"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}

func (h *Handler) GetBackup(w http.ResponseWriter, r *http.Request) {
getBackupResponse := GetBackupResponse{}

backup, err := snapshot.GetBackupDetail(r.Context(), util.PodNamespace, mux.Vars(r)["snapshotName"])
backups, err := snapshot.GetBackupDetail(r.Context(), util.PodNamespace, mux.Vars(r)["snapshotName"])
if err != nil {
logger.Error(err)
getBackupResponse.Error = "failed to get backup detail"
JSON(w, 500, getBackupResponse)
return
}
getBackupResponse.BackupDetail = backup
getBackupResponse.BackupDetails = backups

getBackupResponse.Success = true

Expand Down
51 changes: 45 additions & 6 deletions pkg/kotsadmsnapshot/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,7 @@ func HasUnfinishedInstanceBackup(ctx context.Context, kotsadmNamespace string) (
return false, nil
}

func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID string) (*types.BackupDetail, error) {
func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupName string) ([]types.BackupDetail, error) {
cfg, err := k8sutil.GetClusterConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to get cluster config")
Expand All @@ -1195,20 +1195,59 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri

veleroNamespace := backendStorageLocation.Namespace

backup, err := veleroClient.Backups(veleroNamespace).Get(ctx, backupID, metav1.GetOptions{})
backups, err := listBackupsByName(ctx, veleroClient, veleroNamespace, backupName)
if err != nil {
return nil, errors.Wrap(err, "failed to get backup")
}

results := []types.BackupDetail{}

for _, backup := range backups {
result, err := getBackupDetailForBackup(ctx, veleroClient, veleroNamespace, backup)
if err != nil {
return nil, fmt.Errorf("failed to get backup detail for backup %s: %w", backup.Name, err)
}

results = append(results, *result)
}

return results, nil
}

// listBackupsByName returns a list of backups for the specified backup name. First it tries to get
// the backup by the replicated.com/backup-name label, and if that fails, it tries to get the
// backup by the metadata name.
func listBackupsByName(ctx context.Context, veleroClient veleroclientv1.VeleroV1Interface, veleroNamespace string, backupName string) ([]velerov1.Backup, error) {
// first try to get the backup from the backup-name label
backupList, err := veleroClient.Backups(veleroNamespace).List(ctx, metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", types.InstanceBackupNameLabel, velerolabel.GetValidName(backupName)),
})
if err != nil {
return nil, fmt.Errorf("failed to list backups by label: %w", err)
}
if len(backupList.Items) > 0 {
return backupList.Items, nil
}
backup, err := veleroClient.Backups(veleroNamespace).Get(ctx, backupName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get backup by name: %w", err)
}

return []velerov1.Backup{*backup}, nil
}

// getBackupDetailForBackup returns a BackupDetail object for the specified backup.
func getBackupDetailForBackup(ctx context.Context, veleroClient veleroclientv1.VeleroV1Interface, veleroNamespace string, backup velerov1.Backup) (*types.BackupDetail, error) {
backupVolumes, err := veleroClient.PodVolumeBackups(veleroNamespace).List(ctx, metav1.ListOptions{
LabelSelector: fmt.Sprintf("velero.io/backup-name=%s", velerolabel.GetValidName(backupID)),
LabelSelector: fmt.Sprintf("velero.io/backup-name=%s", velerolabel.GetValidName(backup.Name)),
})
if err != nil {
return nil, errors.Wrap(err, "failed to list volumes")
}

result := &types.BackupDetail{
result := types.BackupDetail{
Name: backup.Name,
Type: types.GetInstanceBackupType(backup),
Status: string(backup.Status.Phase),
Namespaces: backup.Spec.IncludedNamespaces,
Volumes: listBackupVolumes(backupVolumes.Items),
Expand All @@ -1221,7 +1260,7 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri
result.VolumeSizeHuman = units.HumanSize(float64(totalBytesDone)) // TODO: should this be TotalBytes rather than BytesDone?

if backup.Status.Phase == velerov1.BackupPhaseCompleted || backup.Status.Phase == velerov1.BackupPhasePartiallyFailed || backup.Status.Phase == velerov1.BackupPhaseFailed {
errs, warnings, execs, err := downloadBackupLogs(ctx, veleroNamespace, backupID)
errs, warnings, execs, err := downloadBackupLogs(ctx, veleroNamespace, backup.Name)
result.Errors = errs
result.Warnings = warnings
result.Hooks = execs
Expand All @@ -1231,7 +1270,7 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri
}
}

return result, nil
return &result, nil
}

func listBackupVolumes(backupVolumes []velerov1.PodVolumeBackup) []types.SnapshotVolume {
Expand Down
188 changes: 188 additions & 0 deletions pkg/kotsadmsnapshot/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3673,3 +3673,191 @@ func TestDeleteBackup(t *testing.T) {
})
}
}

func TestGetBackupDetail(t *testing.T) {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
velerov1.AddToScheme(scheme)

objects := []runtime.Object{
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "kotsadm-velero-namespace",
},
Data: map[string]string{
"veleroNamespace": "velero",
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "velero",
Namespace: "velero",
},
},
}

veleroObjects := []runtime.Object{
&velerov1.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "velero",
},
Spec: velerov1.BackupStorageLocationSpec{
Provider: "aws",
Default: true,
},
},
&velerov1.Backup{
TypeMeta: metav1.TypeMeta{
Kind: "Backup",
APIVersion: "velero.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "instance-abcd",
Namespace: "velero",
Labels: map[string]string{
types.InstanceBackupNameLabel: "app-slug-abcd",
},
Annotations: map[string]string{
types.BackupIsECAnnotation: "true",
types.InstanceBackupVersionAnnotation: types.InstanceBackupVersionCurrent,
types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeInfra,
types.InstanceBackupCountAnnotation: "2",
},
CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 3, 0, 0, 0, 0, time.Local)},
},
Spec: velerov1.BackupSpec{
StorageLocation: "default",
IncludedNamespaces: []string{"*"},
},
Status: velerov1.BackupStatus{
Phase: velerov1.BackupPhaseInProgress,
},
},
&velerov1.Backup{
TypeMeta: metav1.TypeMeta{
Kind: "Backup",
APIVersion: "velero.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "application-abcd",
Namespace: "velero",
Labels: map[string]string{
types.InstanceBackupNameLabel: "app-slug-abcd",
},
Annotations: map[string]string{
types.BackupIsECAnnotation: "true",
types.InstanceBackupVersionAnnotation: types.InstanceBackupVersionCurrent,
types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeApp,
types.InstanceBackupCountAnnotation: "2",
},
CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 4, 0, 0, 0, 0, time.Local)},
},
Spec: velerov1.BackupSpec{
StorageLocation: "default",
IncludedNamespaces: []string{"*"},
},
Status: velerov1.BackupStatus{
Phase: velerov1.BackupPhaseInProgress,
},
},
&velerov1.Backup{
TypeMeta: metav1.TypeMeta{
Kind: "Backup",
APIVersion: "velero.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "instance-efgh",
Namespace: "velero",
Annotations: map[string]string{
types.BackupIsECAnnotation: "true",
types.InstanceBackupAnnotation: "true",
// legacy backups do not have the InstanceBackupTypeAnnotation
},
CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 2, 0, 0, 0, 0, time.Local)},
},
Spec: velerov1.BackupSpec{
StorageLocation: "default",
IncludedNamespaces: []string{"*"},
},
Status: velerov1.BackupStatus{
Phase: velerov1.BackupPhaseInProgress,
},
},
}

type args struct {
backupName string
}
tests := []struct {
name string
args args
want []types.BackupDetail
wantErr bool
}{
{
name: "legacy backup by name should return a single backup from metadata.name",
args: args{
backupName: "instance-efgh",
},
want: []types.BackupDetail{
{
Name: "instance-efgh",
Type: types.InstanceBackupTypeLegacy,
Status: "InProgress",
Namespaces: []string{"*"},
VolumeSizeHuman: "0B",
Volumes: []types.SnapshotVolume{},
},
},
},
{
name: "new backup by name label should return multiple backups",
args: args{
backupName: "app-slug-abcd",
},
want: []types.BackupDetail{
{
Name: "application-abcd",
Type: types.InstanceBackupTypeApp,
Status: "InProgress",
Namespaces: []string{"*"},
VolumeSizeHuman: "0B",
Volumes: []types.SnapshotVolume{},
},
{
Name: "instance-abcd",
Type: types.InstanceBackupTypeInfra,
Status: "InProgress",
Namespaces: []string{"*"},
VolumeSizeHuman: "0B",
Volumes: []types.SnapshotVolume{},
},
},
},
{
name: "not found should return an error",
args: args{
backupName: "not-exists",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k8sclient.SetBuilder(&k8sclient.MockBuilder{
Client: fake.NewSimpleClientset(objects...),
})
veleroclient.SetBuilder(&veleroclient.MockBuilder{
Client: velerofake.NewSimpleClientset(veleroObjects...).VeleroV1(),
})
got, err := GetBackupDetail(context.Background(), "velero", tt.args.backupName)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want, got)
})
}
}
4 changes: 4 additions & 0 deletions pkg/kotsadmsnapshot/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
)

const (
// BackupIsECAnnotation is the annotation used to store if the backup is from an EC install.
BackupIsECAnnotation = "kots.io/embedded-cluster"

// InstanceBackupNameLabel is the label used to store the name of the backup for an instance
// backup.
InstanceBackupNameLabel = "replicated.com/backup-name"
Expand Down Expand Up @@ -76,6 +79,7 @@ type Backup struct {

type BackupDetail struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
VolumeSizeHuman string `json:"volumeSizeHuman"`
Namespaces []string `json:"namespaces"`
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/RestoreSnapshotRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class RestoreSnapshotRow extends Component {
if (result.success) {
this.setState({
isLoadingBackupInfo: false,
backupInfo: result.backupDetail,
backupInfo: result.backupDetails?.[0],
backupInfoMsg: "",
});
} else {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/snapshots/SnapshotDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class SnapshotDetails extends Component {
}
const response = await res.json();

const snapshotDetails = response.backupDetail;
const snapshotDetails = response.backupDetails?.[0];

let series = [];
if (!isEmpty(snapshotDetails?.volumes)) {
Expand Down
Loading