Compare commits

..

5 Commits

Author SHA1 Message Date
Leonardo Cecchi
18e0ae58d9
Merge 5560083160 into 0bb78879ce 2026-05-28 15:35:55 +00:00
Armando Ruocco
5560083160 chore(crd): regenerate after barman-cloud restoreAdditionalCommandArgs addition
The replace directive in go.mod pulls in a barman-cloud version that
exposes the new restoreAdditionalCommandArgs field on
BarmanObjectStoreConfiguration. The plugin's CRD embeds that struct,
so controller-gen + kustomize must be rerun to surface the field in
the published CRD and bundled manifest. CI's uncommitted-changes
guard caught the gap.

Regenerated with controller-gen v0.21.0 (matching the daggerverse
module version) and kustomize v5.5.0; only the new field is added,
no annotation drift.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-05-28 17:35:07 +02:00
Armando Ruocco
7c63be1d24 test(common): cover WAL restore error classification
Extract the barman-restorer error -> gRPC code switch from
restoreFromBarmanObjectStore into a pure helper,
classifyWALRestoreError, so it can be exercised in isolation
without the surrounding k8s client / configuration scaffolding.

Add ginkgo specs that check:

  - each barman sentinel maps to the expected gRPC status code
    (ErrConnectivity and ErrGeneric both -> Unavailable, since
    barman uses exit 4 for some retryable conditions too),
  - an unclassified error falls through to codes.Internal,
  - classification still works through multiple fmt.Errorf wraps,
  - the switch matches by errors.Is identity rather than message
    substring (so a NotFound whose message happens to mention
    "connectivity" still maps to NotFound).

