Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote restore support for Medusa #454

Merged
merged 2 commits into from
May 13, 2022

Conversation

adejanovski
Copy link
Contributor

@adejanovski adejanovski commented Mar 8, 2022

What this PR does:
Allow restoring a backup from another cluster (including backups created with Medusa outside of K8ssandra).
Requires these medusa PR to be merged first, and a Medusa release so that we can update the tag for the medusa image: 448 and 449.

The design is fully documented in docs/medusa/remote-restore-design.md.

Which issue(s) this PR fixes:
Fixes #450
Fixes #451
Fixes #452

Checklist

  • Changes manually tested
  • Automated Tests added/updated
  • Documentation added/updated
  • CHANGELOG.md updated (not required for documentation PRs)
  • CLA Signed: DataStax CLA

@adejanovski adejanovski requested a review from a team as a code owner March 8, 2022 14:38
@adejanovski adejanovski marked this pull request as draft March 8, 2022 14:38
@adejanovski adejanovski changed the title Remote restore Remote restore support for Medusa Mar 8, 2022
@adejanovski adejanovski requested review from adutra and jsanda March 8, 2022 17:32
return ctrl.Result{RequeueAfter: r.DefaultDelay}, err
}

// If the CassandraBackup object was created by a sync operation, we mustn't trigger a backup.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with this approach is that I don't think "backup" definition and "backupJob" should be the same. That would solve this mentioned issue. Also, if metadata per object is necessary, we could use annotation instead of polluting the CRD with extra fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that change is coming in the next PR 👍

if !task.Status.StartTime.IsZero() {
// If there is anything in progress, simply requeue the request
if len(task.Status.InProgress) > 0 {
logger.Info("Tasks already in progress")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and next logger lines, without other info in the log statement the reader can't tell which restore process this is related to. So we need some KeyValues in the .Info()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it what line 59 does?

logger := log.FromContext(ctx).WithValues("MedusaTask", req.NamespacedName)

// Set the finish time
// Note that the time here is not accurate, but that is ok. For now we are just
// using it as a completion marker.
patch := client.MergeFrom(task.DeepCopy())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

task is already DeepCopy of instance. Do we really need double copy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm following the conventions we've used throughout the project when patching CRs.
Do you think we're wrong to do so?
I seem to remember that patches wouldn't be applied properly without this, but I could be wrong.

}

// If the task is already finished, there is nothing to do.
if taskFinished(task) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be before the previous if statement? StartTime is not zero for finished jobs either?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it would be better to move it before the previous if indeed 👍
I'll move it.

return nil, err
}

pods := make([]corev1.Pod, 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, len(podList.Items)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

// Invoke the purge operation on all pods, in the background
go func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the verification done if this go func is still alive or failed? What if the k8ssandra-operator crashes while this is running?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we lose track of the operation. For better or worse, this is still what we currently have for backups as well (I re-used the same code structure).
We could definitely improve the resiliency by making the calls non blocking with a way to check the state asynchronously, like we're about to do with backups.
The tasks we run here should be far more short lived and easy to rerun in case of failure though, so it's less worrisome than backups and could be dealt with in a future PR.

}

// Invoke the purge operation on all pods, in the background
go func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as next go func() question. Liveness check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer :)

Copy link
Contributor

@jsanda jsanda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A partial review. More to follow tomorrow...

Failed []string `json:"failed,omitempty"`
}

type TaskResult struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all of the fields in this struct specific to a purge operation? If so maybe rename to PurgeTaskResult or PurgeResult.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the case. The prepare_restore will generate TaskResult instances with just the PodName filled, leaving purge specific variables empty.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this. I think it obscures the code. From a maintenance perspective for example, how am I supposed to know that a particular field is just empty at a given point in time vs empty because it isn't used for the given task type. How about some comments to explain which fields are used for which task types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fair, I'll add the comments.

