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
15 changes: 13 additions & 2 deletions velero-plugins/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ const (
DCPodLabels string = "oadp.openshift.io/pod-labels" // labels from DC pod
)

// Namespace SCC UID/GID range bookkeeping annotations.
// Namespace objects never pass through RestoreItemAction plugins (Velero core
// special-cases and skips them), so these values are stashed onto each backed-up
// ServiceAccount at backup time and read back at restore time to detect a
// UID/GID-range mismatch between the backed-up and restored namespace.
const (
BackupNsSccUIDRange string = "oadp.openshift.io/backup-ns-scc-uid-range"
BackupNsSccSupplementalGroups string = "oadp.openshift.io/backup-ns-scc-supplemental-groups"
BackupNsSccMcs string = "oadp.openshift.io/backup-ns-scc-mcs"
)

// Configmap Name
const RegistryConfigMap string = "oadp-registry-config"

Expand All @@ -127,8 +138,8 @@ const (
// CSI-Addons controller (shipped with ODF) uses to create a ReclaimSpaceCronJob
// that runs rbd sparsify automatically. No-op when CSI-Addons is not installed.
const (
ReclaimSpaceScheduleAnnotation string = "oadp.openshift.io/reclaim-space-schedule" // set on Restore CR
CSIAddonsReclaimSpaceScheduleAnnotation string = "reclaimspace.csiaddons.openshift.io/schedule" // set on restored PVCs
ReclaimSpaceScheduleAnnotation string = "oadp.openshift.io/reclaim-space-schedule" // set on Restore CR
CSIAddonsReclaimSpaceScheduleAnnotation string = "reclaimspace.csiaddons.openshift.io/schedule" // set on restored PVCs
)

// OVN-Kubernetes and Multus CNI-injected annotations.
Expand Down
11 changes: 11 additions & 0 deletions velero-plugins/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/konveyor/openshift-velero-plugin/velero-plugins/imagestreamtag"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/imagetag"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/job"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/namespacescc"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/nonadmin"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/persistentvolume"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/pod"
Expand All @@ -40,6 +41,8 @@ func main() {
RegisterBackupItemAction("openshift.io/02-serviceaccount-backup-plugin", newServiceAccountBackupPlugin).
RegisterRestoreItemAction("openshift.io/02-serviceaccount-restore-plugin", newServiceAccountRestorePlugin).
RegisterItemBlockAction("openshift.io/02-serviceaccount-iba-plugin", newServiceAccountIBAPlugin).
RegisterBackupItemAction("openshift.io/02-namespacescc-backup-plugin", newNamespaceSccBackupPlugin).
RegisterRestoreItemAction("openshift.io/02-namespacescc-restore-plugin", newNamespaceSccRestorePlugin).
RegisterBackupItemAction("openshift.io/03-pv-backup-plugin", newPVBackupPlugin).
RegisterRestoreItemAction("openshift.io/03-pv-restore-plugin", newPVRestorePlugin).
RegisterRestoreItemAction("openshift.io/04-pvc-restore-plugin", newPVCRestorePlugin).
Expand Down Expand Up @@ -240,3 +243,11 @@ func newNonAdminRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
func newRBACRoleBindingRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
return &rolebindings.K8sRestorePlugin{Log: logger}, nil
}

func newNamespaceSccBackupPlugin(logger logrus.FieldLogger) (interface{}, error) {
return &namespacescc.BackupPlugin{Log: logger}, nil
}

func newNamespaceSccRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
return &namespacescc.RestorePlugin{Log: logger}, nil
}
127 changes: 127 additions & 0 deletions velero-plugins/namespacescc/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package namespacescc

