Skip to content

Commit 50ec220

Browse files
committed
Migrate vaultwarden to stage/action architecture
Signed-off-by: solidDoWant <fred.heinecke@yahoo.com>
1 parent 5bbb480 commit 50ec220

17 files changed

Lines changed: 2180 additions & 2052 deletions

.mockery.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ packages:
3333
: <<: *baseline_config
3434
interfaces:
3535
CNPGRestoreInterface:
36+
? github.com/solidDoWant/backup-tool/pkg/disasterrecovery/actions/remote/files/backup
37+
: <<: *baseline_config
38+
interfaces:
39+
FilesBackupInterface:
40+
? github.com/solidDoWant/backup-tool/pkg/disasterrecovery/actions/remote/files/restore
41+
: <<: *baseline_config
42+
interfaces:
43+
FilesRestoreInterface:
3644
github.com/solidDoWant/backup-tool/pkg/disasterrecovery/actions/remote/s3sync:
3745
<<: *baseline_config
3846
interfaces:

CLAUDE.md

Lines changed: 9 additions & 7 deletions
Large diffs are not rendered by default.

pkg/disasterrecovery/actions/remote/cnpg/backup/cnpgbackup.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,20 @@ type baseBackupState struct {
126126

127127
// BeforeConsistencyPoint takes the base backup that fixes this cluster's recoverable state. It runs
128128
// before the stage establishes the event's shared consistency point, so that point lands after this
129-
// backup completes and the clone can recover forward to it. The base backup is owned by this action and
130-
// torn down in Cleanup; it must outlive clone creation, since the clone's recovery volume snapshots are
131-
// owned by it. Implements remote.PreConsistencyPointAction.
132-
func (bs *baseBackupState) BeforeConsistencyPoint(ctx *contexts.Context) (err error) {
129+
// backup completes and the clone can recover forward to it. The base backup pins no instant of its own —
130+
// it only needs to precede the point — so it returns the zero time. The base backup is owned by this
131+
// action and torn down in Cleanup; it must outlive clone creation, since the clone's recovery volume
132+
// snapshots are owned by it. Implements remote.PreConsistencyPointAction.
133+
func (bs *baseBackupState) BeforeConsistencyPoint(ctx *contexts.Context) (_ time.Time, err error) {
133134
bs.ctxLogWith(ctx).Info("Taking base backup for CNPG backup")
134135
defer ctx.Log.Info("CNPG base backup complete", ctx.Stopwatch.Keyval(), contexts.ErrorKeyvals(&err))
135136

136137
if !bs.isValidated {
137-
return trace.Errorf("attempted to create base backup without validating")
138+
return time.Time{}, trace.Errorf("attempted to create base backup without validating")
138139
}
139140

140141
if bs.isBaseBackedUp {
141-
return trace.Errorf("attempted to create base backup multiple times")
142+
return time.Time{}, trace.Errorf("attempted to create base backup multiple times")
142143
}
143144

144145
if bs.opts.CloningOpts.CleanupTimeout == 0 {
@@ -147,13 +148,13 @@ func (bs *baseBackupState) BeforeConsistencyPoint(ctx *contexts.Context) (err er
147148

148149
backup, err := bs.kubeClusterClient.CreateClusterBackup(ctx.Child(), bs.namespace, bs.clusterName, bs.opts.CloningOpts)
149150
if err != nil {
150-
return trace.Wrap(err, "failed to back up cluster %q", bs.clusterName)
151+
return time.Time{}, trace.Wrap(err, "failed to back up cluster %q", bs.clusterName)
151152
}
152153

153154
bs.baseBackup = backup
154155
bs.isBaseBackedUp = true
155156

156-
return nil
157+
return time.Time{}, nil
157158
}
158159

159160
// SetConsistencyPoint records the shared consistency point established by the stage. Implements

pkg/disasterrecovery/actions/remote/cnpg/backup/cnpgbackup_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,15 @@ func TestBeforeConsistencyPoint(t *testing.T) {
375375
})
376376
}
377377

378-
err := currentState.BeforeConsistencyPoint(ctx)
378+
pinnedTime, err := currentState.BeforeConsistencyPoint(ctx)
379379
if th.ErrExpected(tt.notValidated, tt.alreadyBackedUp, tt.simulateBackupErr) {
380380
assert.Error(t, err)
381381
return
382382
}
383383

384384
require.NoError(t, err)
385+
// The base backup pins no instant of its own — it only needs to precede the consistency point.
386+
assert.True(t, pinnedTime.IsZero())
385387
assert.Equal(t, baseBackup, currentState.baseBackup)
386388
assert.True(t, currentState.isBaseBackedUp)
387389
})

pkg/disasterrecovery/actions/remote/cnpg/backup/cnpgbackupinterface_mock.go

