Compare commits

...

4 Commits

Author SHA1 Message Date
Rasmus Kock Thygesen
2608cdc5f6
Merge 4632011ea4 into 65b028003e 2026-07-23 13:33:24 +02:00
renovate[bot]
65b028003e
chore(deps): update documentation dependencies to v19.2.8 (#1030)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-23 12:23:12 +02:00
Marco Nenciarini
7bcb74b987
feat: honor the operator's check_empty_wal_archive decision (#1009)
Archive() and the restore job hook each re-derived, on their own,
whether to verify the WAL archive destination is empty, by reading a
Cluster annotation and, for Archive, an on-disk marker file. That
decision belongs to the operator, which already tracks both the
annotation and the marker file's lifecycle.

Honor cnpg-i's new WALArchiveRequest/RestoreRequest field
CheckEmptyWalArchive when the operator sets it: obey it directly,
without re-inspecting the marker file. Only fall back to the previous
annotation-and-marker-file logic when talking to an operator that
predates this field.

Related: cloudnative-pg/cnpg-i#353 adds the field this depends on;
cloudnative-pg/cloudnative-pg#11216 is the operator-side counterpart.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Co-authored-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
2026-07-21 17:29:10 +02:00
Marco Nenciarini
1d777435d0
ci: run image publish in a separate parallel job (#1026)
Neither task depends on the other's output: task ci runs lint, tests,
e2e, and docs, while task publish builds and pushes the multi-arch
images. Running them as sequential steps in one job only adds their
durations. Splitting publish into its own job lets it run alongside ci
instead, and lets each job carry only the permissions it actually needs:
ci never writes to the registry, and neither task touches repository
contents, so only packages: write on the publish job remains.

QEMU is only needed for publish, since it's the only job that builds
non-native (arm64) platforms; ci's e2e-ephemeral image build is
amd64-only. The disk cleanup steps aren't needed for publish either: it
only builds two lean multi-arch images, well within the free disk space
available even on the default ubuntu-latest runner.

Note that publishing testing images no longer waits on ci passing, since
gating it away would remove the parallelism this is meant to gain.

Note: main's required status checks are currently ["DCO", "ci"]. A
failure in the new publish job will no longer block merging a PR, since
it isn't in that list (today it does, since publishing is a step inside
the required ci job). Flagging for a decision on whether publish should
be added as a required check.

Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
Co-authored-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
2026-07-21 17:03:39 +02:00
7 changed files with 249 additions and 19 deletions

View File

@ -6,12 +6,15 @@ on:
permissions: read-all permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs: jobs:
ci: ci:
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }} runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions: permissions:
packages: write contents: read
contents: write
steps: steps:
- name: Cleanup Disk - name: Cleanup Disk
if: vars.CI_RUNNERS == '' || vars.CI_RUNNERS == 'ubuntu-latest' if: vars.CI_RUNNERS == '' || vars.CI_RUNNERS == 'ubuntu-latest'
@ -39,8 +42,6 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Install QEMU static binaries
uses: docker/setup-qemu-action@v4
- name: Install Task - name: Install Task
uses: arduino/setup-task@v3.0.0 uses: arduino/setup-task@v3.0.0
- name: Install Dagger - name: Install Dagger
@ -52,6 +53,27 @@ jobs:
- name: Run CI task - name: Run CI task
run: | run: |
task ci task ci
publish:
runs-on: ${{ vars.CI_RUNNERS || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install QEMU static binaries
uses: docker/setup-qemu-action@v4
- name: Install Task
uses: arduino/setup-task@v3.0.0
- name: Install Dagger
env:
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
DAGGER_VERSION: 0.21.7
run: |
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- name: Write manifest - name: Write manifest
run: | run: |
task manifest task manifest

View File

@ -156,13 +156,17 @@ func (w WALServiceImplementation) Archive(
return nil, err return nil, err
} }
// Step 2: Check if the archive location is safe to perform archiving // Step 2: Check if the archive location is safe to perform archiving.
checkFileExisting, err := fileutils.FileExists(emptyWalArchiveFile) checkEmptyWalArchive, err := resolveArchiveEmptyWalArchiveCheck(
request.CheckEmptyWalArchive,
configuration.Cluster,
emptyWalArchiveFile,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("while checking for empty wal archive check file %q: %w", emptyWalArchiveFile, err) return nil, err
} }
if utils.IsEmptyWalArchiveCheckEnabled(&configuration.Cluster.ObjectMeta) && checkFileExisting { if checkEmptyWalArchive {
if err := CheckBackupDestination( if err := CheckBackupDestination(
ctx, ctx,
&objectStore.Spec.Configuration, &objectStore.Spec.Configuration,
@ -224,6 +228,31 @@ func (w WALServiceImplementation) Archive(
return &wal.WALArchiveResult{}, nil return &wal.WALArchiveResult{}, nil
} }
// resolveArchiveEmptyWalArchiveCheck reports whether the WAL archive
// destination must be verified before archiving this segment.
//
// The operator owns the marker file's lifecycle, so when it sets the decision
// (non-nil) that value already accounts for the marker and is obeyed as-is. A
// nil value comes from an operator that predates this field, so we fall back to
// the previous logic: the Cluster annotation combined with the on-disk marker
// file.
func resolveArchiveEmptyWalArchiveCheck(
operatorDecision *bool,
cluster *cnpgv1.Cluster,
markerFilePath string,
) (bool, error) {
if operatorDecision != nil {
return *operatorDecision, nil
}
markerFilePresent, err := fileutils.FileExists(markerFilePath)
if err != nil {
return false, fmt.Errorf("while checking for empty wal archive check file %q: %w", markerFilePath, err)
}
return utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta) && markerFilePresent, nil
}
// Restore implements the WALService interface // Restore implements the WALService interface
func (w WALServiceImplementation) Restore( func (w WALServiceImplementation) Restore(
ctx context.Context, ctx context.Context,

View File

@ -21,15 +21,20 @@ package common
import ( import (
"context" "context"
"os"
"path/filepath"
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer" barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
) )
var _ = Describe("resolveRestoreObjectStore", func() { var _ = Describe("resolveRestoreObjectStore", func() {
@ -177,3 +182,65 @@ var _ = Describe("clearEndOfWALStreamFlag", func() {
Expect(isEOS).To(BeFalse()) Expect(isEOS).To(BeFalse())
}) })
}) })
var _ = Describe("resolveArchiveEmptyWalArchiveCheck", func() {
// skipAnnotation mirrors the unexported constant in cloudnative-pg's
// pkg/utils; hard-coding the literal makes a divergence surface as a
// failing test rather than silently disabling the check.
const skipAnnotation = "cnpg.io/skipEmptyWalArchiveCheck"
clusterWith := func(annotationValue *string) *cnpgv1.Cluster {
cluster := &cnpgv1.Cluster{}
if annotationValue != nil {
cluster.Annotations = map[string]string{skipAnnotation: *annotationValue}
}
return cluster
}
// markerPath returns the marker file path inside a fresh temp dir,
// creating the file there when present is true.
markerPath := func(present bool) string {
filePath := filepath.Join(GinkgoT().TempDir(), metadata.CheckEmptyWalArchiveFile)
if present {
Expect(os.WriteFile(filePath, []byte{}, 0o600)).To(Succeed())
}
return filePath
}
When("the operator sets the decision", func() {
It("obeys true, ignoring the annotation and the marker file", func() {
// annotation would skip the check and the marker is absent, yet the
// operator's explicit true must still win.
got, err := resolveArchiveEmptyWalArchiveCheck(
ptr.To(true), clusterWith(ptr.To("enabled")), markerPath(false))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeTrue())
})
It("obeys false, ignoring the annotation and the marker file", func() {
// annotation would keep the check on and the marker is present, yet the
// operator's explicit false must still win.
got, err := resolveArchiveEmptyWalArchiveCheck(
ptr.To(false), clusterWith(nil), markerPath(true))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeFalse())
})
})
When("the operator predates the field (nil decision)", func() {
DescribeTable(
"falls back to the annotation combined with the marker file",
func(annotationValue *string, markerPresent bool, expected bool) {
got, err := resolveArchiveEmptyWalArchiveCheck(
nil, clusterWith(annotationValue), markerPath(markerPresent))
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(expected))
},
Entry("no annotation and marker present: check runs", nil, true, true),
Entry("no annotation and marker absent: check skipped", nil, false, false),
Entry("opt-out annotation and marker present: check skipped", ptr.To("enabled"), true, false),
Entry("unrelated annotation value and marker present: check runs", ptr.To("something-else"), true, true),
Entry("empty annotation value and marker present: check runs", ptr.To(""), true, true),
)
})
})