import (
"context"
"encoding/json"
"sync"

"github.com/konveyor/openshift-velero-plugin/velero-plugins/clients"
"github.com/konveyor/openshift-velero-plugin/velero-plugins/common"
apisecurity "github.com/openshift/api/security/v1"
"github.com/sirupsen/logrus"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

// sccAnnotationCarrier maps the namespace's SCC UID/GID-range annotations to the
// bookkeeping annotation keys they're stashed under on the ServiceAccount.
var sccAnnotationCarrier = map[string]string{
apisecurity.UIDRangeAnnotation: common.BackupNsSccUIDRange,
apisecurity.SupplementalGroupsAnnotation: common.BackupNsSccSupplementalGroups,
apisecurity.MCSAnnotation: common.BackupNsSccMcs,
}

// BackupPlugin stashes the parent namespace's SCC UID/GID-range annotations onto
// each ServiceAccount at backup time. Namespace objects never pass through
// RestoreItemAction plugins on restore (Velero core special-cases and skips
// them), so ServiceAccounts - always present in every namespace - are used as
// the carrier to detect a range mismatch at restore time (see restore.go).
type BackupPlugin struct {
Log logrus.FieldLogger

// namespaceAnnotationCache avoids one Namespaces().Get() per ServiceAccount
// when a namespace has multiple service accounts. Cleared whenever
// cachedForBackup no longer matches the current backup, so entries don't
// leak across backups handled by the same long-lived plugin process.
// Guarded by mu since the shared plugin process may serve concurrent
// operations.
mu sync.Mutex
namespaceAnnotationCache map[string]map[string]string
cachedForBackup string
}

// AppliesTo returns a velero.ResourceSelector that applies to service accounts.
func (p *BackupPlugin) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"serviceaccounts"},
}, nil
}

