Skip to content

Commit 3c0cd6a

Browse files
Merge pull request #123 from JohnStrunk/replication-create-delete
CLI replication create & delete
2 parents f84d09f + 9b3b611 commit 3c0cd6a

9 files changed

Lines changed: 649 additions & 2 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ helm-lint: helm ## Lint Helm chart
105105
TEST_ARGS ?= -progress -randomizeAllSpecs -randomizeSuites -slowSpecThreshold 30 -p -cover -coverprofile cover.out -outputdir .
106106
TEST_PACKAGES ?= ./...
107107
test: manifests generate lint envtest helm-lint ginkgo ## Run tests.
108+
-rm -f cover.out
108109
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GINKGO) $(TEST_ARGS) $(TEST_PACKAGES)
109110

110111
.PHONY: test-e2e

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/spf13/cobra v1.2.1
1414
github.com/spf13/pflag v1.0.5
1515
github.com/spf13/viper v1.8.1
16+
gopkg.in/yaml.v2 v2.4.0
1617
k8s.io/api v0.22.1
1718
k8s.io/apimachinery v0.22.1
1819
k8s.io/cli-runtime v0.22.1
@@ -105,7 +106,6 @@ require (
105106
gopkg.in/inf.v0 v0.9.1 // indirect
106107
gopkg.in/ini.v1 v1.62.0 // indirect
107108
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
108-
gopkg.in/yaml.v2 v2.4.0 // indirect
109109
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
110110
k8s.io/apiextensions-apiserver v0.22.1 // indirect
111111
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect

kubectl-volsync/cmd/relationship.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ import (
2929
"gopkg.in/yaml.v2"
3030
"k8s.io/apimachinery/pkg/api/resource"
3131
"k8s.io/klog/v2"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
)
34+
35+
const (
36+
// RelationshipLabelKey is a label applied to objects that are created as a
37+
// part of a given relationship. The value of the key is the UUID of the
38+
// relationship.
39+
RelationshipLabelKey = "volsync.backube/relationship"
3240
)
3341

3442
// Each relationship type (e.g., replication, migration, backup, etc.) should
@@ -180,3 +188,12 @@ func (r *Relationship) GetData(data interface{}) error {
180188
return resource.ParseQuantity(data.(string))
181189
}))
182190
}
191+
192+
func (r *Relationship) AddIDLabel(object client.Object) {
193+
labels := object.GetLabels()
194+
if labels == nil {
195+
labels = map[string]string{}
196+
}
197+
labels[RelationshipLabelKey] = r.ID().String()
198+
object.SetLabels(labels)
199+
}

kubectl-volsync/cmd/relationship_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"github.com/google/uuid"
2525
. "github.com/onsi/ginkgo"
2626
. "github.com/onsi/gomega"
27+
corev1 "k8s.io/api/core/v1"
2728
"k8s.io/apimachinery/pkg/api/resource"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830
utilrand "k8s.io/apimachinery/pkg/util/rand"
2931
)
3032

@@ -165,5 +167,27 @@ var _ = Describe("Relationships", func() {
165167
Expect(initial.RdataPtr.RdataPtr).To(BeNil())
166168
Expect(loaded.RdataPtr.RdataPtr).To(BeNil())
167169
})
170+
It("can set the ID label on an object", func() {
171+
pvcNoLabels := &corev1.PersistentVolumeClaim{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Name: "foo",
174+
Namespace: "bar",
175+
},
176+
}
177+
rel.AddIDLabel(pvcNoLabels)
178+
Expect(pvcNoLabels.Labels).To(HaveKeyWithValue(RelationshipLabelKey, rel.ID().String()))
179+
pvcLabels := &corev1.PersistentVolumeClaim{
180+
ObjectMeta: metav1.ObjectMeta{
181+
Name: "foo",
182+
Namespace: "bar",
183+
Labels: map[string]string{
184+
"one": "two",
185+
"three": "four",
186+
},
187+
},
188+
}
189+
rel.AddIDLabel(pvcLabels)
190+
Expect(pvcLabels.Labels).To(HaveKeyWithValue(RelationshipLabelKey, rel.ID().String()))
191+
})
168192
})
169193
})