Lines changed: 18 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package backup
2+
3+
import (
4+
"path/filepath"
5+
"time"
6+
7+
"github.com/google/uuid"
8+
"github.com/gravitational/trace"
9+
"github.com/solidDoWant/backup-tool/pkg/cleanup"
10+
"github.com/solidDoWant/backup-tool/pkg/contexts"
11+
"github.com/solidDoWant/backup-tool/pkg/disasterrecovery/actions/remote"
12+
"github.com/solidDoWant/backup-tool/pkg/grpc/clients"
13+
"github.com/solidDoWant/backup-tool/pkg/kubecluster"
14+
"github.com/solidDoWant/backup-tool/pkg/kubecluster/composite/backuptoolinstance"
15+
"github.com/solidDoWant/backup-tool/pkg/kubecluster/composite/clonepvc"
16+
"github.com/solidDoWant/backup-tool/pkg/kubecluster/helpers"
17+
"github.com/solidDoWant/backup-tool/pkg/kubecluster/primatives/core"
18+
corev1 "k8s.io/api/core/v1"
19+
)
20+
21+
type FilesBackupOptions struct {
22+
CleanupTimeout helpers.MaxWaitTime `yaml:"cleanupTimeout,omitempty"`
23+
}
24+
25+
// FilesBackupInterface is a RemoteStage action that captures a live data-directory PVC into the DR
26+
// volume. The source PVC is in use, so it cannot be read directly; the action snapshots/clones it for a
27+
// consistent point-in-time view, syncs the clone's contents into a subdirectory of the DR volume, and
28+
// tears the clone down afterwards (so it is a CleanupAction). A volume snapshot exists only at the moment
29+
// it is taken and cannot be reconstructed for an arbitrary instant, so as a remote.PreConsistencyPointAction
30+
// it takes the clone before the consistency point is fixed and pins the point to the clone's creation time;
31+
// the other captures then align to that filesystem freeze (a database clone recovers forward to it).
32+
type FilesBackupInterface interface {
33+
remote.CleanupAction
34+
remote.PreConsistencyPointAction
35+
Configure(kubeClusterClient kubecluster.ClientInterface, namespace, sourcePVCName, drVolName, backupDirRelPath string, opts FilesBackupOptions) error
36+
}
37+
38+
type configureState struct {
39+
uid string // Unique identifier to prevent accidental collisions between multiple instances
40+
isConfigured bool
41+
kubeClusterClient kubecluster.ClientInterface
42+
namespace string
43+
sourcePVCName string
44+
drVolName string
45+
backupDirRelPath string
46+
opts FilesBackupOptions
47+
}
48+
49+
func (cs *configureState) Configure(kubeClusterClient kubecluster.ClientInterface, namespace, sourcePVCName, drVolName, backupDirRelPath string, opts FilesBackupOptions) error {
50+
if cs.isConfigured {
51+
return trace.Errorf("attempted to configure multiple times")
52+
}
53+
54+
cs.uid = uuid.NewString()
55+
cs.kubeClusterClient = kubeClusterClient
56+
cs.namespace = namespace
57+
cs.sourcePVCName = sourcePVCName
58+
cs.drVolName = drVolName
59+
cs.backupDirRelPath = backupDirRelPath
60+
cs.opts = opts
61+
62+
cs.isConfigured = true
63+
return nil
64+
}
65+
66+
func (cs *configureState) ctxLogWith(ctx *contexts.Context) *contexts.LoggerContext {
67+
return ctx.Log.With("sourcePVC", cs.sourcePVCName, "uid", cs.uid)
68+
}
69+
70+
type validateState struct {
71+
configureState
72+
isValidated bool
73+
}
74+
75+
func (vs *validateState) Validate(ctx *contexts.Context) (err error) {
76+
vs.ctxLogWith(ctx).Info("Validating configuration for files backup")
77+
defer ctx.Log.Info("Completed files backup configuration validation", ctx.Stopwatch.Keyval(), contexts.ErrorKeyvals(&err))
78+
79+
if !vs.isConfigured {
80+
return trace.Errorf("attempted to validate without configuring")
81+
}
82+
83+
if _, err := vs.kubeClusterClient.Core().GetPVC(ctx.Child(), vs.namespace, vs.sourcePVCName); err != nil {
84+
return trace.Wrap(err, "failed to get source data PVC %q", vs.sourcePVCName)
85+
}
86+
87+
if _, err := vs.kubeClusterClient.Core().GetPVC(ctx.Child(), vs.namespace, vs.drVolName); err != nil {
88+
return trace.Wrap(err, "failed to get DR PVC %q", vs.drVolName)
89+
}
90+
91+
vs.isValidated = true
92+
return nil
93+
}
94+
95+
// cloneState holds the PVC clone taken before the consistency point is established.
96+
type cloneState struct {
97+
validateState
98+
clonedPVC *corev1.PersistentVolumeClaim
99+
isCloned bool
100+
}
101+
102+
// BeforeConsistencyPoint clones the source data PVC and returns the clone's creation time, pinning the
103+
// event's consistency point to the moment the filesystem was frozen so the database recovers forward to
104+
// exactly that state. Implements remote.PreConsistencyPointAction.
105+
func (cs *cloneState) BeforeConsistencyPoint(ctx *contexts.Context) (_ time.Time, err error) {
106+
cs.ctxLogWith(ctx).Info("Cloning data directory for files backup")
107+
defer ctx.Log.Info("Files backup clone complete", ctx.Stopwatch.Keyval(), contexts.ErrorKeyvals(&err))
108+
109+
if !cs.isValidated {
110+
return time.Time{}, trace.Errorf("attempted to clone without validating")
111+
}
112+
113+
if cs.isCloned {
114+
return time.Time{}, trace.Errorf("attempted to clone multiple times")
115+
}
116+
117+
// ForceBind is required because the snapshot is deleted once the clone exists, so the clone must bind
118+
// immediately.
119+
clonedPVC, err := cs.kubeClusterClient.ClonePVC(ctx.Child(), cs.namespace, cs.sourcePVCName, clonepvc.ClonePVCOptions{
120+
DestPvcNamePrefix: cs.drVolName,
121+
ForceBind: true,
122+
CleanupTimeout: cs.opts.CleanupTimeout,
123+
})
124+
if err != nil {
125+
return time.Time{}, trace.Wrap(err, "failed to clone source data PVC %q", cs.sourcePVCName)
126+
}
127+
cs.clonedPVC = clonedPVC
128+
cs.isCloned = true
129+
130+
return clonedPVC.CreationTimestamp.Time, nil
131+
}
132+
133+
type setupStateMountPaths struct {
134+
drVolume string
135+
data string
136+
}
137+
138+
type setupState struct {
139+
cloneState
140+
mountPaths setupStateMountPaths
141+
isSetup bool
142+
}
143+
144+
func (ss *setupState) Setup(ctx *contexts.Context, btiOpts *backuptoolinstance.CreateBackupToolInstanceOptions) (err error) {
145+
ss.ctxLogWith(ctx).Info("Setting up for files backup")
146+
defer ctx.Log.Info("Files backup setup complete", ctx.Stopwatch.Keyval(), contexts.ErrorKeyvals(&err))
147+
148+
if !ss.isCloned {
149+
return trace.Errorf("attempted to setup without cloning")
150+
}
151+
152+
if ss.isSetup {
153+
return trace.Errorf("attempted to setup multiple times")
154+
}
155+
156+
baseMountPath := filepath.Join("/mnt", "filesbackup", ss.uid)
157+
ss.mountPaths = setupStateMountPaths{
158+
drVolume: filepath.Join(baseMountPath, "dr"),
159+
data: filepath.Join(baseMountPath, "data"),
160+
}
161+
162+
btiOpts.Volumes = append(btiOpts.Volumes,
163+
core.NewSingleContainerPVC(ss.drVolName, ss.mountPaths.drVolume),
164+
core.NewSingleContainerPVC(ss.clonedPVC.Name, ss.mountPaths.data),
165+
)
166+
167+
ss.isSetup = true
168+
return nil
169+
}
170+
171+
// Cleanup tears down the cloned PVC. It tolerates partial state (e.g. the action never reached Setup
172+
// because another action failed first), deleting nothing in that case.
173+
func (ss *setupState) Cleanup(ctx *contexts.Context) error {
174+
if ss.clonedPVC == nil {
175+
return nil
176+
}
177+
178+
err := cleanup.To(func(ctx *contexts.Context) error {
179+
return ss.kubeClusterClient.Core().DeletePVC(ctx, ss.namespace, ss.clonedPVC.Name)
180+
}).WithErrMessage("failed to cleanup cloned data PVC %q", helpers.FullName(ss.clonedPVC)).
181+
WithParentCtx(ctx).WithTimeout(ss.opts.CleanupTimeout.MaxWait(time.Minute)).
182+
RunError()
183+
return trace.Wrap(err, "failed to cleanup files backup resources")
184+
}
185+
186+
type executeState struct {
187+
setupState
188+
}
189+
190+
func (es *executeState) Execute(ctx *contexts.Context, backupToolClient clients.ClientInterface) (err error) {
191+
es.ctxLogWith(ctx).Info("Executing files backup")
192+
defer ctx.Log.Info("Files backup complete", ctx.Stopwatch.Keyval(), contexts.ErrorKeyvals(&err))
193+
194+
if !es.isSetup {
195+
return trace.Errorf("attempted to execute without setting up")
196+
}
197+
198+
drDataPath := filepath.Join(es.mountPaths.drVolume, es.backupDirRelPath)
199+
err = backupToolClient.Files().SyncFiles(ctx.Child(), es.mountPaths.data, drDataPath)
200+
return trace.Wrap(err, "failed to sync data directory files at %q to the disaster recovery volume at %q", es.mountPaths.data, drDataPath)
201+
}
202+
203+
type FilesBackup struct {
204+
executeState
205+
}
206+
207+
func NewFilesBackup() FilesBackupInterface {
208+
return &FilesBackup{}
209+
}

0 commit comments

Comments
 (0)