Skip to content
Open
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
7 changes: 6 additions & 1 deletion internal/cnpgi/instance/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ func (b BackupServiceImplementation) Backup(
EndLsn: executedBackupInfo.EndLSN,
InstanceId: b.InstanceName,
Online: true,
Metadata: newBackupResultMetadata(configuration.Cluster.ObjectMeta.UID, executedBackupInfo.TimeLine).toMap(),
Metadata: newBackupResultMetadata(
configuration.Cluster.ObjectMeta.UID,
executedBackupInfo.TimeLine,
&objectStore,
configuration.ServerName,
).toMap(),
}, nil
}

Expand Down
53 changes: 50 additions & 3 deletions internal/cnpgi/instance/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ SPDX-License-Identifier: Apache-2.0
package instance

import (
"net/url"
"strconv"

"k8s.io/apimachinery/pkg/types"

barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
)

Expand All @@ -34,29 +36,63 @@ type backupResultMetadata struct {
displayName string
clusterUID string
pluginName string

barmanObjectName string
serverName string
destinationPath string
endpointURL string
}

func (b backupResultMetadata) toMap() map[string]string {
return map[string]string{
result := map[string]string{
"timeline": b.timeline,
"version": b.version,
"name": b.name,
"displayName": b.displayName,
"clusterUID": b.clusterUID,
"pluginName": b.pluginName,
}

// location keys are omitted when empty (for retrocompatibility)
location := map[string]string{
"barmanObjectName": b.barmanObjectName,
"serverName": b.serverName,
"destinationPath": b.destinationPath,
"endpointURL": b.endpointURL,
}
for key, value := range location {
if len(value) > 0 {
result[key] = value
}
}

return result
}

func newBackupResultMetadata(clusterUID types.UID, timeline int) backupResultMetadata {
return backupResultMetadata{
func newBackupResultMetadata(
clusterUID types.UID,
timeline int,
objectStore *barmancloudv1.ObjectStore,
serverName string,
) backupResultMetadata {
result := backupResultMetadata{
timeline: strconv.Itoa(timeline),
clusterUID: string(clusterUID),
serverName: serverName,
// static values
version: metadata.Data.Version,
name: metadata.Data.Name,
displayName: metadata.Data.DisplayName,
pluginName: metadata.PluginName,
}

if objectStore != nil {
result.barmanObjectName = objectStore.Name
result.destinationPath = redactURL(objectStore.Spec.Configuration.DestinationPath)
result.endpointURL = redactURL(objectStore.Spec.Configuration.EndpointURL)
}

return result
}

func newBackupResultMetadataFromMap(m map[string]string) backupResultMetadata {
Expand All @@ -73,3 +109,14 @@ func newBackupResultMetadataFromMap(m map[string]string) backupResultMetadata {
pluginName: m["pluginName"],
}
}

// redactURL masks any password embedded in the given URL, so that it
// is not exposed in the status of the Backup object
func redactURL(rawURL string) string {
parsed, err := url.Parse(rawURL)
if err != nil || parsed.User == nil {
return rawURL
}

return parsed.Redacted()
}
97 changes: 97 additions & 0 deletions internal/cnpgi/instance/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
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 instance

import (
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("backupResultMetadata", func() {
newObjectStore := func(endpointURL string) *barmancloudv1.ObjectStore {
return &barmancloudv1.ObjectStore{
ObjectMeta: metav1.ObjectMeta{Name: "store", Namespace: "default"},
Spec: barmancloudv1.ObjectStoreSpec{
Configuration: barmanapi.BarmanObjectStoreConfiguration{
DestinationPath: "s3://bucket/path",
EndpointURL: endpointURL,
},
},
}
}

It("records the location the backup was written to", func() {
m := newBackupResultMetadata("uid", 2, newObjectStore("https://s3.example.com"), "mydb-20261003T120000").toMap()
Expect(m).To(Equal(map[string]string{
"timeline": "2",
"clusterUID": "uid",
"version": metadata.Data.Version,
"name": metadata.Data.Name,
"displayName": metadata.Data.DisplayName,
"pluginName": metadata.PluginName,
"barmanObjectName": "store",
"serverName": "mydb-20261003T120000",
"destinationPath": "s3://bucket/path",
"endpointURL": "https://s3.example.com",
}))
})

It("omits the endpointURL when it is not set", func() {
m := newBackupResultMetadata("uid", 1, newObjectStore(""), "mydb").toMap()
Expect(m).NotTo(HaveKey("endpointURL"))
Expect(m).To(HaveKeyWithValue("destinationPath", "s3://bucket/path"))
})

It("omits the location keys of a missing object store", func() {
m := newBackupResultMetadata("uid", 1, nil, "mydb").toMap()
Expect(m).To(HaveKeyWithValue("serverName", "mydb"))
Expect(m).NotTo(HaveKey("barmanObjectName"))
Expect(m).NotTo(HaveKey("destinationPath"))
Expect(m).NotTo(HaveKey("endpointURL"))
})

It("omits the serverName when it is not set", func() {
m := newBackupResultMetadata("uid", 1, newObjectStore(""), "").toMap()
Expect(m).NotTo(HaveKey("serverName"))
Expect(m).To(HaveKeyWithValue("barmanObjectName", "store"))
})

It("redacts credentials embedded in the endpointURL", func() {
m := newBackupResultMetadata("uid", 1, newObjectStore("https://user:secret@s3.example.com"), "mydb").toMap()
Expect(m).To(HaveKeyWithValue("endpointURL", "https://user:xxxxx@s3.example.com"))
})
})

var _ = Describe("redactURL", func() {
It("masks passwords embedded in the URL", func() {
Expect(redactURL("https://user:secret@s3.example.com")).To(Equal("https://user:xxxxx@s3.example.com"))
})

It("leaves URLs without credentials untouched", func() {
Expect(redactURL("s3://bucket/path")).To(Equal("s3://bucket/path"))
Expect(redactURL("")).To(BeEmpty())
})
})
31 changes: 31 additions & 0 deletions web/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,37 @@ kubectl cnpg backup -n <namespace> <cluster-name> \
```
:::

### Locating a backup

Once a backup completes, the plugin records where it was written in the
`.status.pluginMetadata` section of the `Backup` object:

| Key | Value |
|--------------------|-----------------------------------------------------------------------------|
| `barmanObjectName` | Name of the `ObjectStore` the backup was written to |
| `serverName` | Server name used for the backup (the `serverName` parameter, or the cluster name when unset) |
| `destinationPath` | `destinationPath` of the `ObjectStore` at backup time |
| `endpointURL` | `endpointURL` of the `ObjectStore` at backup time, only when set |

Credentials embedded in `destinationPath` or `endpointURL` are masked.

Use `barmanObjectName` and `serverName` to fill in the `externalClusters`
entry when [restoring a cluster](#restoring-a-cluster) from this backup.
`destinationPath` and `endpointURL` show whether the `ObjectStore` has
been changed to point somewhere else since the backup was taken.

:::important
`barmanObjectName` always refers to an `ObjectStore` in the namespace of
the `Cluster` that uses it. To restore into a different namespace, create
an `ObjectStore` there with the same `destinationPath`, `endpointURL` and
credentials, and reference that one instead.
:::

:::note
Backups taken with plugin versions that predate this feature don't carry
these keys.
:::

## Restoring a Cluster

To restore a cluster from an object store, create a new `Cluster` resource that
Expand Down