Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions kubectl-volsync/cmd/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
24 changes: 24 additions & 0 deletions kubectl-volsync/cmd/relationship_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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()))
})
})
})
222 changes: 222 additions & 0 deletions kubectl-volsync/cmd/replication.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
64 changes: 64 additions & 0 deletions kubectl-volsync/cmd/replication_create.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
Loading