From 63e886e57f84390bb558bde263d78ea031b5667a Mon Sep 17 00:00:00 2001 From: jiw-team <270182909+jiw-team@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:53:19 -0600 Subject: [PATCH] Treat untracked collection backups as terminal Treat a "notfound" async backup status as terminal and unsuccessful instead of polling it indefinitely. Preserve the async status so the reason remains visible, while allowing recurring backups to finish and schedule their next run. Fixes #824 --- controllers/solrbackup_controller.go | 7 +++- controllers/util/backup_util.go | 6 +++ controllers/util/backup_util_test.go | 62 ++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/controllers/solrbackup_controller.go b/controllers/solrbackup_controller.go index 7d9c2444..c2bd2eef 100644 --- a/controllers/solrbackup_controller.go +++ b/controllers/solrbackup_controller.go @@ -311,7 +311,12 @@ func reconcileSolrCollectionBackup(ctx context.Context, backup *solrv1beta1.Solr if collectionBackupStatus.Successful == nil { collectionBackupStatus.Successful = &successful } - collectionBackupStatus.AsyncBackupStatus = "" + // Keep "notfound" so the status keeps saying why the backup could not be confirmed. + if asyncStatus == "notfound" { + collectionBackupStatus.AsyncBackupStatus = asyncStatus + } else { + collectionBackupStatus.AsyncBackupStatus = "" + } if collectionBackupStatus.FinishTime == nil { collectionBackupStatus.FinishTime = &now } diff --git a/controllers/util/backup_util.go b/controllers/util/backup_util.go index ca4056e1..a714a028 100644 --- a/controllers/util/backup_util.go +++ b/controllers/util/backup_util.go @@ -124,6 +124,12 @@ func CheckBackupForCollection(ctx context.Context, cloud *solr.SolrCloud, collec finished = true success = false } + if asyncStatus == "notfound" { + finished = true + success = false + logger.Info("Solr has no record of the collection backup, so it cannot be confirmed. Marking it unsuccessful; the backup repository may still contain a usable backup.", + "solrCloud", cloud.Name, "collection", collection) + } } else { logger.Error(err, "Error checking on collection backup", "solrCloud", cloud.Name, "collection", collection, "message", message) } diff --git a/controllers/util/backup_util_test.go b/controllers/util/backup_util_test.go index 691f8a3b..961cb620 100644 --- a/controllers/util/backup_util_test.go +++ b/controllers/util/backup_util_test.go @@ -18,13 +18,27 @@ package util import ( + "context" + "crypto/tls" + "fmt" solr "github.com/apache/solr-operator/api/v1beta1" + "github.com/apache/solr-operator/controllers/util/solr_api" + "github.com/go-logr/logr" "github.com/stretchr/testify/assert" + "io" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net/http" + "strings" "testing" ) +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + func TestSolrBackupApiParamsForVolumeRepositoryBackup(t *testing.T) { volumeRepository := &solr.SolrBackupRepository{ Name: "some-volume-repository", @@ -413,3 +427,51 @@ func TestRepositoryLookupFailsIfNoNameProvidedAndMultipleRepositoriesDefined(t * assert.Nil(t, found, "Expected GetBackupRepositoryByName to report no match") } + +// A backup whose async status Solr no longer holds must be terminal, otherwise it is checked +// forever. It is unsuccessful because success cannot be shown, not because failure is known. +func TestCheckBackupForCollectionOutcome(t *testing.T) { + tests := map[string]struct { + finished bool + success bool + }{ + "completed": {finished: true, success: true}, + "failed": {finished: true, success: false}, + "notfound": {finished: true, success: false}, + "running": {finished: false, success: false}, + "submitted": {finished: false, success: false}, + } + + defer func() { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + solr_api.SetNoVerifyTLSHttpClient(&http.Client{Transport: transport}) + }() + + for asyncStatus, expected := range tests { + solr_api.SetNoVerifyTLSHttpClient(&http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + assert.Equal(t, "REQUESTSTATUS", request.URL.Query().Get("action")) + assert.Equal(t, "backup-collection", request.URL.Query().Get("requestid")) + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(fmt.Sprintf( + `{"responseHeader":{"status":0},"status":{"state":%q}}`, asyncStatus, + ))), + }, nil + })}) + + finished, success, actualStatus, err := CheckBackupForCollection( + context.Background(), + &solr.SolrCloud{ObjectMeta: metav1.ObjectMeta{Name: "cloud", Namespace: "namespace"}}, + "collection", + "backup", + logr.Discard(), + ) + + assert.NoError(t, err) + assert.Equal(t, asyncStatus, actualStatus) + assert.Equal(t, expected.finished, finished, "Wrong finished for async status %q", asyncStatus) + assert.Equal(t, expected.success, success, "Wrong success for async status %q", asyncStatus) + } +}