Skip to content

Commit bb70922

Browse files
stubbiclaude
andcommitted
feat(resources): BuildRestoreInitContainer (returns nil when no restore needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e356a37 commit bb70922

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

internal/resources/restore_init.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package resources
2+
3+
import (
4+
"fmt"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
8+
hermesv1 "github.com/stubbi/hermes-operator/api/v1"
9+
)
10+
11+
// BuildRestoreInitContainer returns the init container that restores a snapshot
12+
// into the PVC. Returns nil when no restore is requested or one already finished.
13+
func BuildRestoreInitContainer(inst *hermesv1.HermesInstance) *corev1.Container {
14+
if inst.Spec.RestoreFrom == "" {
15+
return nil
16+
}
17+
if inst.Status.RestoredFrom == inst.Spec.RestoreFrom {
18+
return nil
19+
}
20+
if inst.Spec.Backup.S3 == nil {
21+
return nil
22+
}
23+
24+
image := inst.Spec.Backup.Image
25+
if image == "" {
26+
image = ResticImage
27+
}
28+
region := inst.Spec.Backup.S3.Region
29+
30+
args := []string{
31+
"-c",
32+
fmt.Sprintf(
33+
`set -euo pipefail
34+
SNAPSHOT_KEY=%q
35+
DEST=/home/hermes/.hermes
36+
if [ -n "$(ls -A "$DEST" 2>/dev/null)" ] && [ -z "${HERMES_RESTORE_FORCE:-}" ]; then
37+
echo "ERROR: restore destination $DEST is not empty; refusing to overwrite. Set HERMES_RESTORE_FORCE=1 to override." >&2
38+
exit 1
39+
fi
40+
restic --repo "$RESTIC_REPOSITORY" --no-cache dump latest "$SNAPSHOT_KEY" \
41+
| zstd -d \
42+
| tar -xf - -C "$DEST"
43+
echo "restore complete: $SNAPSHOT_KEY -> $DEST" >&2
44+
`,
45+
inst.Spec.RestoreFrom,
46+
),
47+
}
48+
49+
return &corev1.Container{
50+
Name: "init-restore",
51+
Image: image,
52+
ImagePullPolicy: corev1.PullIfNotPresent,
53+
Command: []string{"/bin/sh"},
54+
Args: args,
55+
TerminationMessagePath: "/dev/termination-log",
56+
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
57+
Env: []corev1.EnvVar{
58+
{Name: "RESTIC_REPOSITORY", Value: resticRepo(inst.Spec.Backup.S3)},
59+
{Name: "AWS_DEFAULT_REGION", Value: region},
60+
},
61+
EnvFrom: []corev1.EnvFromSource{{
62+
SecretRef: &corev1.SecretEnvSource{
63+
LocalObjectReference: corev1.LocalObjectReference{Name: s3CredsSecretName(inst)},
64+
},
65+
}},
66+
VolumeMounts: []corev1.VolumeMount{
67+
{Name: "data", MountPath: "/home/hermes/.hermes"},
68+
},
69+
SecurityContext: &corev1.SecurityContext{
70+
AllowPrivilegeEscalation: Ptr(false),
71+
ReadOnlyRootFilesystem: Ptr(true),
72+
Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
73+
},
74+
}
75+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package resources
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
11+
hermesv1 "github.com/stubbi/hermes-operator/api/v1"
12+
)
13+
14+
func restoreInstance() *hermesv1.HermesInstance {
15+
return &hermesv1.HermesInstance{
16+
ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: "agents"},
17+
Spec: hermesv1.HermesInstanceSpec{
18+
RestoreFrom: "prod/agents/demo/2026-05-10T03-00-00Z.tar.zst",
19+
Backup: hermesv1.BackupSpec{
20+
S3: &hermesv1.BackupS3Spec{
21+
Bucket: "hermes-backups",
22+
Endpoint: "s3.amazonaws.com",
23+
Region: "us-east-1",
24+
CredentialsSecretRef: hermesv1.LocalObjectReference{Name: "hermes-s3-creds"},
25+
},
26+
},
27+
},
28+
}
29+
}
30+
31+
func TestBuildRestoreInitContainer_NameAndImage(t *testing.T) {
32+
c := BuildRestoreInitContainer(restoreInstance())
33+
require.NotNil(t, c)
34+
assert.Equal(t, "init-restore", c.Name)
35+
assert.Equal(t, "restic/restic:0.16.4", c.Image)
36+
}
37+
38+
func TestBuildRestoreInitContainer_EmbedsSnapshotKey(t *testing.T) {
39+
c := BuildRestoreInitContainer(restoreInstance())
40+
joined := strings.Join(c.Args, " ")
41+
assert.Contains(t, joined, "prod/agents/demo/2026-05-10T03-00-00Z.tar.zst")
42+
assert.Contains(t, joined, "/home/hermes/.hermes")
43+
}
44+
45+
func TestBuildRestoreInitContainer_SecurityContext(t *testing.T) {
46+
c := BuildRestoreInitContainer(restoreInstance())
47+
require.NotNil(t, c.SecurityContext)
48+
require.NotNil(t, c.SecurityContext.AllowPrivilegeEscalation)
49+
assert.False(t, *c.SecurityContext.AllowPrivilegeEscalation)
50+
require.NotNil(t, c.SecurityContext.ReadOnlyRootFilesystem)
51+
assert.True(t, *c.SecurityContext.ReadOnlyRootFilesystem)
52+
}
53+
54+
func TestBuildRestoreInitContainer_S3CredsViaEnvFromSecret(t *testing.T) {
55+
c := BuildRestoreInitContainer(restoreInstance())
56+
require.Len(t, c.EnvFrom, 1)
57+
require.NotNil(t, c.EnvFrom[0].SecretRef)
58+
assert.Equal(t, "hermes-s3-creds", c.EnvFrom[0].SecretRef.Name)
59+
}
60+
61+
func TestBuildRestoreInitContainer_VolumeMount(t *testing.T) {
62+
c := BuildRestoreInitContainer(restoreInstance())
63+
require.Len(t, c.VolumeMounts, 1)
64+
vm := c.VolumeMounts[0]
65+
assert.Equal(t, "data", vm.Name)
66+
assert.Equal(t, "/home/hermes/.hermes", vm.MountPath)
67+
}
68+
69+
func TestBuildRestoreInitContainer_NilWhenNoRestore(t *testing.T) {
70+
inst := restoreInstance()
71+
inst.Spec.RestoreFrom = ""
72+
assert.Nil(t, BuildRestoreInitContainer(inst))
73+
}
74+
75+
func TestBuildRestoreInitContainer_NilWhenAlreadyRestored(t *testing.T) {
76+
inst := restoreInstance()
77+
inst.Status.RestoredFrom = inst.Spec.RestoreFrom
78+
assert.Nil(t, BuildRestoreInitContainer(inst))
79+
}

0 commit comments

Comments
 (0)