diff --git a/Makefile b/Makefile
index 9d7de760e..8384d7d3f 100644
--- a/Makefile
+++ b/Makefile
@@ -105,6 +105,7 @@ helm-lint: helm ## Lint Helm chart
TEST_ARGS ?= -progress -randomizeAllSpecs -randomizeSuites -slowSpecThreshold 30 -p -cover -coverprofile cover.out -outputdir .
TEST_PACKAGES ?= ./...
test: manifests generate lint envtest helm-lint ginkgo ## Run tests.
+ -rm -f cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GINKGO) $(TEST_ARGS) $(TEST_PACKAGES)
.PHONY: test-e2e
diff --git a/go.mod b/go.mod
index 4e2a7c5b1..9636154e3 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
+ gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.22.1
k8s.io/apimachinery v0.22.1
k8s.io/cli-runtime v0.22.1
@@ -105,7 +106,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.22.1 // indirect
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
diff --git a/kubectl-volsync/cmd/relationship.go b/kubectl-volsync/cmd/relationship.go
index fb73d7d7c..444118b6f 100644
--- a/kubectl-volsync/cmd/relationship.go
+++ b/kubectl-volsync/cmd/relationship.go
@@ -29,6 +29,14 @@ import (
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/klog/v2"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ // RelationshipLabelKey is a label applied to objects that are created as a
+ // part of a given relationship. The value of the key is the UUID of the
+ // relationship.
+ RelationshipLabelKey = "volsync.backube/relationship"
)
// Each relationship type (e.g., replication, migration, backup, etc.) should
@@ -180,3 +188,12 @@ func (r *Relationship) GetData(data interface{}) error {
return resource.ParseQuantity(data.(string))
}))
}
+
+func (r *Relationship) AddIDLabel(object client.Object) {
+ labels := object.GetLabels()
+ if labels == nil {
+ labels = map[string]string{}
+ }
+ labels[RelationshipLabelKey] = r.ID().String()
+ object.SetLabels(labels)
+}
diff --git a/kubectl-volsync/cmd/relationship_test.go b/kubectl-volsync/cmd/relationship_test.go
index 3344bc6f3..3fd65bd24 100644
--- a/kubectl-volsync/cmd/relationship_test.go
+++ b/kubectl-volsync/cmd/relationship_test.go
@@ -24,7 +24,9 @@ import (
"github.com/google/uuid"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilrand "k8s.io/apimachinery/pkg/util/rand"
)
@@ -165,5 +167,27 @@ var _ = Describe("Relationships", func() {
Expect(initial.RdataPtr.RdataPtr).To(BeNil())
Expect(loaded.RdataPtr.RdataPtr).To(BeNil())
})
+ It("can set the ID label on an object", func() {
+ pvcNoLabels := &corev1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Namespace: "bar",
+ },
+ }
+ rel.AddIDLabel(pvcNoLabels)
+ Expect(pvcNoLabels.Labels).To(HaveKeyWithValue(RelationshipLabelKey, rel.ID().String()))
+ pvcLabels := &corev1.PersistentVolumeClaim{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Namespace: "bar",
+ Labels: map[string]string{
+ "one": "two",
+ "three": "four",
+ },
+ },
+ }
+ rel.AddIDLabel(pvcLabels)
+ Expect(pvcLabels.Labels).To(HaveKeyWithValue(RelationshipLabelKey, rel.ID().String()))
+ })
})
})
diff --git a/kubectl-volsync/cmd/replication.go b/kubectl-volsync/cmd/replication.go
new file mode 100644
index 000000000..403fd4f86
--- /dev/null
+++ b/kubectl-volsync/cmd/replication.go
@@ -0,0 +1,222 @@
+/*
+Copyright © 2021 The VolSync authors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/spf13/cobra"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ errorsutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/klog/v2"
+ "k8s.io/kubectl/pkg/util/i18n"
+ "k8s.io/kubectl/pkg/util/templates"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ volsyncv1alpha1 "github.com/backube/volsync/api/v1alpha1"
+)
+
+const ReplicationRelationshipType RelationshipType = "replication"
+
+// replicationRelationship holds the config state for replication-type
+// relationships
+type replicationRelationship struct {
+ Relationship
+ data replicationRelationshipData
+}
+
+// replicationRelationshipData is the state that will be saved to the
+// relationship config file
+type replicationRelationshipData struct {
+ // Config file/struct version used so we know how to decode when parsing
+ // from disk
+ Version int
+ // Config info for the source side of the relationship
+ Source *replicationRelationshipSource
+ // Config info for the destination side of the relationship
+ Destination *replicationRelationshipDestination
+}
+
+type replicationRelationshipSource struct {
+ // Cluster context name
+ Cluster string
+ // Namespace on source cluster
+ Namespace string
+ // Name of PVC being replicated
+ PVCName string
+ // Name of ReplicationSource object
+ RSName string
+ // Parameters for the ReplicationSource
+ Source volsyncv1alpha1.ReplicationSourceRsyncSpec
+}
+
+type replicationRelationshipDestination struct {
+ // Cluster context name
+ Cluster string
+ // Namespace on destination cluster
+ Namespace string
+ // Name of the ReplicationDestination object
+ RDName string
+ // Parameters for the ReplicationDestination
+ Destination volsyncv1alpha1.ReplicationDestinationRsyncSpec
+}
+
+// replicationCmd represents the replication command
+var replicationCmd = &cobra.Command{
+ Use: "replication",
+ Short: i18n.T("Replicate data between two PersistentVolumes"),
+ Long: templates.LongDesc(i18n.T(`
+ Replicate the contents of one PersistentVolume to another.
+
+ This set of commands is designed to set up and manage a replication
+ relationship between two different PVCs in the same Namespace, across
+ Namespaces, or in different clusters. The contents of the volume can be
+ replicated either on-demand or based on a provided schedule.
+ `)),
+}
+
+func init() {
+ rootCmd.AddCommand(replicationCmd)
+
+ replicationCmd.PersistentFlags().StringP("relationship", "r", "", "relationship name")
+ cobra.CheckErr(replicationCmd.MarkPersistentFlagRequired("relationship"))
+}
+
+func newReplicationRelationship(cmd *cobra.Command) (*replicationRelationship, error) {
+ r, err := CreateRelationshipFromCommand(cmd, ReplicationRelationshipType)
+ if err != nil {
+ return nil, err
+ }
+
+ return &replicationRelationship{
+ Relationship: *r,
+ data: replicationRelationshipData{
+ Version: 1,
+ },
+ }, nil
+}
+
+func loadReplicationRelationship(cmd *cobra.Command) (*replicationRelationship, error) {
+ r, err := LoadRelationshipFromCommand(cmd, ReplicationRelationshipType)
+ if err != nil {
+ return nil, err
+ }
+
+ rr := &replicationRelationship{
+ Relationship: *r,
+ }
+ // Decode according to the file version
+ version := rr.GetInt("data.version")
+ switch version {
+ case 1:
+ if err := rr.GetData(&rr.data); err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("unsupported config file version %d", version)
+ }
+ return rr, nil
+}
+
+func (rr *replicationRelationship) Save() error {
+ if err := rr.SetData(rr.data); err != nil {
+ return err
+ }
+ // resource.Quantity doesn't properly encode, so we need to do it manually
+ if rr.data.Source != nil && rr.data.Source.Source.Capacity != nil {
+ rr.Set("data.source.source.replicationsourcevolumeoptions.capacity",
+ rr.data.Source.Source.Capacity.String())
+ }
+ if rr.data.Destination != nil && rr.data.Destination.Destination.Capacity != nil {
+ rr.Set("data.destination.destination.replicationdestinationvolumeoptions.capacity",
+ rr.data.Destination.Destination.Capacity.String())
+ }
+ return rr.Relationship.Save()
+}
+
+// GetClients returns clients to access the src & dst clusters (srcClient,
+// dstClient, error)
+func (rr *replicationRelationship) GetClients() (client.Client, client.Client, error) {
+ var srcClient, dstClient client.Client
+ var err error
+ errList := []error{}
+ if rr.data.Source != nil {
+ if srcClient, err = newClient(rr.data.Source.Cluster); err != nil {
+ klog.Errorf("unable to create client for source cluster: %w", err)
+ errList = append(errList, err)
+ }
+ }
+ if rr.data.Destination != nil {
+ if dstClient, err = newClient(rr.data.Destination.Cluster); err != nil {
+ klog.Errorf("unable to create client for destination cluster: %w", err)
+ errList = append(errList, err)
+ }
+ }
+ return srcClient, dstClient, errorsutil.NewAggregate(errList)
+}
+
+// DeleteSource removes the resources we've created on the source cluster
+func (rr *replicationRelationship) DeleteSource(ctx context.Context,
+ srcClient client.Client) error {
+ src := rr.data.Source
+ if srcClient == nil || src == nil {
+ // Nothing to do because we don't have a client or the source isn't
+ // defined
+ return nil
+ }
+
+ errList := []error{}
+ for _, o := range []client.Object{
+ // cleaning up requires deleting both RS and the Secret we copied
+ &volsyncv1alpha1.ReplicationSource{},
+ &corev1.Secret{},
+ } {
+ err := srcClient.DeleteAllOf(ctx, o,
+ client.InNamespace(src.Namespace),
+ client.MatchingLabels{RelationshipLabelKey: rr.ID().String()},
+ client.PropagationPolicy(metav1.DeletePropagationBackground))
+ if client.IgnoreNotFound(err) != nil {
+ klog.Errorf("unable to remove previous Source objects: %w", err)
+ errList = append(errList, err)
+ }
+ }
+ return errorsutil.NewAggregate(errList)
+}
+
+// DeleteDestination removes the resources we've created on the destination
+// cluster
+func (rr *replicationRelationship) DeleteDestination(ctx context.Context,
+ dstClient client.Client) error {
+ dst := rr.data.Destination
+ if dstClient == nil || dst == nil {
+ // Nothing to do because we don't have a client or the destination isn't
+ // defined
+ return nil
+ }
+
+ err := dstClient.DeleteAllOf(ctx, &volsyncv1alpha1.ReplicationDestination{},
+ client.InNamespace(dst.Namespace),
+ client.MatchingLabels{RelationshipLabelKey: rr.ID().String()},
+ client.PropagationPolicy(metav1.DeletePropagationBackground))
+ err = client.IgnoreNotFound(err)
+ if err != nil {
+ klog.Errorf("unable to remove previous Destination objects: %w", err)
+ }
+ return err
+}
diff --git a/kubectl-volsync/cmd/replication_create.go b/kubectl-volsync/cmd/replication_create.go
new file mode 100644
index 000000000..63a41f2ef
--- /dev/null
+++ b/kubectl-volsync/cmd/replication_create.go
@@ -0,0 +1,64 @@
+/*
+Copyright © 2021 The VolSync authors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+ "k8s.io/kubectl/pkg/util/i18n"
+ "k8s.io/kubectl/pkg/util/templates"
+)
+
+type replicationCreate struct {
+ cobra.Command
+}
+
+// replicationCreateCmd represents the create command
+var replicationCreateCmd = &cobra.Command{
+ Use: "create",
+ Short: i18n.T("Create a new replication relationship"),
+ Long: templates.LongDesc(i18n.T(`
+ This command creates a new, empty replication relationship.
+
+ Once created, both a source (set-source) and a destination (set-destination)
+ must be added.
+ `)),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ r := &replicationCreate{
+ Command: *cmd,
+ }
+ return r.Run()
+ },
+}
+
+func init() {
+ replicationCmd.AddCommand(replicationCreateCmd)
+}
+
+func (cmd *replicationCreate) Run() error {
+ r, err := newReplicationRelationship(&cmd.Command)
+ if err != nil {
+ return err
+ }
+
+ if err = r.Save(); err != nil {
+ return fmt.Errorf("unable to save relationship configuration: %w", err)
+ }
+
+ return nil
+}
diff --git a/kubectl-volsync/cmd/replication_delete.go b/kubectl-volsync/cmd/replication_delete.go
new file mode 100644
index 000000000..b71b1aa9e
--- /dev/null
+++ b/kubectl-volsync/cmd/replication_delete.go
@@ -0,0 +1,80 @@
+/*
+Copyright © 2021 The VolSync authors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "context"
+
+ "github.com/spf13/cobra"
+ errorsutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/kubectl/pkg/util/i18n"
+ "k8s.io/kubectl/pkg/util/templates"
+)
+
+type replicationDelete struct {
+ rel *replicationRelationship
+}
+
+// replicationDeleteCmd represents the delete command
+var replicationDeleteCmd = &cobra.Command{
+ Use: "delete",
+ Short: i18n.T("Delete an existing replication relationship"),
+ Long: templates.LongDesc(i18n.T(`
+ This command deletes a replication relationship and removes the associated
+ objects from the cluster(s).
+
+ The delete command removes the VolSync CRs for this relationship as well as
+ any associated VolumeSnapshot objects. In order to preserve the replicated
+ data, the destination should be "promote"-ed prior to deleting the
+ relationship.
+ `)),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ r, err := newReplicationDelete(cmd)
+ if err != nil {
+ return err
+ }
+ r.rel, err = loadReplicationRelationship(cmd)
+ if err != nil {
+ return err
+ }
+ return r.Run(cmd.Context())
+ },
+}
+
+func init() {
+ replicationCmd.AddCommand(replicationDeleteCmd)
+}
+
+func newReplicationDelete(cmd *cobra.Command) (*replicationDelete, error) {
+ rdel := &replicationDelete{}
+ return rdel, nil
+}
+
+func (rdel *replicationDelete) Run(ctx context.Context) error {
+ srcClient, dstClient, _ := rdel.rel.GetClients()
+ errList := []error{}
+ if err := rdel.rel.DeleteSource(ctx, srcClient); err != nil {
+ errList = append(errList, err)
+ }
+ if err := rdel.rel.DeleteDestination(ctx, dstClient); err != nil {
+ errList = append(errList, err)
+ }
+ if err := rdel.rel.Delete(); err != nil {
+ errList = append(errList, err)
+ }
+ return errorsutil.NewAggregate(errList)
+}
diff --git a/kubectl-volsync/cmd/replication_test.go b/kubectl-volsync/cmd/replication_test.go
new file mode 100644
index 000000000..c70ec6135
--- /dev/null
+++ b/kubectl-volsync/cmd/replication_test.go
@@ -0,0 +1,236 @@
+/*
+Copyright © 2021 The VolSync authors
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "context"
+ "io/ioutil"
+ "os"
+ "reflect"
+
+ volsyncv1alpha1 "github.com/backube/volsync/api/v1alpha1"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/spf13/cobra"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/utils/pointer"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var _ = Describe("Replication relationships can create/save/load", func() {
+ var dirname string
+ var cmd *cobra.Command
+ BeforeEach(func() {
+ var err error
+ // Create temp directory for relationship files
+ dirname, err = ioutil.TempDir("", "relation")
+ Expect(err).NotTo(HaveOccurred())
+
+ cmd = &cobra.Command{}
+ cmd.Flags().StringP("relationship", "r", "test-name", "")
+ cmd.Flags().String("config-dir", dirname, "")
+ })
+ AfterEach(func() {
+ os.RemoveAll(dirname)
+ })
+ It("can be round-triped", func() {
+ By("creating a new relationship")
+ rr, err := newReplicationRelationship(cmd)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(rr.data.Version).To(Equal(1))
+ Expect(rr.data.Destination).To(BeNil())
+ Expect(rr.data.Source).To(BeNil())
+
+ By("saving the relationship")
+ // Assign some values to test round-trip
+ caps := resource.MustParse("1Gi")
+ rr.data.Source = &replicationRelationshipSource{
+ Cluster: "cluster",
+ Namespace: "the-ns",
+ PVCName: "a-pvc",
+ RSName: "an-rs",
+ Source: volsyncv1alpha1.ReplicationSourceRsyncSpec{
+ ReplicationSourceVolumeOptions: volsyncv1alpha1.ReplicationSourceVolumeOptions{
+ CopyMethod: volsyncv1alpha1.CopyMethodClone,
+ Capacity: &caps,
+ StorageClassName: pointer.String("scn"),
+ AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
+ VolumeSnapshotClassName: pointer.String("vscn"),
+ },
+ },
+ }
+ capd := resource.MustParse("99Gi")
+ rr.data.Destination = &replicationRelationshipDestination{
+ Cluster: "c2",
+ Namespace: "n2",
+ RDName: "rd2",
+ Destination: volsyncv1alpha1.ReplicationDestinationRsyncSpec{
+ ReplicationDestinationVolumeOptions: volsyncv1alpha1.ReplicationDestinationVolumeOptions{
+ CopyMethod: volsyncv1alpha1.CopyMethodSnapshot,
+ Capacity: &capd,
+ StorageClassName: pointer.String("scn2"),
+ AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany},
+ VolumeSnapshotClassName: pointer.String("vscn2"),
+ DestinationPVC: pointer.String("dpvc"),
+ },
+ ServiceType: (*corev1.ServiceType)(pointer.String(string(corev1.ServiceTypeClusterIP))),
+ },
+ }
+ Expect(rr.Save()).To(Succeed())
+
+ By("loading it back in, they should match")
+ rr2, err := loadReplicationRelationship(cmd)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(reflect.DeepEqual(rr2.data, rr.data)).To(BeTrue())
+ })
+})
+
+var _ = Describe("Replication relationships", func() {
+ var ctx context.Context
+ var dirname string
+ var repRel *replicationRelationship
+ BeforeEach(func() {
+ ctx = context.TODO()
+ var err error
+ // Create temp directory for relationship files
+ dirname, err = ioutil.TempDir("", "relation")
+ Expect(err).NotTo(HaveOccurred())
+ // Create a new generic relationship
+ rel, err := createRelationship(dirname, "test", ReplicationRelationshipType)
+ Expect(err).NotTo(HaveOccurred())
+ repRel = &replicationRelationship{
+ Relationship: *rel,
+ data: replicationRelationshipData{
+ Version: 1,
+ Source: nil,
+ Destination: nil,
+ },
+ }
+ })
+ AfterEach(func() {
+ os.RemoveAll(dirname)
+ })
+
+ When("the cluster is empty", func() {
+ When("trying to delete", func() {
+ It("succeeds if no resources are defined", func() {
+ Expect(repRel.DeleteSource(ctx, k8sClient)).To(Succeed())
+ Expect(repRel.DeleteDestination(ctx, k8sClient)).To(Succeed())
+ })
+ It("succeeds if the cluster is empty", func() {
+ repRel.data.Source = &replicationRelationshipSource{
+ RSName: "xxx",
+ Namespace: "zzz",
+ }
+ repRel.data.Destination = &replicationRelationshipDestination{
+ RDName: "xxx",
+ Namespace: "zzz",
+ }
+ Expect(repRel.DeleteSource(ctx, k8sClient)).To(Succeed())
+ Expect(repRel.DeleteDestination(ctx, k8sClient)).To(Succeed())
+ })
+ })
+ })
+ When("there are existing resources in the cluster", func() {
+ var srcNs, dstNs *corev1.Namespace
+ var rs *volsyncv1alpha1.ReplicationSource
+ var rd *volsyncv1alpha1.ReplicationDestination
+ BeforeEach(func() {
+ // Create Namespaces
+ srcNs = &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: "src-",
+ },
+ }
+ Expect(k8sClient.Create(ctx, srcNs)).To(Succeed())
+ Expect(srcNs.Name).NotTo(BeEmpty())
+ dstNs = &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: "dst-",
+ },
+ }
+ Expect(k8sClient.Create(ctx, dstNs)).To(Succeed())
+ Expect(dstNs.Name).NotTo(BeEmpty())
+
+ rs = &volsyncv1alpha1.ReplicationSource{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "thesource",
+ Namespace: srcNs.Name,
+ },
+ Spec: volsyncv1alpha1.ReplicationSourceSpec{
+ Rsync: &volsyncv1alpha1.ReplicationSourceRsyncSpec{},
+ },
+ }
+ repRel.AddIDLabel(rs)
+ Expect(k8sClient.Create(ctx, rs)).To(Succeed())
+ rd = &volsyncv1alpha1.ReplicationDestination{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "thedestination",
+ Namespace: dstNs.Name,
+ Labels: map[string]string{
+ "some": "label",
+ },
+ },
+ Spec: volsyncv1alpha1.ReplicationDestinationSpec{
+ Rsync: &volsyncv1alpha1.ReplicationDestinationRsyncSpec{},
+ },
+ }
+ repRel.AddIDLabel(rd)
+ Expect(k8sClient.Create(ctx, rd)).To(Succeed())
+ repRel.data.Source = &replicationRelationshipSource{
+ RSName: rs.Name,
+ Namespace: srcNs.Name,
+ }
+ repRel.data.Destination = &replicationRelationshipDestination{
+ RDName: rd.Name,
+ Namespace: dstNs.Name,
+ }
+ })
+ AfterEach(func() {
+ Expect(k8sClient.Delete(ctx, srcNs)).To(Succeed())
+ Expect(k8sClient.Delete(ctx, dstNs)).To(Succeed())
+ })
+ It("cleans them up when trying to delete", func() {
+ rd2 := &volsyncv1alpha1.ReplicationDestination{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "thedestination2",
+ Namespace: dstNs.Name,
+ },
+ Spec: volsyncv1alpha1.ReplicationDestinationSpec{
+ Rsync: &volsyncv1alpha1.ReplicationDestinationRsyncSpec{},
+ },
+ }
+ // Note: we didn't add the relationship label, therefore it
+ // won't get deleted
+ Expect(k8sClient.Create(ctx, rd2)).To(Succeed())
+ // Set the relationship such that the Replication objs should be deleted
+ repRel.data.Source.RSName = ""
+ repRel.data.Destination.RDName = ""
+ Expect(repRel.DeleteSource(ctx, k8sClient)).To(Succeed())
+ Expect(repRel.DeleteDestination(ctx, k8sClient)).To(Succeed())
+
+ newRs := volsyncv1alpha1.ReplicationSource{}
+ Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(rs), &newRs)).NotTo(Succeed())
+ newRd := volsyncv1alpha1.ReplicationDestination{}
+ Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(rd), &newRd)).NotTo(Succeed())
+ // extra one should still be there since it doesn't have the relationship ID label
+ Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(rd2), &newRd)).To(Succeed())
+ })
+ })
+})
diff --git a/kubectl-volsync/cmd/suite_test.go b/kubectl-volsync/cmd/suite_test.go
index 25cf7216f..e7335ed78 100644
--- a/kubectl-volsync/cmd/suite_test.go
+++ b/kubectl-volsync/cmd/suite_test.go
@@ -8,6 +8,7 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
+ "k8s.io/klog/v2"
"k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
@@ -28,7 +29,9 @@ func TestCmd(t *testing.T) {
}
var _ = BeforeSuite(func() {
- logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
+ logger := zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))
+ logf.SetLogger(logger)
+ klog.SetLogger(logger)
By("bootstrapping test environment")
testEnv = &envtest.Environment{