internal/cnpgi/common had no tests before; this introduces the
suite scaffolding alongside the new specs.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-05-28 16:27:00 +02:00
Armando Ruocco
273bbb474c docs(wal): describe gRPC code intent, not future operator behavior
The per-case and helper comments asserted what the operator will do
with each gRPC code ("the operator stops retrying", "the operator
will retry the request", etc.). On the current cloudnative-pg main,
the operator only differentiates ErrWALNotFound; the precise-code
distinctions become meaningful only after the operator-side retry
work lands.

Rewrite the comments to describe what the code emits and why (per
gRPC convention), so they stay accurate regardless of the operator
version on the other end. Also note that barman returns ErrGeneric
(exit 4) for some retryable conditions too — not only ErrConnectivity
(exit 2) — which justifies why both map to codes.Unavailable.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-05-28 16:25:28 +02:00
Leonardo Cecchi
263ebe2bc1 fix: classify WAL restore errors with precise gRPC statuses
Map each restorer sentinel to the gRPC status that best reflects whether
the operator should retry:

  - ErrWALNotFound      -> NotFound        (terminal)
  - ErrInvalidWalName   -> InvalidArgument (terminal)
  - ErrConnectivity     -> Unavailable     (retry)
  - ErrGeneric          -> Unavailable     (retry)
  - anything else       -> Internal        (terminal)

Previously every non-NotFound failure was returned verbatim, leaving the
operator unable to tell transient blips apart from terminal conditions
like a malformed WAL name.

Signed-off-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com>
2026-05-28 15:54:29 +02:00
6 changed files with 200 additions and 39 deletions

View File

@ -176,6 +176,25 @@ spec:
format: int32
minimum: 1
type: integer
restoreAdditionalCommandArgs:
description: |-
Additional arguments that can be appended to the 'barman-cloud-restore'
command-line invocation. These arguments provide flexibility to customize
the data restore process further, according to specific requirements or
configurations.
Example:
In a scenario where specialized restore options are required, such as setting
a specific read timeout or defining custom behavior, users can use this field
to specify additional command arguments.
Note:
It's essential to ensure that the provided arguments are valid and supported
by the 'barman-cloud-restore' command, to avoid potential errors or unintended
behavior during execution.
items:
type: string
type: array
type: object
destinationPath:
description: |-

View File

@ -20,10 +20,38 @@ SPDX-License-Identifier: Apache-2.0
package common
import (
"errors"
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// classifyWALRestoreError maps an error returned by the WAL
// restorer to a gRPC-coded error so the caller can tell terminal
// failures apart from transient ones via the status code.
func classifyWALRestoreError(walName string, walErr error) error {
switch {
case errors.Is(walErr, barmanRestorer.ErrWALNotFound):
return newWALNotFoundError(walName)
case errors.Is(walErr, barmanRestorer.ErrInvalidWalName):
// A malformed WAL name will never become valid on retry.
return newInvalidWALNameError(walName, walErr)
case errors.Is(walErr, barmanRestorer.ErrConnectivity),
errors.Is(walErr, barmanRestorer.ErrGeneric):
// barman-cloud exit codes 2 (connectivity) and 4
// (generic) both surface conditions that are retryable
// in practice — barman uses the "generic" bucket for
// some connection-class failures too, not just exit 2.
return newUnavailableError(walName, walErr)
default:
// Unrecognized exit codes and unexpected failures (e.g.
// the barman-cloud command could not be executed). No
// positive signal that retry would help.
return newInternalWALRestoreError(walName, walErr)
}
}
// ErrEndOfWALStreamReached is returned when end of WAL is detected in the cloud archive.
var ErrEndOfWALStreamReached = status.Errorf(codes.OutOfRange, "end of WAL reached")
@ -35,19 +63,18 @@ var ErrEndOfWALStreamReached = status.Errorf(codes.OutOfRange, "end of WAL reach
var ErrMissingPermissions = status.Errorf(codes.FailedPrecondition,
"no permission to download the backup credentials, retrying")
// newWALNotFoundError returns an error indicating that the
// requested WAL file is not present in the object store.
// It carries a gRPC NotFound status so the operator can
// treat it as a terminal condition.
// newWALNotFoundError reports that the requested WAL file is not
// present in the object store. Emits codes.NotFound: this is a
// terminal condition (the file won't appear on retry).
func newWALNotFoundError(walName string) error {
return status.Errorf(codes.NotFound, "wal %q not found", walName)
}
// newUnavailableError returns an error indicating that
// downloading the given WAL file failed for a transient
// reason (e.g. a connectivity blip). It carries a gRPC
// Unavailable status so the operator will retry the
// request.
// newUnavailableError reports that downloading the WAL file failed
// for a reason expected to be transient (a barman-cloud connectivity
// blip, or a generic exit code that in practice covers retryable
// conditions too). Emits codes.Unavailable: per gRPC conventions,
// the canonical signal for "retry may succeed".
func newUnavailableError(walName string, err error) error {
return status.Errorf(
codes.Unavailable,
@ -57,10 +84,9 @@ func newUnavailableError(walName string, err error) error {
)
}
// newInvalidWALNameError returns an error indicating that
// the requested WAL name is not valid. It carries a gRPC
// InvalidArgument status so the operator treats it as a
// terminal condition.
// newInvalidWALNameError reports that the requested WAL name is
// not a valid name. Emits codes.InvalidArgument: this is a
// terminal condition (the same name won't become valid on retry).
func newInvalidWALNameError(walName string, err error) error {
return status.Errorf(
codes.InvalidArgument,
@ -70,10 +96,10 @@ func newInvalidWALNameError(walName string, err error) error {
)
}
// newInternalWALRestoreError returns an error indicating
// that downloading the given WAL file failed for an
// unclassified reason. It carries a gRPC Internal status
// so the operator treats it as a terminal condition.
// newInternalWALRestoreError reports that downloading the WAL
// file failed for an unclassified reason. Emits codes.Internal:
// we have no positive signal that retry would help, so by gRPC
// convention this is treated as terminal.
func newInternalWALRestoreError(walName string, err error) error {
return status.Errorf(
codes.Internal,

View File

@ -0,0 +1,86 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package common
import (
"errors"
"fmt"
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ = Describe("classifyWALRestoreError", func() {
const walName = "000000010000000000000001"
DescribeTable(
"maps barman restorer sentinels to gRPC status codes",
func(walErr error, expectedCode codes.Code) {
got := classifyWALRestoreError(walName, walErr)
Expect(got).To(HaveOccurred())
st, ok := status.FromError(got)
Expect(ok).To(BeTrue(), "returned error must carry a gRPC status")
Expect(st.Code()).To(Equal(expectedCode))
Expect(st.Message()).To(ContainSubstring(walName))
},
Entry("ErrWALNotFound -> NotFound",
fmt.Errorf("object storage or file not found: %w", barmanRestorer.ErrWALNotFound),
codes.NotFound),
Entry("ErrInvalidWalName -> InvalidArgument",
fmt.Errorf("invalid name for a WAL file: %w", barmanRestorer.ErrInvalidWalName),
codes.InvalidArgument),
Entry("ErrConnectivity -> Unavailable",
fmt.Errorf("connectivity failure, retrying: %w", barmanRestorer.ErrConnectivity),
codes.Unavailable),
Entry("ErrGeneric -> Unavailable (barman uses exit 4 for some retryable cases too)",
fmt.Errorf("generic error: %w", barmanRestorer.ErrGeneric),
codes.Unavailable),
Entry("unknown error -> Internal",
errors.New("something we did not classify"),
codes.Internal),
)
It("matches the sentinel even through several wrapping layers", func() {
// The plugin wraps barman errors via fmt.Errorf("...: %w", ...);
// classification must keep working if more wraps appear above.
inner := fmt.Errorf("connectivity failure, retrying: %w", barmanRestorer.ErrConnectivity)
wrapped := fmt.Errorf("while restoring WAL %s: %w", walName, inner)
got := classifyWALRestoreError(walName, wrapped)
st, ok := status.FromError(got)
Expect(ok).To(BeTrue())
Expect(st.Code()).To(Equal(codes.Unavailable))
})
It("treats ErrWALNotFound as terminal even when the error chain mentions other sentinels in its message", func() {
// Defensive: if the underlying error stringifies to something
// resembling another sentinel's message, the switch must still
// match by identity (errors.Is), not by substring.
walErr := fmt.Errorf("not found, looks like a connectivity failure: %w", barmanRestorer.ErrWALNotFound)
got := classifyWALRestoreError(walName, walErr)
st, _ := status.FromError(got)
Expect(st.Code()).To(Equal(codes.NotFound))
})
})

View File

@ -0,0 +1,32 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SPDX-License-Identifier: Apache-2.0
*/
package common
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestCommon(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Common Suite")
}

View File

@ -365,28 +365,7 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
// is the one that PostgreSQL has requested to restore.
// The failure has already been logged in walRestorer.RestoreList method
if walStatus[0].Err != nil {
walName := walStatus[0].WalName
walErr := walStatus[0].Err
switch {
case errors.Is(walErr, barmanRestorer.ErrWALNotFound):
// A missing WAL is a terminal condition: surface it as a
// gRPC NotFound so the operator stops retrying.
return newWALNotFoundError(walName)
case errors.Is(walErr, barmanRestorer.ErrInvalidWalName):
// A malformed WAL name will never succeed on retry.
return newInvalidWALNameError(walName, walErr)
case errors.Is(walErr, barmanRestorer.ErrConnectivity),
errors.Is(walErr, barmanRestorer.ErrGeneric):
// Connectivity and generic barman-cloud failures are
// considered transient: surface them as gRPC Unavailable
// so the operator retries the download.
return newUnavailableError(walName, walErr)
default:
// Unrecognized exit codes and unexpected failures (e.g. the
// barman-cloud command could not be executed) are surfaced
// as gRPC Internal so the operator treats them as terminal.
return newInternalWALRestoreError(walName, walErr)
}
return classifyWALRestoreError(walStatus[0].WalName, walStatus[0].Err)
}
// We skip this step if streaming connection is not available

View File

@ -175,6 +175,25 @@ spec:
format: int32
minimum: 1
type: integer
restoreAdditionalCommandArgs:
description: |-
Additional arguments that can be appended to the 'barman-cloud-restore'
command-line invocation. These arguments provide flexibility to customize
the data restore process further, according to specific requirements or
configurations.
Example:
In a scenario where specialized restore options are required, such as setting
a specific read timeout or defining custom behavior, users can use this field
to specify additional command arguments.
Note:
It's essential to ensure that the provided arguments are valid and supported
by the 'barman-cloud-restore' command, to avoid potential errors or unintended
behavior during execution.
items:
type: string
type: array
type: object
destinationPath:
description: |-