// Execute stashes the namespace's SCC UID/GID-range annotations onto the service account being backed up.
func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []velero.ResourceIdentifier, error) {
p.Log.Info("[namespacescc-backup] Entering namespace SCC range backup plugin")

serviceAccount := corev1.ServiceAccount{}
itemMarshal, _ := json.Marshal(item)
json.Unmarshal(itemMarshal, &serviceAccount)
Comment on lines +59 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '=== outline backup.go ===\n'
ast-grep outline velero-plugins/namespacescc/backup.go --view expanded || true
printf '\n=== outline restore.go ===\n'
ast-grep outline velero-plugins/namespacescc/restore.go --view expanded || true
printf '\n=== backup.go excerpt ===\n'
cat -n velero-plugins/namespacescc/backup.go | sed -n '1,220p'
printf '\n=== restore.go excerpt ===\n'
cat -n velero-plugins/namespacescc/restore.go | sed -n '1,220p'

Repository: openshift/openshift-velero-plugin

Length of output: 14216


🏁 Script executed:

set -e
printf '=== json conversion usages in namespacescc ===\n'
rg -n 'json\.(Marshal|Unmarshal)|DefaultUnstructuredConverter|UnstructuredConverter' velero-plugins/namespacescc || true
printf '\n=== related tests or helpers ===\n'
rg -n 'namespacescc|ServiceAccount|unstructured' velero-plugins -g '*_test.go' || true

Repository: openshift/openshift-velero-plugin

Length of output: 1786


🏁 Script executed:

set -e
printf '=== backup_test.go excerpt ===\n'
cat -n velero-plugins/namespacescc/backup_test.go | sed -n '1,220p'
printf '\n=== restore_test.go excerpt ===\n'
cat -n velero-plugins/namespacescc/restore_test.go | sed -n '1,240p'

Repository: openshift/openshift-velero-plugin

Length of output: 8537


🏁 Script executed:

set -e
cat >/tmp/namespacescc_json_verify.go <<'GO'
package main

import (
	"encoding/json"
	"fmt"
)

type ServiceAccount struct {
	Namespace   string            `json:"namespace"`
	Annotations map[string]string `json:"annotations"`
}

func main() {
	bad := []byte(`{"namespace": 123, "annotations": {"a": "b"}}`)
	var sa ServiceAccount
	err := json.Unmarshal(bad, &sa)
	fmt.Printf("unmarshal err: %v\n", err)
	fmt.Printf("result: %#v\n", sa)

	var out map[string]interface{}
	marshalBad := []byte(`{"namespace":"ns","annotations":` + "`" + `oops` + "`" + `}`)
	err = json.Unmarshal(marshalBad, &out)
	fmt.Printf("unmarshal-to-map err: %v\n", err)
	fmt.Printf("map is nil: %v\n", out == nil)
}
GO
go run /tmp/namespacescc_json_verify.go

Repository: openshift/openshift-velero-plugin

Length of output: 451


Propagate these conversion errors.

These JSON round-trips are unchecked in both backup and restore. A failed decode can leave a partially populated ServiceAccount, and a failed encode/decode can return a nil Object while still reporting success. Return the error (or use runtime.DefaultUnstructuredConverter) at:

  • velero-plugins/namespacescc/backup.go#L59-L60, #L73-L75
  • velero-plugins/namespacescc/restore.go#L51-L52, #L56-L58, #L78-L80
📍 Affects 2 files
  • velero-plugins/namespacescc/backup.go#L59-L60 (this comment)
  • velero-plugins/namespacescc/backup.go#L73-L75
  • velero-plugins/namespacescc/restore.go#L51-L52
  • velero-plugins/namespacescc/restore.go#L56-L58
  • velero-plugins/namespacescc/restore.go#L78-L80
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@velero-plugins/namespacescc/backup.go` around lines 59 - 60, Propagate all
JSON conversion errors instead of ignoring them, preventing partially populated
ServiceAccount values or nil Objects from being treated as successful. Update
the conversion sites in velero-plugins/namespacescc/backup.go at lines 59-60 and
73-75, and velero-plugins/namespacescc/restore.go at lines 51-52, 56-58, and
78-80; either return each Marshal/Unmarshal error through the surrounding backup
and restore flows or replace the round-trips with
runtime.DefaultUnstructuredConverter.

Source: Path instructions


namespaceAnnotations, err := p.getNamespaceAnnotations(backup.Name, serviceAccount.Namespace)
if err != nil {
return nil, nil, err
}

annotations, stashed := stashNamespaceSCCAnnotations(serviceAccount.Annotations, namespaceAnnotations)
if !stashed {
return item, nil, nil
}
serviceAccount.Annotations = annotations

var out map[string]interface{}
objrec, _ := json.Marshal(serviceAccount)
json.Unmarshal(objrec, &out)

return &unstructured.Unstructured{Object: out}, nil, nil
}

// getNamespaceAnnotations returns the annotations of the named namespace,
// caching the result for the lifetime of the current backup so a namespace
// with multiple service accounts only needs one Get() call.
func (p *BackupPlugin) getNamespaceAnnotations(backupName, namespace string) (map[string]string, error) {
p.mu.Lock()
defer p.mu.Unlock()

if p.cachedForBackup != backupName {
p.namespaceAnnotationCache = map[string]map[string]string{}
p.cachedForBackup = backupName
}
if annotations, ok := p.namespaceAnnotationCache[namespace]; ok {
return annotations, nil
}

client, err := clients.CoreClient()
if err != nil {
return nil, err
}
ns, err := client.Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## backup.go\n'
cat -n velero-plugins/namespacescc/backup.go | sed -n '1,220p'

printf '\n## restore.go\n'
cat -n velero-plugins/namespacescc/restore.go | sed -n '1,220p'

printf '\n## search for mutex/context usage\n'
rg -n "WithTimeout|context\\.Background|mutex|Lock\\(|Unlock\\(|ServiceAccount|Namespaces\\(\\)\\.Get" velero-plugins/namespacescc -S

Repository: openshift/openshift-velero-plugin

Length of output: 14037


Bound the namespace lookups with a timeout.
context.Background() leaves both Namespaces().Get(...) calls uncancellable, and each one runs while mu is held, so a slow apiserver can stall every ServiceAccount processed by the same plugin instance.

  • velero-plugins/namespacescc/backup.go#L99
  • velero-plugins/namespacescc/restore.go#L104
📍 Affects 2 files
  • velero-plugins/namespacescc/backup.go#L99-L99 (this comment)
  • velero-plugins/namespacescc/restore.go#L104-L104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@velero-plugins/namespacescc/backup.go` at line 99, Replace the uncancellable
context used by the Namespaces().Get lookups with a bounded-timeout context in
both velero-plugins/namespacescc/backup.go:99 and
velero-plugins/namespacescc/restore.go:104. Update the surrounding backup and
restore lookup flows to create and cancel the timeout context appropriately
while preserving the existing namespace retrieval behavior.

Source: Path instructions

if err != nil {
return nil, err
}

p.namespaceAnnotationCache[namespace] = ns.Annotations
return ns.Annotations, nil
}