View File

@ -113,6 +113,7 @@ func (impl JobHookImpl) Restore(
configuration.Cluster, configuration.Cluster,
&targetObjectStore.Spec.Configuration, &targetObjectStore.Spec.Configuration,
targetObjectStore.Name, targetObjectStore.Name,
req.CheckEmptyWalArchive,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -250,6 +251,7 @@ func (impl *JobHookImpl) checkBackupDestination(
cluster *cnpgv1.Cluster, cluster *cnpgv1.Cluster,
barmanConfiguration *cnpgv1.BarmanObjectStoreConfiguration, barmanConfiguration *cnpgv1.BarmanObjectStoreConfiguration,
objectStoreName string, objectStoreName string,
operatorCheckEmptyWalArchive *bool,
) error { ) error {
// Get environment from cache // Get environment from cache
env, err := barmanCredentials.EnvSetCloudCredentialsAndCertificates(ctx, env, err := barmanCredentials.EnvSetCloudCredentialsAndCertificates(ctx,
@ -288,14 +290,26 @@ func (impl *JobHookImpl) checkBackupDestination(
} }
} }
// Check if we're ok to archive in the desired destination if resolveRestoreEmptyWalArchiveCheck(operatorCheckEmptyWalArchive, cluster) {
if utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta) {
return common.CheckBackupDestination(ctx, barmanConfiguration, walArchiver, serverName) return common.CheckBackupDestination(ctx, barmanConfiguration, walArchiver, serverName)
} }
return nil return nil
} }
// resolveRestoreEmptyWalArchiveCheck reports whether the destination must be
// verified before restoring. When the operator sets the decision (non-nil) it
// is obeyed as-is; a nil value comes from an operator that predates this field,
// so we fall back to the Cluster annotation. Unlike archiving, restore is a
// one-shot operation that has never been gated on the first-archive marker
// file, so the annotation is the only fallback needed.
func resolveRestoreEmptyWalArchiveCheck(operatorDecision *bool, cluster *cnpgv1.Cluster) bool {
if operatorDecision != nil {
return *operatorDecision
}
return utils.IsEmptyWalArchiveCheckEnabled(&cluster.ObjectMeta)
}
// restoreCustomWalDir moves the current pg_wal data to the specified custom wal dir and applies the symlink // restoreCustomWalDir moves the current pg_wal data to the specified custom wal dir and applies the symlink
// returns indicating if any changes were made and any error encountered in the process // returns indicating if any changes were made and any error encountered in the process
func (impl JobHookImpl) restoreCustomWalDir(ctx context.Context) (bool, error) { func (impl JobHookImpl) restoreCustomWalDir(ctx context.Context) (bool, error) {

View File

@ -0,0 +1,66 @@
/*
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 restore
import (
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
"k8s.io/utils/ptr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("resolveRestoreEmptyWalArchiveCheck", func() {
// skipAnnotation mirrors the unexported constant in cloudnative-pg's
// pkg/utils; hard-coding the literal makes a divergence surface as a
// failing test rather than silently disabling the check.
const skipAnnotation = "cnpg.io/skipEmptyWalArchiveCheck"
clusterWith := func(annotationValue *string) *cnpgv1.Cluster {
cluster := &cnpgv1.Cluster{}
if annotationValue != nil {
cluster.Annotations = map[string]string{skipAnnotation: *annotationValue}
}
return cluster
}
When("the operator sets the decision", func() {
It("obeys true even when the annotation would skip the check", func() {
Expect(resolveRestoreEmptyWalArchiveCheck(ptr.To(true), clusterWith(ptr.To("enabled")))).To(BeTrue())
})
It("obeys false even when the annotation would keep the check on", func() {
Expect(resolveRestoreEmptyWalArchiveCheck(ptr.To(false), clusterWith(nil))).To(BeFalse())
})
})
When("the operator predates the field (nil decision)", func() {
DescribeTable(
"falls back to the Cluster annotation, never to a marker file",
func(annotationValue *string, expected bool) {
Expect(resolveRestoreEmptyWalArchiveCheck(nil, clusterWith(annotationValue))).To(Equal(expected))
},
Entry("no annotation: check runs", nil, true),
Entry("opt-out annotation: check skipped", ptr.To("enabled"), false),
Entry("unrelated annotation value: check runs", ptr.To("something-else"), true),
Entry("empty annotation value: check runs", ptr.To(""), true),
)
})
})

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 restore
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestRestore(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Restore job hook test suite")
}

View File

@ -7970,9 +7970,9 @@ rc@1.2.8:
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-dom@^19.0.0: react-dom@^19.0.0:
version "19.2.7" version "19.2.8"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.7.tgz#0450dc9ae9ddbff76ef196401cd8b8c7fb466ccc" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.8.tgz#3b46b9eeda877cdff2cf13d2770fff4ae36c2ec2"
integrity sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== integrity sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==
dependencies: dependencies:
scheduler "^0.27.0" scheduler "^0.27.0"
@ -8052,9 +8052,9 @@ react-router@5.3.4, react-router@^5.3.4:
tiny-warning "^1.0.0" tiny-warning "^1.0.0"
react@^19.0.0: react@^19.0.0:
version "19.2.7" version "19.2.8"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" resolved "https://registry.yarnpkg.com/react/-/react-19.2.8.tgz#a80663dbb58d69c6fe3fd291d3cb324e8a7dff2d"
integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== integrity sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==
readable-stream@^2.0.1: readable-stream@^2.0.1:
version "2.3.8" version "2.3.8"