Skip to content

Commit 3a6e4ae

Browse files
committed
feat: support take backup from specified node
1 parent 62657cc commit 3a6e4ae

File tree

7 files changed

+130
-13
lines changed

7 files changed

+130
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88
### Added
99
* `MysqlDatabase` `MysqlUser` Add delete policy
1010
* Add `PtHeartbeatResources` in `.Spec.PodSpec` to allow the user specifying resources for pt-heartbeat.
11+
* Add `CandidateNode` field in `MysqlBackup.Spec` to allow the user specifying candidate node for backup.
1112
* Set `MysqlCluter.Spec.BackupSchedule` to empty string to disable recurrent backups
1213
### Changed
1314
* Set default MySQL server version to `5.7.35`

config/crd/bases/mysql.presslabs.org_mysqlbackups.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ spec:
3636
backupURL:
3737
description: BackupURL represents the URL to the backup location, this can be partially specifyied. Default is used the one specified in the cluster.
3838
type: string
39+
candidateNode:
40+
description: CandidateNode is the node host that will be used to take the backup. If not set or set to wrong node, the operator will calculate candidate node by itself.
41+
type: string
3942
clusterName:
4043
description: ClustterName represents the cluster for which to take backup
4144
type: string

deploy/charts/mysql-operator/crds/mysql.presslabs.org_mysqlbackups.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ spec:
3737
backupURL:
3838
description: BackupURL represents the URL to the backup location, this can be partially specifyied. Default is used the one specified in the cluster.
3939
type: string
40+
candidateNode:
41+
description: CandidateNode is the node host that will be used to take the backup. If not set or set to wrong node, the operator will calculate candidate node by itself.
42+
type: string
4043
clusterName:
4144
description: ClustterName represents the cluster for which to take backup
4245
type: string

pkg/apis/mysql/v1alpha1/mysqlbackup_types.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ type MysqlBackupSpec struct {
4444
// default it's used softDelete.
4545
// +optional
4646
RemoteDeletePolicy DeletePolicy `json:"remoteDeletePolicy,omitempty"`
47+
48+
// CandidateNode is the node host that will be used to take the backup.
49+
// If not set or set to wrong node, the operator will calculate candidate node by itself.
50+
// +optional
51+
CandidateNode string `json:"candidateNode,omitempty"`
4752
}
4853