// stashNamespaceSCCAnnotations returns a copy of saAnnotations with the
// namespace's SCC UID/GID-range annotations (if present on namespaceAnnotations)
// stashed under bookkeeping keys. The second return value reports whether
// anything was stashed.
func stashNamespaceSCCAnnotations(saAnnotations, namespaceAnnotations map[string]string) (map[string]string, bool) {
var stashed bool
out := saAnnotations
for src, dst := range sccAnnotationCarrier {
v, ok := namespaceAnnotations[src]
if !ok || v == "" {
continue
}
if out == nil {
out = map[string]string{}
}
out[dst] = v
stashed = true
}
return out, stashed
}
77 changes: 77 additions & 0 deletions velero-plugins/namespacescc/backup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package namespacescc

import (
"reflect"
"testing"

"github.com/konveyor/openshift-velero-plugin/velero-plugins/util/test"
apisecurity "github.com/openshift/api/security/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
)

func TestBackupPluginAppliesTo(t *testing.T) {
backupPlugin := &BackupPlugin{Log: test.NewLogger()}
actual, err := backupPlugin.AppliesTo()
require.NoError(t, err)
assert.Equal(t, velero.ResourceSelector{IncludedResources: []string{"serviceaccounts"}}, actual)
}

func Test_stashNamespaceSCCAnnotations(t *testing.T) {
tests := []struct {
name string
saAnnotations map[string]string
namespaceAnnotations map[string]string
wantAnnotations map[string]string
wantStashed bool
}{
{
name: "namespace has no SCC annotations, nothing stashed",
saAnnotations: nil,
namespaceAnnotations: map[string]string{},
wantAnnotations: nil,
wantStashed: false,
},
{
name: "namespace has all 3 SCC annotations, all stashed onto nil SA annotations",
saAnnotations: nil,
namespaceAnnotations: map[string]string{
apisecurity.UIDRangeAnnotation: "1000700000/10000",
apisecurity.SupplementalGroupsAnnotation: "1000700000/10000",
apisecurity.MCSAnnotation: "s0:c26,c5",
},
wantAnnotations: map[string]string{
"oadp.openshift.io/backup-ns-scc-uid-range": "1000700000/10000",
"oadp.openshift.io/backup-ns-scc-supplemental-groups": "1000700000/10000",
"oadp.openshift.io/backup-ns-scc-mcs": "s0:c26,c5",
},
wantStashed: true,
},
{
name: "existing SA annotations are preserved alongside stashed ones",
saAnnotations: map[string]string{
"kubernetes.io/service-account.name": "default",
},
namespaceAnnotations: map[string]string{
apisecurity.UIDRangeAnnotation: "1000700000/10000",
},
wantAnnotations: map[string]string{
"kubernetes.io/service-account.name": "default",
"oadp.openshift.io/backup-ns-scc-uid-range": "1000700000/10000",
},
wantStashed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAnnotations, gotStashed := stashNamespaceSCCAnnotations(tt.saAnnotations, tt.namespaceAnnotations)
if gotStashed != tt.wantStashed {
t.Errorf("stashNamespaceSCCAnnotations() stashed = %v, want %v", gotStashed, tt.wantStashed)
}
if !reflect.DeepEqual(gotAnnotations, tt.wantAnnotations) {
t.Errorf("stashNamespaceSCCAnnotations() annotations = %v, want %v", gotAnnotations, tt.wantAnnotations)
}
})
}
}
Loading