kubectl-volsync/cmd/replication.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright © 2021 The VolSync authors
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Affero General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Affero General Public License for more details.
13+
14+
You should have received a copy of the GNU Affero General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/spf13/cobra"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
errorsutil "k8s.io/apimachinery/pkg/util/errors"
27+
"k8s.io/klog/v2"
28+
"k8s.io/kubectl/pkg/util/i18n"
29+
"k8s.io/kubectl/pkg/util/templates"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
32+
volsyncv1alpha1 "github.com/backube/volsync/api/v1alpha1"
33+
)
34+
35+
const ReplicationRelationshipType RelationshipType = "replication"
36+
37+
// replicationRelationship holds the config state for replication-type
38+
// relationships
39+
type replicationRelationship struct {
40+
Relationship
41+
data replicationRelationshipData
42+
}
43+
44+
// replicationRelationshipData is the state that will be saved to the
45+
// relationship config file
46+
type replicationRelationshipData struct {
47+
// Config file/struct version used so we know how to decode when parsing
48+
// from disk
49+
Version int
50+
// Config info for the source side of the relationship
51+
Source *replicationRelationshipSource
52+
// Config info for the destination side of the relationship
53+
Destination *replicationRelationshipDestination
54+
}
55+
56+
type replicationRelationshipSource struct {
57+
// Cluster context name
58+
Cluster string
59+
// Namespace on source cluster
60+
Namespace string
61+
// Name of PVC being replicated
62+
PVCName string
63+
// Name of ReplicationSource object
64+
RSName string
65+
// Parameters for the ReplicationSource
66+
Source volsyncv1alpha1.ReplicationSourceRsyncSpec
67+
}
68+
69+
type replicationRelationshipDestination struct {
70+
// Cluster context name
71+
Cluster string
72+
// Namespace on destination cluster
73+
Namespace string
74+
// Name of the ReplicationDestination object
75+
RDName string
76+
// Parameters for the ReplicationDestination
77+
Destination volsyncv1alpha1.ReplicationDestinationRsyncSpec
78+
}
79+
80+
// replicationCmd represents the replication command
81+
var replicationCmd = &cobra.Command{
82+
Use: "replication",
83+
Short: i18n.T("Replicate data between two PersistentVolumes"),
84+
Long: templates.LongDesc(i18n.T(`
85+
Replicate the contents of one PersistentVolume to another.
86+
87+
This set of commands is designed to set up and manage a replication
88+
relationship between two different PVCs in the same Namespace, across
89+
Namespaces, or in different clusters. The contents of the volume can be
90+
replicated either on-demand or based on a provided schedule.
91+
`)),
92+
}
93+
94+
func init() {
95+
rootCmd.AddCommand(replicationCmd)
96+
97+
replicationCmd.PersistentFlags().StringP("relationship", "r", "", "relationship name")
98+
cobra.CheckErr(replicationCmd.MarkPersistentFlagRequired("relationship"))
99+
}
100+
101+
func newReplicationRelationship(cmd *cobra.Command) (*replicationRelationship, error) {
102+
r, err := CreateRelationshipFromCommand(cmd, ReplicationRelationshipType)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
return &replicationRelationship{
108+
Relationship: *r,
109+
data: replicationRelationshipData{
110+
Version: 1,
111+
},
112+
}, nil
113+
}
114+
115+
func loadReplicationRelationship(cmd *cobra.Command) (*replicationRelationship, error) {
116+
r, err := LoadRelationshipFromCommand(cmd, ReplicationRelationshipType)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
rr := &replicationRelationship{
122+
Relationship: *r,
123+
}
124+
// Decode according to the file version
125+
version := rr.GetInt("data.version")
126+
switch version {
127+
case 1:
128+
if err := rr.GetData(&rr.data); err != nil {
129+
return nil, err
130+
}
131+
default:
132+
return nil, fmt.Errorf("unsupported config file version %d", version)
133+
}
134+
return rr, nil
135+
}
136+
137+
func (rr *replicationRelationship) Save() error {
138+
if err := rr.SetData(rr.data); err != nil {
139+
return err
140+
}
141+
// resource.Quantity doesn't properly encode, so we need to do it manually
142+
if rr.data.Source != nil && rr.data.Source.Source.Capacity != nil {
143+
rr.Set("data.source.source.replicationsourcevolumeoptions.capacity",
144+
rr.data.Source.Source.Capacity.String())
145+
}
146+
if rr.data.Destination != nil && rr.data.Destination.Destination.Capacity != nil {
147+
rr.Set("data.destination.destination.replicationdestinationvolumeoptions.capacity",
148+
rr.data.Destination.Destination.Capacity.String())
149+
}
150+
return rr.Relationship.Save()
151+
}
152+
153+
// GetClients returns clients to access the src & dst clusters (srcClient,
154+
// dstClient, error)
155+
func (rr *replicationRelationship) GetClients() (client.Client, client.Client, error) {
156+
var srcClient, dstClient client.Client
157+
var err error
158+
errList := []error{}
159+
if rr.data.Source != nil {
160+
if srcClient, err = newClient(rr.data.Source.Cluster); err != nil {
161+
klog.Errorf("unable to create client for source cluster: %w", err)
162+
errList = append(errList, err)
163+
}
164+
}
165+
if rr.data.Destination != nil {
166+
if dstClient, err = newClient(rr.data.Destination.Cluster); err != nil {
167+
klog.Errorf("unable to create client for destination cluster: %w", err)
168+
errList = append(errList, err)
169+
}
170+
}
171+
return srcClient, dstClient, errorsutil.NewAggregate(errList)
172+
}
173+
174+
// DeleteSource removes the resources we've created on the source cluster
175+
func (rr *replicationRelationship) DeleteSource(ctx context.Context,
176+
srcClient client.Client) error {
177+
src := rr.data.Source
178+
if srcClient == nil || src == nil {
179+
// Nothing to do because we don't have a client or the source isn't
180+
// defined
181+
return nil
182+
}
183+
184+
errList := []error{}
185+
for _, o := range []client.Object{
186+
// cleaning up requires deleting both RS and the Secret we copied
187+
&volsyncv1alpha1.ReplicationSource{},
188+
&corev1.Secret{},
189+
} {
190+
err := srcClient.DeleteAllOf(ctx, o,
191+
client.InNamespace(src.Namespace),
192+
client.MatchingLabels{RelationshipLabelKey: rr.ID().String()},
193+
client.PropagationPolicy(metav1.DeletePropagationBackground))
194+
if client.IgnoreNotFound(err) != nil {
195+
klog.Errorf("unable to remove previous Source objects: %w", err)
196+
errList = append(errList, err)
197+
}
198+
}
199+
return errorsutil.NewAggregate(errList)
200+
}
201+
202+
// DeleteDestination removes the resources we've created on the destination
203+
// cluster
204+
func (rr *replicationRelationship) DeleteDestination(ctx context.Context,
205+
dstClient client.Client) error {
206+
dst := rr.data.Destination
207+
if dstClient == nil || dst == nil {
208+
// Nothing to do because we don't have a client or the destination isn't
209+
// defined
210+
return nil
211+
}
212+
213+
err := dstClient.DeleteAllOf(ctx, &volsyncv1alpha1.ReplicationDestination{},
214+
client.InNamespace(dst.Namespace),
215+
client.MatchingLabels{RelationshipLabelKey: rr.ID().String()},
216+
client.PropagationPolicy(metav1.DeletePropagationBackground))
217+
err = client.IgnoreNotFound(err)
218+
if err != nil {
219+
klog.Errorf("unable to remove previous Destination objects: %w", err)
220+
}
221+
return err
222+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright © 2021 The VolSync authors
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Affero General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Affero General Public License for more details.
13+
14+
You should have received a copy of the GNU Affero General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
"k8s.io/kubectl/pkg/util/i18n"
24+
"k8s.io/kubectl/pkg/util/templates"
25+
)
26+
27+
type replicationCreate struct {
28+
cobra.Command
29+
}
30+
31+
// replicationCreateCmd represents the create command
32+
var replicationCreateCmd = &cobra.Command{
33+
Use: "create",
34+
Short: i18n.T("Create a new replication relationship"),
35+
Long: templates.LongDesc(i18n.T(`
36+
This command creates a new, empty replication relationship.
37+
38+
Once created, both a source (set-source) and a destination (set-destination)
39+
must be added.
40+
`)),
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
r := &replicationCreate{
43+
Command: *cmd,
44+
}
45+
return r.Run()
46+
},
47+
}
48+
49+
func init() {
50+
replicationCmd.AddCommand(replicationCreateCmd)
51+
}
52+
53+
func (cmd *replicationCreate) Run() error {
54+
r, err := newReplicationRelationship(&cmd.Command)
55+
if err != nil {
56+
return err
57+
}
58+
59+
if err = r.Save(); err != nil {
60+
return fmt.Errorf("unable to save relationship configuration: %w", err)
61+
}
62+
63+
return nil
64+
}

0 commit comments

Comments
 (0)