4954
// BackupCondition defines condition struct for backup resource
@@ -94,7 +99,6 @@ type MysqlBackupStatus struct {
9499

95100
// MysqlBackup is the Schema for the mysqlbackups API
96101
// +kubebuilder:object:root=true
97-
//
98102
type MysqlBackup struct {
99103
metav1.TypeMeta `json:",inline"`
100104
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -105,7 +109,6 @@ type MysqlBackup struct {
105109

106110
// MysqlBackupList contains a list of MysqlBackup
107111
// +kubebuilder:object:root=true
108-
//
109112
type MysqlBackupList struct {
110113
metav1.TypeMeta `json:",inline"`
111114
metav1.ListMeta `json:"metadata,omitempty"`

pkg/controller/mysqlbackup/internal/syncer/job.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ func (s *jobSyncer) SyncFn() error {
8888
return nil
8989
}
9090

91-
// getBackupCandidate returns the hostname of the first not-lagged and
91+
// getBackupCandidate returns candidate node in mysqlbackup spec at first,
92+
// if not, it will return the hostname of the first not-lagged and
9293
// replicating slave node, else returns the master node.
9394
func (s *jobSyncer) getBackupCandidate() string {
95+
if s.backup.Spec.CandidateNode != "" {
96+
if err := s.backup.Validate(s.cluster); err == nil {
97+
return s.backup.Spec.CandidateNode
98+
}
99+
log.Info("backup's candidate node is not valid, will try to calculate candidate node")
100+
}
101+
94102
for _, node := range s.cluster.Status.Nodes {
95103
master := s.cluster.GetNodeCondition(node.Name, api.NodeConditionMaster)
96104
replicating := s.cluster.GetNodeCondition(node.Name, api.NodeConditionReplicating)

pkg/controller/mysqlbackup/mysqlbackup_controller_test.go

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,7 @@ var _ = Describe("MysqlBackup controller", func() {
119119
BeforeEach(func() {
120120
// create a cluster with 2 nodes
121121
Expect(c.Create(context.TODO(), cluster.Unwrap())).To(Succeed())
122-
cluster.Status.Nodes = []api.NodeStatus{
123-
{
124-
Name: cluster.GetPodHostname(0),
125-
Conditions: testutil.NodeConditions(true, false, false, false),
126-
},
127-
{
128-
Name: cluster.GetPodHostname(1),
129-
Conditions: testutil.NodeConditions(false, true, false, true),
130-
},
131-
}
122+
cluster.Status.Nodes = getHealthyNodeStatus(cluster, 2)
132123
Expect(c.Status().Update(context.TODO(), cluster.Unwrap())).To(Succeed())
133124
// create the backup
134125
Expect(c.Create(context.TODO(), backup.Unwrap())).To(Succeed())
@@ -316,8 +307,74 @@ var _ = Describe("MysqlBackup controller", func() {
316307
Expect(c.Delete(context.TODO(), cluster.Unwrap())).To(Succeed())
317308
})
318309
})
310+
311+
When("candidate node is setted to wrong node", func() {
312+
BeforeEach(func() {
313+
backup.Spec.CandidateNode = cluster.GetPodHostname(3)
314+
Expect(c.Create(context.TODO(), backup.Unwrap())).To(Succeed())
315+
316+
Expect(c.Create(context.TODO(), cluster.Unwrap())).To(Succeed())
317+
cluster.Status.Nodes = getHealthyNodeStatus(cluster, 2)
318+
Expect(c.Status().Update(context.TODO(), cluster.Unwrap())).To(Succeed())
319+
320+
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
321+
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
322+
testutil.DrainChan(requests)
323+
})
324+
AfterEach(func() {
325+
Expect(c.Delete(context.TODO(), backup.Unwrap())).To(Succeed())
326+
Expect(c.Delete(context.TODO(), cluster.Unwrap())).To(Succeed())
327+
})
328+
329+
It("should take backup from replica 1", func() {
330+
job := &batch.Job{}
331+
Expect(c.Get(context.TODO(), jobKey, job)).To(Succeed())
332+
Expect(job.Spec.Template.Spec.Containers[0].Args).To(ContainElement(Equal(cluster.GetPodHostname(1))))
333+
})
334+
})
335+
336+
When("candidate node is setted to master", func() {
337+
BeforeEach(func() {
338+
backup.Spec.CandidateNode = cluster.GetPodHostname(0)
339+
Expect(c.Create(context.TODO(), backup.Unwrap())).To(Succeed())
340+
341+
Expect(c.Create(context.TODO(), cluster.Unwrap())).To(Succeed())
342+
cluster.Status.Nodes = getHealthyNodeStatus(cluster, 2)
343+
Expect(c.Status().Update(context.TODO(), cluster.Unwrap())).To(Succeed())
344+
345+
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
346+
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
347+
testutil.DrainChan(requests)
348+
})
349+
AfterEach(func() {
350+
Expect(c.Delete(context.TODO(), backup.Unwrap())).To(Succeed())
351+
Expect(c.Delete(context.TODO(), cluster.Unwrap())).To(Succeed())
352+
})
353+
354+
It("should take backup from master", func() {
355+
job := &batch.Job{}
356+
Expect(c.Get(context.TODO(), jobKey, job)).To(Succeed())
357+
Expect(job.Spec.Template.Spec.Containers[0].Args).To(ContainElement(Equal(cluster.GetPodHostname(0))))
358+
})
359+
})
319360
})
320361

362+
func getHealthyNodeStatus(cluster *mysqlcluster.MysqlCluster, count int) []api.NodeStatus {
363+
status := []api.NodeStatus{
364+
{
365+
Name: cluster.GetPodHostname(0),
366+
Conditions: testutil.NodeConditions(true, false, false, false),
367+
},
368+
}
369+
for i := 1; i < count; i++ {
370+
status = append(status, api.NodeStatus{
371+
Name: cluster.GetPodHostname(i),
372+
Conditions: testutil.NodeConditions(false, true, false, false),
373+
})
374+
}
375+
return status
376+
}
377+
321378
func refreshFn(c client.Client, backupKey types.NamespacedName) func() *api.MysqlBackup {
322379
return func() *api.MysqlBackup {
323380
backup := &api.MysqlBackup{}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2018 Pressinfra SRL
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mysqlbackup
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/bitpoke/mysql-operator/pkg/internal/mysqlcluster"
23+
)
24+
25+
// Validate checks if the backup spec is validated
26+
func (c *MysqlBackup) Validate(cluster *mysqlcluster.MysqlCluster) error {
27+
// TODO: this validation should be done in an admission web-hook
28+
29+
if c.Spec.CandidateNode != "" {
30+
validate := false
31+
for i := 0; i < int(*cluster.Spec.Replicas); i++ {
32+
if c.Spec.CandidateNode == cluster.GetPodHostname(i) {
33+
validate = true
34+
break
35+
}
36+
}
37+
if !validate {
38+
return fmt.Errorf("spec.candidateNode is not a valid node")
39+
}
40+
}
41+
return nil
42+
}

0 commit comments

Comments
 (0)