Skip to content

Commit 64cfaaf

Browse files
GT-428 Add disallowConcurrent option to ArangoBackupPolicy (#1318)
1 parent f96273b commit 64cfaaf

File tree

8 files changed

+259
-28
lines changed

8 files changed

+259
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- (Maintenance) Generics for type handling
1111
- (Bugfix) Fix creating sync components with EA type set to Managed and headless svc
1212
- (Feature) Check if Volume with LocalStorage is missing
13+
- (Feature) Add disallowConcurrent option to ArangoBackupPolicy
1314

1415
## [1.2.27](https://github.com/arangodb/kube-arangodb/tree/1.2.27) (2023-04-27)
1516
- (Feature) Add InSync Cache

pkg/apis/backup/v1/backup.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -46,6 +46,10 @@ type ArangoBackupList struct {
4646
Items []ArangoBackup `json:"items"`
4747
}
4848

49+
func (a ArangoBackupList) GetItems() []ArangoBackup {
50+
return a.Items
51+
}
52+
4953
// +genclient
5054
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
5155

pkg/apis/backup/v1/backup_policy_spec.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -22,16 +22,26 @@ package v1
2222

2323
import (
2424
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
26+
"github.com/arangodb/kube-arangodb/pkg/util"
2527
)
2628

2729
type ArangoBackupPolicySpec struct {
30+
// Schedule is cron-compatible specification of backup schedule
2831
Schedule string `json:"schedule"`
29-
32+
// AllowConcurrent if true, ArangoBackup will not be created when previous Backups are not finished. Defaults to true
33+
AllowConcurrent *bool `json:"allowConcurrent,omitempty"`
34+
// DeploymentSelector specifies which deployments should get a backup
3035
DeploymentSelector *meta.LabelSelector `json:"selector,omitempty"`
31-
36+
// ArangoBackupTemplate specifies additional options for newly created ArangoBackup
3237
BackupTemplate ArangoBackupTemplate `json:"template"`
3338
}
3439

40+
// GetAllowConcurrent returns AllowConcurrent values. If AllowConcurrent is nil returns true
41+
func (a ArangoBackupPolicySpec) GetAllowConcurrent() bool {
42+
return util.TypeOrDefault(a.AllowConcurrent, true)
43+
}
44+
3545
type ArangoBackupTemplate struct {
3646
Options *ArangoBackupSpecOptions `json:"options,omitempty"`
3747

pkg/apis/backup/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/handlers/policy/handler.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -32,16 +32,21 @@ import (
3232

3333
"github.com/arangodb/kube-arangodb/pkg/apis/backup"
3434
backupApi "github.com/arangodb/kube-arangodb/pkg/apis/backup/v1"
35+
deployment "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
3536
arangoClientSet "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned"
3637
operator "github.com/arangodb/kube-arangodb/pkg/operatorV2"
3738
"github.com/arangodb/kube-arangodb/pkg/operatorV2/event"
3839
"github.com/arangodb/kube-arangodb/pkg/operatorV2/operation"
40+
"github.com/arangodb/kube-arangodb/pkg/util/errors"
41+
"github.com/arangodb/kube-arangodb/pkg/util/globals"
42+
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
3943
)
4044

4145
const (
42-
backupCreated = "ArangoBackupCreated"
43-
policyError = "Error"
44-
rescheduled = "Rescheduled"
46+
backupCreated = "ArangoBackupCreated"
47+
policyError = "Error"
48+
rescheduled = "Rescheduled"
49+
scheduleSkipped = "ScheduleSkipped"
4550
)
4651

4752
type handler struct {
@@ -153,8 +158,25 @@ func (h *handler) processBackupPolicy(policy *backupApi.ArangoBackupPolicy) back
153158
}
154159

155160
for _, deployment := range deployments.Items {
156-
b := policy.NewBackup(deployment.DeepCopy())
161+
depl := deployment.DeepCopy()
162+
163+
if !policy.Spec.GetAllowConcurrent() {
164+
previousBackupInProgress, err := h.isPreviousBackupInProgress(context.Background(), depl, policy.Name)
165+
if err != nil {
166+
h.eventRecorder.Warning(policy, policyError, "Policy Error: %s", err.Error())
167+
return backupApi.ArangoBackupPolicyStatus{
168+
Scheduled: policy.Status.Scheduled,
169+
Message: fmt.Sprintf("backup creation failed: %s", err.Error()),
170+
}
171+
}
172+
if previousBackupInProgress {
173+
eventMsg := fmt.Sprintf("Skipping ArangoBackup creation because earlier backup still running %s/%s", deployment.Namespace, deployment.Name)
174+
h.eventRecorder.Normal(policy, scheduleSkipped, eventMsg)
175+
continue
176+
}
177+
}
157178

179+
b := policy.NewBackup(depl)
158180
if _, err := h.client.BackupV1().ArangoBackups(b.Namespace).Create(context.Background(), b, meta.CreateOptions{}); err != nil {
159181
h.eventRecorder.Warning(policy, policyError, "Policy Error: %s", err.Error())
160182

@@ -183,3 +205,60 @@ func (*handler) CanBeHandled(item operation.Item) bool {
183205
item.Version == backupApi.SchemeGroupVersion.Version &&
184206
item.Kind == backup.ArangoBackupPolicyResourceKind
185207
}
208+
209+
func (h *handler) listAllBackupsForPolicy(ctx context.Context, d *deployment.ArangoDeployment, policyName string) ([]*backupApi.ArangoBackup, error) {
210+
var r []*backupApi.ArangoBackup
211+
212+
if err := k8sutil.APIList[*backupApi.ArangoBackupList](ctx, h.client.BackupV1().ArangoBackups(d.Namespace), meta.ListOptions{
213+
Limit: globals.GetGlobals().Kubernetes().RequestBatchSize().Get(),
214+
}, func(result *backupApi.ArangoBackupList, err error) error {
215+
if err != nil {
216+
return err
217+
}
218+
219+
for _, b := range result.Items {
220+
if b.Spec.PolicyName == nil || *b.Spec.PolicyName != policyName {
221+
continue
222+
}
223+
if b.Spec.Deployment.Name != d.Name {
224+
continue
225+
}
226+
r = append(r, b.DeepCopy())
227+
}
228+
229+
return nil
230+
}); err != nil {
231+
return nil, err
232+
}
233+
234+
return r, nil
235+
}
236+
237+
func (h *handler) isPreviousBackupInProgress(ctx context.Context, d *deployment.ArangoDeployment, policyName string) (bool, error) {
238+
// It would be nice to List CRs with fieldSelector set, but this is not supported:
239+
// https://github.com/kubernetes/kubernetes/issues/53459
240+
// Instead we fetch all ArangoBackups:
241+
backups, err := h.listAllBackupsForPolicy(ctx, d, policyName)
242+
if err != nil {
243+
return false, errors.Wrap(err, "Failed to list ArangoBackups")
244+
}
245+
246+
for _, b := range backups {
247+
// Check if we are in the failed state
248+
switch b.Status.State {
249+
case backupApi.ArangoBackupStateFailed:
250+
continue
251+
}
252+
253+
if b.Spec.Download != nil {
254+
continue
255+
}
256+
257+
// Backup is not yet done
258+
if b.Status.Backup == nil {
259+
return true, nil
260+
}
261+
}
262+
263+
return false, nil
264+
}

pkg/handlers/policy/handler_scheduler_test.go

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
package policy
2222

2323
import (
24+
"context"
2425
"testing"
2526
"time"
2627

@@ -30,6 +31,7 @@ import (
3031

3132
backupApi "github.com/arangodb/kube-arangodb/pkg/apis/backup/v1"
3233
"github.com/arangodb/kube-arangodb/pkg/operatorV2/operation"
34+
"github.com/arangodb/kube-arangodb/pkg/util"
3335
)
3436

3537
func Test_Scheduler_Schedule(t *testing.T) {
@@ -39,7 +41,7 @@ func Test_Scheduler_Schedule(t *testing.T) {
3941
name := string(uuid.NewUUID())
4042
namespace := string(uuid.NewUUID())
4143

42-
policy := newArangoBackupPolicy("* * * */2 *", namespace, name, map[string]string{}, backupApi.ArangoBackupTemplate{})
44+
policy := newArangoBackupPolicy(namespace, name, newSimpleArangoBackupPolicySpec("* * * */2 *"))
4345

4446
database := newArangoDeployment(namespace, map[string]string{
4547
"test": "me",
@@ -67,7 +69,7 @@ func Test_Scheduler_InvalidSchedule(t *testing.T) {
6769
name := string(uuid.NewUUID())
6870
namespace := string(uuid.NewUUID())
6971

70-
policy := newArangoBackupPolicy("", namespace, name, map[string]string{}, backupApi.ArangoBackupTemplate{})
72+
policy := newArangoBackupPolicy(namespace, name, newSimpleArangoBackupPolicySpec(""))
7173

7274
database := newArangoDeployment(namespace, map[string]string{})
7375

@@ -93,7 +95,7 @@ func Test_Scheduler_Valid_OneObject_SelectAll(t *testing.T) {
9395
name := string(uuid.NewUUID())
9496
namespace := string(uuid.NewUUID())
9597

96-
policy := newArangoBackupPolicy("* * * */2 *", namespace, name, map[string]string{}, backupApi.ArangoBackupTemplate{})
98+
policy := newArangoBackupPolicy(namespace, name, newSimpleArangoBackupPolicySpec("* * * */2 *"))
9799
policy.Status.Scheduled = meta.Time{
98100
Time: time.Now().Add(-1 * time.Hour),
99101
}
@@ -131,8 +133,9 @@ func Test_Scheduler_Valid_OneObject_Selector(t *testing.T) {
131133
selectors := map[string]string{
132134
"SELECTOR": string(uuid.NewUUID()),
133135
}
134-
135-
policy := newArangoBackupPolicy("* * * */2 *", namespace, name, selectors, backupApi.ArangoBackupTemplate{})
136+
spec := newSimpleArangoBackupPolicySpec("* * * */2 *")
137+
spec.DeploymentSelector = &meta.LabelSelector{MatchLabels: selectors}
138+
policy := newArangoBackupPolicy(namespace, name, spec)
136139
policy.Status.Scheduled = meta.Time{
137140
Time: time.Now().Add(-1 * time.Hour),
138141
}
@@ -170,7 +173,9 @@ func Test_Scheduler_Valid_MultipleObject_Selector(t *testing.T) {
170173
"SELECTOR": string(uuid.NewUUID()),
171174
}
172175

173-
policy := newArangoBackupPolicy("* * * */2 *", namespace, name, selectors, backupApi.ArangoBackupTemplate{})
176+
spec := newSimpleArangoBackupPolicySpec("* * * */2 *")
177+
spec.DeploymentSelector = &meta.LabelSelector{MatchLabels: selectors}
178+
policy := newArangoBackupPolicy(namespace, name, spec)
174179
policy.Status.Scheduled = meta.Time{
175180
Time: time.Now().Add(-1 * time.Hour),
176181
}
@@ -211,7 +216,9 @@ func Test_Reschedule(t *testing.T) {
211216
"SELECTOR": string(uuid.NewUUID()),
212217
}
213218

214-
policy := newArangoBackupPolicy("* 13 * * *", namespace, name, selectors, backupApi.ArangoBackupTemplate{})
219+
spec := newSimpleArangoBackupPolicySpec("* 13 * * *")
220+
spec.DeploymentSelector = &meta.LabelSelector{MatchLabels: selectors}
221+
policy := newArangoBackupPolicy(namespace, name, spec)
215222

216223
// Act
217224
createArangoBackupPolicy(t, handler, policy)
@@ -269,8 +276,9 @@ func Test_Validate(t *testing.T) {
269276
selectors := map[string]string{
270277
"SELECTOR": string(uuid.NewUUID()),
271278
}
272-
273-
policy := newArangoBackupPolicy(c, namespace, name, selectors, backupApi.ArangoBackupTemplate{})
279+
spec := newSimpleArangoBackupPolicySpec(c)
280+
spec.DeploymentSelector = &meta.LabelSelector{MatchLabels: selectors}
281+
policy := newArangoBackupPolicy(namespace, name, spec)
274282

275283
require.NoError(t, policy.Validate())
276284

@@ -286,3 +294,72 @@ func Test_Validate(t *testing.T) {
286294
})
287295
}
288296
}
297+
298+
func Test_Concurrent(t *testing.T) {
299+
testCase := func(t *testing.T, allowConcurrent *bool) {
300+
handler := newFakeHandler()
301+
302+
name := string(uuid.NewUUID())
303+
namespace := string(uuid.NewUUID())
304+
305+
spec := newSimpleArangoBackupPolicySpec("* * * */2 *")
306+
spec.AllowConcurrent = allowConcurrent
307+
policy := newArangoBackupPolicy(namespace, name, spec)
308+
policy.Status.Scheduled = meta.Time{
309+
Time: time.Now().Add(-1 * time.Hour),
310+
}
311+
312+
database := newArangoDeployment(namespace, map[string]string{
313+
"test": "me",
314+
})
315+
316+
createArangoBackupPolicy(t, handler, policy)
317+
createArangoDeployment(t, handler, database)
318+
319+
// "create" first backup
320+
require.NoError(t, handler.Handle(newItemFromBackupPolicy(operation.Update, policy)))
321+
backups := listArangoBackups(t, handler, namespace)
322+
require.Len(t, backups, 1)
323+
324+
// "try create" second backup but first one still is not in TerminalState
325+
policy = refreshArangoBackupPolicy(t, handler, policy)
326+
policy.Status.Scheduled = meta.Time{
327+
Time: time.Now().Add(-1 * time.Hour),
328+
}
329+
updateArangoBackupPolicy(t, handler, policy)
330+
require.NoError(t, handler.Handle(newItemFromBackupPolicy(operation.Update, policy)))
331+
backups = listArangoBackups(t, handler, namespace)
332+
require.Len(t, backups, util.BoolSwitch(policy.Spec.GetAllowConcurrent(), 2, 1))
333+
334+
// mark previous backup as Ready
335+
backup := backups[0].DeepCopy()
336+
backup.Status.State = backupApi.ArangoBackupStateReady
337+
backup.Status.Backup = &backupApi.ArangoBackupDetails{
338+
ID: "SOME_ID",
339+
}
340+
_, err := handler.client.BackupV1().ArangoBackups(namespace).UpdateStatus(context.Background(), backup, meta.UpdateOptions{})
341+
require.NoError(t, err)
342+
343+
// "try create" second backup again, should succeed
344+
policy = refreshArangoBackupPolicy(t, handler, policy)
345+
policy.Status.Scheduled = meta.Time{
346+
Time: time.Now().Add(-1 * time.Hour),
347+
}
348+
updateArangoBackupPolicy(t, handler, policy)
349+
require.NoError(t, handler.Handle(newItemFromBackupPolicy(operation.Update, policy)))
350+
backups = listArangoBackups(t, handler, namespace)
351+
require.Len(t, backups, util.BoolSwitch(policy.Spec.GetAllowConcurrent(), 3, 2))
352+
}
353+
354+
t.Run("Explicit Allow", func(t *testing.T) {
355+
testCase(t, util.NewType(true))
356+
})
357+
358+
t.Run("Default Allow", func(t *testing.T) {
359+
testCase(t, nil)
360+
})
361+
362+
t.Run("Explicit Disallow", func(t *testing.T) {
363+
testCase(t, util.NewType(false))
364+
})
365+
}

0 commit comments

Comments
 (0)