-
Notifications
You must be signed in to change notification settings - Fork 45
Detect SCC UID/GID-range mismatch on namespace restore #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: oadp-dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
|
||
| 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{}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -SRepository: openshift/openshift-velero-plugin Length of output: 14037 Bound the namespace lookups with a timeout.
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: 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 | ||
| } | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
Repository: openshift/openshift-velero-plugin
Length of output: 14216
🏁 Script executed:
Repository: openshift/openshift-velero-plugin
Length of output: 1786
🏁 Script executed:
Repository: openshift/openshift-velero-plugin
Length of output: 8537
🏁 Script executed:
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 nilObjectwhile still reporting success. Return the error (or useruntime.DefaultUnstructuredConverter) at:velero-plugins/namespacescc/backup.go#L59-L60,#L73-L75velero-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-L75velero-plugins/namespacescc/restore.go#L51-L52velero-plugins/namespacescc/restore.go#L56-L58velero-plugins/namespacescc/restore.go#L78-L80🤖 Prompt for AI Agents
Source: Path instructions