controllers/medusa/medusatask_controller.go Outdated Show resolved Hide resolved
task.Status.FinishTime = metav1.Now()
if err := r.Status().Patch(ctx, task, patch); err != nil {
logger.Error(err, "failed to patch status with finish time")
return ctrl.Result{RequeueAfter: r.DefaultDelay}, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set RequeueAfter when returning a non-nil error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return ctrl.Result{}, err will reschedule a reconcile later?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, with rate limiting. Here is the relevant code from the controller:

	result, err := c.Reconcile(ctx, req)
	switch {
	case err != nil:
		c.Queue.AddRateLimited(req)
		ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Inc()
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelError).Inc()
		log.Error(err, "Reconciler error")
	case result.RequeueAfter > 0:
		// The result.RequeueAfter request will be lost, if it is returned
		// along with a non-nil error. But this is intended as
		// We need to drive to stable reconcile loops before queuing due
		// to result.RequestAfter
		c.Queue.Forget(obj)
		c.Queue.AddAfter(req, result.RequeueAfter)
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeueAfter).Inc()
	case result.Requeue:
		c.Queue.AddRateLimited(req)
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeue).Inc()
	default:
		// Finally, if no error occurs we Forget this item so it does not
		// get queued again until another change happens.
		c.Queue.Forget(obj)
		ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelSuccess).Inc()
	}
}

Notice that when a non-nil error is returned the controller calls c.Queue.AddRateLimited(req). See here for the full source.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, I'll do the changes 👍

podList := &corev1.PodList{}
labels := client.MatchingLabels{cassdcapi.DatacenterLabel: cassdc.Name}
if err := r.List(ctx, podList, labels); err != nil {
logger.Error(err, "failed to get pods for cassandradatacenter", "CassandraDatacenter", cassdc.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I don't think we should log an ERROR statement when returning an error. It can create noise in the logs. See #281 for more info. If we need to log something it is probably for debugging purposes. See #282 for details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw you wanted us to remove these loggings. The problem is that the error printed out if we don't put our own logging is usually more obscure and doesn't give details allowing to know precisely what failed. Technically we know what failed, but it's harder to know what we were trying to do when it failed.
I'm ok with some repetition in the logs if that allows more clarity during troubleshooting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error printed out should include your error message. What isn't included though is a stack trace which can be problematic when the same error message is used in multiple places.

Whatever we do or change (if anything) for logging is a separate concern from this PR, so I think we're good to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the Wrap function from github.com/pkg/errors to get a stacktrace in the output. Instead of the logger.Error call, you could just do

return nil, errors.Wrap(err, fmt.Sprintf("failed to get pods for cassandradatacenter %s", cassdc.Name))

The output will include a stacktrace from the point where Wrap is called.

if err := r.Status().Patch(ctx, task, patch); err != nil {
logger.Error(err, "Failed to patch status")
// We received a stale object, requeue for next processing
return ctrl.Result{RequeueAfter: r.DefaultDelay}, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set RequeueAfter when returning a non-nil error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

wg.Wait()
logger.Info("finished task operations")
if err := r.Status().Patch(context.Background(), task, patch); err != nil {
logger.Error(err, "failed to patch status", "MedusaTask", fmt.Sprintf("%s/%s", task.Spec.Operation, task.Namespace))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Sprintf("%s/%s", task.Spec.Operation, task.Namespace) is used in multiple places. Maybe implement the Stringer interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good 👍

if err := r.Status().Patch(ctx, task, patch); err != nil {
logger.Error(err, "Failed to patch status")
// We received a stale object, requeue for next processing
return ctrl.Result{RequeueAfter: r.DefaultDelay}, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set RequeueAfter when returning a non-nil error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

localBackups := &medusav1alpha1.CassandraBackupList{}
if err = r.List(ctx, localBackups, client.InNamespace(task.Namespace)); err != nil {
logger.Error(err, "failed to list backups")
return ctrl.Result{RequeueAfter: r.DefaultDelay}, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set RequeueAfter when returning a non-nil error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

controllers/medusa/medusatask_controller.go Show resolved Hide resolved
@adejanovski adejanovski marked this pull request as ready for review March 21, 2022 15:54
Copy link
Contributor

@jsanda jsanda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we want to support the scenario of restoring from one bucket and then storing subsequent backups in a different bucket. Do the API changes support that? It doesn't look like it.

@adejanovski
Copy link
Contributor Author

I assume we want to support the scenario of restoring from one bucket and then storing subsequent backups in a different bucket. Do the API changes support that? It doesn't look like it.

Not yet, it's part of another upcoming ticket: #453

@adejanovski
Copy link
Contributor Author

@jsanda, I made the adjustments in Medusa so that we can support both the case where IPs aren't resolved to hostnames and the case when they are.
E2E tests are all passing and the PR is ready for another review.

.github/workflows/kind_e2e_tests.yaml Outdated Show resolved Hide resolved
.github/workflows/kind_multicluster_e2e_tests.yaml Outdated Show resolved Hide resolved
@@ -439,3 +439,7 @@ install-kuttl:
mocks:
mockery --dir=./pkg/cassandra --output=./pkg/mocks --name=ManagementApiFacade
mockery --dir=./pkg/reaper --output=./pkg/mocks --name=Manager --filename=reaper_manager.go --structname=ReaperManager

PHONY: protobuf-code-gen
protobuf-code-gen:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that the protobuf compiler is installed. It would be really nice if the Makefile automatically installed the binary into the bin dir like it does with some other tools. That could make for a follow up enhancement. For now how about a comment pointing to some docs for installing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a comment on line 443

Failed []string `json:"failed,omitempty"`
}

type TaskResult struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this. I think it obscures the code. From a maintenance perspective for example, how am I supposed to know that a particular field is just empty at a given point in time vs empty because it isn't used for the given task type. How about some comments to explain which fields are used for which task types?

@@ -18,20 +18,13 @@ package v1alpha1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we intend to replace this api with medusabackup_types, then I think we need to discuss how we want to handle breaking API changes. That is not a discussion that I want to have in the PR review 😄 Do you think though that some comments should be added to indicate that the API is deprecated?

Copy link
Contributor Author

@adejanovski adejanovski Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very much so. I had looked in the kubebuilder book if there was any annotation we could put to mark stuff as deprecated, with no luck.
But I just came across this: kubernetes-sigs/kubebuilder#2116

Turns out the annotation exists but isn't documented: //+kubebuilder:deprecatedversion:warning="example.com/v1alpha1 CronTab is deprecated"

I'll use this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect 🙂

controllers/medusa/medusabackupjob_controller.go Outdated Show resolved Hide resolved
@adejanovski
Copy link
Contributor Author

@jsanda, I've made the requested changes. Let me know what you think.
Whatever happens, I'll need to merge the associated PRs in the Medusa project and changes the tag accordingly here.

Also, I'd like to point out I've removed the backup name in the spec of MedusaBackupJob as I feel it's redundant with the meta.name for that same object. Hence, the MedusaBackupJob name will be the same as the MedusaBackup name (which only has a meta.name as well and no more spec.backupName) and also as the backup name in the object storage backend.

@adejanovski adejanovski requested a review from jsanda April 26, 2022 12:54
}

func (r *MedusaBackupJobReconciler) createMedusaBackup(ctx context.Context, backup *medusav1alpha1.MedusaBackupJob, logger logr.Logger) error {
// Create a prepare_restore medusa task to create the mapping files in each pod.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this comment is in the wrong place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch 👍

}
}

func (r *MedusaTaskReconciler) getCassandraDatacenterPods(ctx context.Context, cassdc *cassdcapi.CassandraDatacenter, logger logr.Logger) ([]corev1.Pod, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is identical to the getCassandraDatacenterPods in medusabackupjob_controller.go. Can we refactor this into a common, shared method or func?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

}

func (r *MedusaTaskReconciler) purgeOperation(ctx context.Context, task *medusav1alpha1.MedusaTask, pods []corev1.Pod, logger logr.Logger) (reconcile.Result, error) {
logger.Info("Starting purge operations")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to include the datacenter name in log statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k, I need to move the logging further in the code so that we can access the info after reading the task object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding the key/value pair to the logger in the Reconcile method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. Done in my upcoming commit 👍

}

func (r *MedusaTaskReconciler) prepareRestoreOperation(ctx context.Context, task *medusav1alpha1.MedusaTask, pods []corev1.Pod, logger logr.Logger) (reconcile.Result, error) {
logger.Info("Starting prepare restore operations")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to include the datacenter name in log statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

backupMutex := sync.Mutex{}
patch := client.MergeFrom(task.DeepCopy())

for _, p := range pods {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The control flow in this loop in nearly identical to the control flow in purgeOperation as well as in MedusaBackupJobReconciler.Reconcile. The status updates appear to be the same as well. I realize that in one case we are updating a MedusaTaskStatus and in other case we are updating a MedusaBackupJobStatus. How about moving the common fields into shared, common struct? And maybe define an interface through which the status updates are made. Both MedusaTaskStatus and MedusaBackupJobStatus would implement the interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry but I'm not entirely convinced of the benefits of doing so. Statuses between both objects are fairly different even if there are common fields.
Also, that would apply to MedusaBackupJob as well, which uses the same control flow as well.
I could see some value if we treated restore jobs and backup jobs as a tasks, and used a common reconcile loop for all, but that's not what your suggesting.
Am I missing your point?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to include my main point 🤦‍♂️ The control logic is duplicated in several places. It would be good to look at deduplicating it. Doing so would handling both MedusaTaskStatus and MedusaBackupJobStatus, hence my suggestions for the common struct.

Copy link
Contributor

@jsanda jsanda Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adejanovski I pushed some initial changes here to illustrate what I had in mind. I introduced a executePodOperations which encapsulates all of the control flow and goroutines for making calls against individual pods. Some more work is needed to handle the additional status updates in purgeOperation. With the status-related refactoring I mentioned in my earlier comment you should be able reuse this in the backup controller as well.

To be clear though, my main observation is that the control flow for executing operations against pod is duplicated in multiple places and should be refactored to eliminate the duplication. A lot of that deduplication however could be done without the status-related changes I suggested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I've refactored the code using your experiment code. Haven't pushed it as far as refactoring the status because I'd like to work on using common code between task and backup operations in a subsequent PR.
There's a lot being done here already IMO and we should scope this out.
I'll create a ticket for this if you agree.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed response. Follow up ticket works for me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I've create the issue: #541

podList := &corev1.PodList{}
labels := client.MatchingLabels{cassdcapi.DatacenterLabel: cassdc.Name}
if err := r.List(ctx, podList, labels); err != nil {
logger.Error(err, "failed to get pods for cassandradatacenter", "CassandraDatacenter", cassdc.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the Wrap function from github.com/pkg/errors to get a stacktrace in the output. Instead of the logger.Error call, you could just do

return nil, errors.Wrap(err, fmt.Sprintf("failed to get pods for cassandradatacenter %s", cassdc.Name))

The output will include a stacktrace from the point where Wrap is called.

}

// Prepare the restore by placing a mapping file in the Cassandra data volume.
if !request.RestoreJob.Status.RestorePrepared {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this for an in-place restore do we? I haven't tested but if I recall I want to say it is fine to do; however, that is a good bit of overhead that could otherwise be avoided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that at this stage, we don't know yet if it's an in place restore or not. That distinction was removed from the MedusaRestoreJob CRD as it is computed by Medusa itself... during the prepare restore task.
In our future design, it's the operator that will compute it based on the host names (if they match between the source and target clusters, then it's in place).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our future design, it's the operator that will compute it based on the host names

Will these changes be included in this PR or a follow up?

Copy link
Contributor Author

@adejanovski adejanovski Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up PR, in this PR Medusa computes the mappings, including the in place/remote nature.

err := f.Client.Create(ctx, kc)
require.NoError(err, "failed to create K8ssandraCluster")

reconcileReplicatedSecret(ctx, t, f, kc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplicate of verifyReplicatedSecretReconciled in k8ssandracluster_controller_test.go (see here). I created #537 to address it.

medusaClientFactory = NewMedusaClientFactory()

err := testEnv.Start(ctx, t, func(controlPlaneMgr manager.Manager, clientCache *clientcache.ClientCache, clusters []cluster.Cluster) error {
err := (&k8ssandractrl.K8ssandraClusterReconciler{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize we are already creating and deploying the K8ssandraCluster controller in main, but you shouldn't be. The integration tests are structured in a way to facilitate testing controllers in isolation. I created #538 to address this. It can be done a separate, follow up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's a lack of knowledge on my side, sorry. I couldn't figure out how to isolate them properly in the tests.

medusaClientFactory = NewMedusaClientFactory()

err := testEnv.Start(ctx, t, func(controlPlaneMgr manager.Manager, clientCache *clientcache.ClientCache, clusters []cluster.Cluster) error {
err := (&k8ssandractrl.K8ssandraClusterReconciler{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per #538 there shouldn't be any need to deploy the K8ssandraCluster controller for backup tests. You should only deploy the backup controller.

}

for _, env := range testEnv.GetDataPlaneEnvTests() {
dataPlaneMgr, err := ctrl.NewManager(env.Config, ctrl.Options{Scheme: scheme.Scheme})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per #538 there shouldn't be any need to deploy any other controller besides the task controller for task integration tests. Considering that the tests are already setup this way in main, I'm fine with addressing in a follow up PR.

return false
}

return !updated.Status.FinishTime.IsZero() && updated.Status.Finished[0].NbBackupsPurged == 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also verify that Finished has 3 elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// Schedule a sync if the task is a purge
if task.Spec.Operation == medusav1alpha1.OperationTypePurge {
r.scheduleSyncForPurge(task)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to handle the error returned here.

Something else I thought about while thinking about this part of the code is cleanup. We need an automatic cleanup of completed tasks. This is done for the Cassandra tasks in cass-operator.

If the CassandraDacenter on which the operation was performed is deleted, should the corresponding tasks be deleted as well? I am inclined to say yes. If we are in agreement on that then we need either owner references or some new finalizer logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're very right. Also, it's not a sync anymore as I've changed this part into directly creating the MedusaBackup object instead.
I'll rework that part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confusing with some code from the backup job controller. This is still necessary and indeed I need to handle the errors.

@@ -36,6 +36,22 @@ func createSingleMedusa(t *testing.T, ctx context.Context, namespace string, f *
verifyRestoreFinished(t, ctx, f, dcKey, backupKey)
}

func createSingleMedusaJob(t *testing.T, ctx context.Context, namespace string, f *framework.E2eFramework) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that the setup will be a bit more involved that existing tests, but we should have a e2e test that covers remote restores. Would you create a follow up ticket for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil, &ctrl.Result{RequeueAfter: 10 * time.Second}, err
}

backup := &api.MedusaBackup{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part needs to be updated. I hit this error when I tried restoring to a new cluster. I had to manually create a sync task to avoid this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the process actually. Did you expect the sync to be done automatically?
Backups won't get synced unless you run a sync MedusaTask, then you'll have them created as MedusaBackup CR which can be restored.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I misread the code last night and thought that the restore controller created the sync task as a preliminary step. It creates the prepare restore task. I'm good here.

// Create the sync task
prepare = &medusav1alpha1.MedusaTask{
ObjectMeta: metav1.ObjectMeta{
Name: request.RestoreJob.Status.RestoreKey,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the task name to something more descriptive, maybe something like <cluster-name>-<dc-name>-prepare-restore? Seeing task objects with UUIDs for their names isn't very helpful. Alternatively we could update the CRD to include the operation type and cassdc name in the output for kubectl get.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be fair, it'll be created in the namespace of the cluster that's being restored.
If multiple restores are done, we need a name that distinguish them, and using the restore key seemed like a good idea.
I'm afraid that we may create names that are too long if we stuff in things like the cluster name 😕

Alternatively we could update the CRD to include the operation type and cassdc name in the output for kubectl get.

Would you add that in the status of the CRD?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that we may create names that are too long if we stuff in things like the cluster name

Definitely a valid concern.

Would you add that in the status of the CRD?

Not in the status. There is a kubebuilder:printcolumn:name annotation. See https://github.com/k8ssandra/k8ssandra-operator/blob/main/apis/stargate/v1alpha1/stargate_types.go#L283 for examples. Would make for a nice follow up enhancement. I don't think it's needed for this PR.

// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// MedusaRestoreJobSpec defines the desired state of MedusaRestoreJob
type MedusaRestoreJobSpec struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For remote restores shouldn't we allow the bucket from which we are restoring to be specified separately instead of just reusing what is specified for backups? I am thinking that I have some CI/CD pipeline where I provision clusters from a known, good backup I would probably want to keep their storage buckets separate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing we could do with the way Medusa is currently designed, is to specify a different prefix for the sync and the restore, which would then be specified at sync and restore time. It probably means that the MedusaBackup object should contain that prefix in the spec.
This is part of a follow up ticket: #453

@@ -66,6 +66,7 @@ jobs:
- CreateMultiReaper
- ClusterScoped/MultiDcMultiCluster
- CreateMultiMedusa
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateMultiMedusa is matching both CreateMultiMedusaOld and CreateMultiMedusaJob, so both tests are being run and CreateMultiMedusaJob is being run twice. It should be changed to CreateMultiMedusaOld.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, I was pretty sure I fixed this already 🤔
Good catch!

Copy link
Contributor

@jsanda jsanda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna go ahead and approve so we don't have to wait until I am back online. I spent some time investigating the e2e test failure, and I think we are good to go with a small change. @adejanovski well done 👏

@adejanovski adejanovski force-pushed the remote-restore branch 3 times, most recently from 29f6967 to 8bcd162 Compare May 13, 2022 07:39
… implement remote restore.

The PrepareRestore grpc call will have Medusa compute the mapping between the backup and restore clusters, and store it in the local storage.
This will allow the restore init container to know which mapping should be performed, without having Cassandra up.
Disable ip address resolving in Medusa to allow restore mappings.
@adejanovski adejanovski merged commit 2e6ed83 into k8ssandra:main May 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants