Skip to content
Merged
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
17 changes: 7 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ BINS = $(wildcard velero-*)

REPO ?= github.com/konveyor/openshift-velero-plugin

BUILD_IMAGE ?= openshift/origin-release:golang-1.14
BUILD_IMAGE ?= golang:1.25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does the official golang:1.25Docker image publish multi-architecture manifests, and whenGOOS/GOARCHare unset doesgo build default to the container's OS/architecture?

💡 Result:

The official golang:1.25 Docker image publishes multi-architecture manifests [1][2]. These manifests allow the Docker engine to automatically pull the appropriate image for the host's architecture (such as amd64, arm64v8, or others) [1][3]. When GOOS and GOARCH are unset, the go build command defaults to the OS and architecture of the environment where the command is executed [4][5]. In the context of a running Docker container, this corresponds to the OS and architecture of that container [3][5]. The Go toolchain determines these default values based on its own build configuration and the system's runtime environment [4][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la
if [ ! -f Makefile ]; then echo "Makefile not found"; exit 1; fi
nl -ba Makefile | sed -n '1,120p'

Repository: openshift/openshift-velero-plugin

Length of output: 1062


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Makefile: lines 1-120 with numbers (awk) ==="
awk '{printf "%d:%s\n", NR, $0}' Makefile | sed -n '1,120p'

echo "=== Makefile: relevant vars and recipe parts ==="
rg -n "BUILD_IMAGE|ARCH\s*\?=|DOCKER_BUILD_ARGS|docker run|go build" Makefile || true

Repository: openshift/openshift-velero-plugin

Length of output: 3840


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "_output/\$\(\s*BIN\s*\)|_output/" -S . || true
rg -n "ARCH\s*\?=|DOCKER_BUILD_ARGS|CLUSTER_OS|CLUSTER_ARCH|BUILD_IMAGE" -S Makefile Dockerfile velero-plugins -g'!*/vendor/*' || true

Repository: openshift/openshift-velero-plugin

Length of output: 1302


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Dockerfile ==="
nl -ba Dockerfile | sed -n '1,120p'
echo "=== konflux.Dockerfile ==="
nl -ba konflux.Dockerfile | sed -n '1,120p'

Repository: openshift/openshift-velero-plugin

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Dockerfile: numbered head ==="
awk '{printf "%d:%s\n", NR, $0}' Dockerfile | sed -n '1,140p'

echo "=== konflux.Dockerfile: numbered head ==="
awk '{printf "%d:%s\n", NR, $0}' konflux.Dockerfile | sed -n '1,220p'

Repository: openshift/openshift-velero-plugin

Length of output: 3171


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "make (build|docker|container)|_output/\$\(BIN\)|_output/velero-plugins|_output/" . -g'!.git/*' -S

Repository: openshift/openshift-velero-plugin

Length of output: 825


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "\bARCH\b|_output/\$\(\s*BIN\s*\)" . -S -g'!vendor/*' -g'!**/node_modules/*'

Repository: openshift/openshift-velero-plugin

Length of output: 105


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Makefile (full) ==="
cat Makefile

echo "=== README: lines around make build/container ==="
rg -n "\$ make (build|container)|make build|make container" README.md
sed -n '1,120p' README.md
sed -n '50,120p' README.md
sed -n '120,220p' README.md

Repository: openshift/openshift-velero-plugin

Length of output: 20451


Make make build honor $(ARCH) when using golang:1.25

golang:1.25 is multi-arch, and the docker run used for _output/$(BIN) doesn’t set --platform, GOOS, or GOARCH, so go build defaults to the container/host architecture; meanwhile ARCH ?= amd64 is unused in that recipe. This can produce _output/$(BIN) for an architecture different from what you intend.

Suggested fix
 BUILD_IMAGE ?= golang:1.25
@@
 	docker run \
+				 --platform=linux/$(ARCH) \
 				 --rm \
 				 -v $$(pwd):/workspace:z \
 				 -v $$(pwd)/.go/.cache:/go/.cache:z \
 				 -w /workspace \
+				 -e GOOS=linux \
+				 -e GOARCH=$(ARCH) \
 				 -e GOCACHE=/go/.cache \
 				 -e GOFLAGS="-mod=mod -buildvcs=false" \
 				 $(BUILD_IMAGE) \
 				 go build -tags $(BUILDTAGS) -v -o _output/$(BIN) ./$(BIN)
🤖 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 `@Makefile` at line 19, The build recipe producing _output/$(BIN) currently
ignores ARCH because docker run uses BUILD_IMAGE without specifying platform or
Go envs; update the Makefile build recipe so it passes the intended architecture
into the container: add --platform=linux/$(ARCH) to the docker run invocation
(or set GOOS/GOARCH env vars inside the container) and export GOARCH from ARCH
(e.g., set GOARCH=$(ARCH) via -e GOARCH=$(ARCH)) so go build inside the
container honors the requested architecture; ensure this change is applied to
the rule that references BUILD_IMAGE and creates _output/$(BIN).


IMAGE ?= docker.io/konveyor/openshift-velero-plugin

Expand All @@ -34,19 +34,16 @@ build-%:
build: _output/$(BIN)

_output/$(BIN): $(BIN)/*.go
mkdir -p .go/src/$(REPO) .go/pkg .go/.cache .go/std/$(ARCH) _output
cp -rp * .go/src/$(REPO)
mkdir -p .go/.cache _output
docker run \
--rm \
-v $$(pwd)/.go/pkg:/go/pkg:z \
-v $$(pwd)/.go/src:/go/src:z \
-v $$(pwd)/.go/std:/go/std:z \
-v $$(pwd):/workspace:z \
-v $$(pwd)/.go/.cache:/go/.cache:z \
-v $$(pwd)/_output:/go/src/$(REPO)/_output:z \
-v $$(pwd)/.go/std/$(ARCH):/usr/local/go/pkg/linux_$(ARCH)_static:z \
-w /go/src/$(REPO) \
-w /workspace \
-e GOCACHE=/go/.cache \
-e GOFLAGS="-mod=mod -buildvcs=false" \
$(BUILD_IMAGE) \
go build -installsuffix "static" -tags $(BUILDTAGS) -i -v -o _output/$(BIN) ./$(BIN)
go build -tags $(BUILDTAGS) -v -o _output/$(BIN) ./$(BIN)

DOCKER_BUILD_ARGS ?= --platform=linux/amd64
ifneq ($(CLUSTER_OS),)
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Velero currently supports the following kinds of plugins:
- Pod
- Replica Set
- Replication Controller
- Role Binding
- Role Binding (authorization.openshift.io)
- Role Binding (rbac.authorization.k8s.io)
- Route
- Security Context Constraints (SCC)
- Secret
Expand Down Expand Up @@ -545,16 +546,23 @@ time="2020-07-29T18:51:04Z" level=info msg="[pvc-restore] Returning pvc object a

### Role Binding

#### Restore Plugin
#### Restore Plugin (OpenShift authorization.openshift.io)

- **Resources**: rolebindings
- **Resources**: rolebinding.authorization.openshift.io
- **Actions**:
- Skips restore of system rolebindings ("system:image-pullers", "system:image-builders", "system:deployers") as these are automatically created by OpenShift
- Updates namespaces in subjects when namespace mapping is enabled
- Handles ServiceAccount subject namespace mapping
- Preserves role references while updating namespace contexts
- If restore namespace mapping is enabled, then the namespaces in RoleRef.Namespace, usernames, groupnames, and subjects are swapped accordingly

#### Restore Plugin (Kubernetes RBAC)

- **Resources**: rolebindings (rbac.authorization.k8s.io)
- **Actions**:
- Skips restore of the same system rolebindings ("system:image-pullers", "system:image-builders", "system:deployers") for the Kubernetes RBAC API group
- OpenShift stores rolebindings in rbac.authorization.k8s.io format and exposes them via both API groups; both must be skipped during restore to avoid conflicts with auto-created rolebindings

### Route

#### Restore Plugin
Expand Down
5 changes: 5 additions & 0 deletions velero-plugins/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func main() {
RegisterBackupItemAction("openshift.io/25-configmap-backup-plugin", newConfigMapBackupPlugin).
RegisterRestoreItemAction("openshift.io/25-configmap-restore-plugin", newConfigMapRestorePlugin).
RegisterRestoreItemAction("openshift.io/26-nonadmin-restore-plugin", newNonAdminRestorePlugin).
RegisterRestoreItemAction("openshift.io/27-rbac-role-bindings-restore-plugin", newRBACRoleBindingRestorePlugin).
Serve()
}

Expand Down Expand Up @@ -235,3 +236,7 @@ func newConfigMapRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
func newNonAdminRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
return &nonadmin.RestorePluginNonAdmin{Log: logger}, nil
}

func newRBACRoleBindingRestorePlugin(logger logrus.FieldLogger) (interface{}, error) {
return &rolebindings.K8sRestorePlugin{Log: logger}, nil
}
46 changes: 46 additions & 0 deletions velero-plugins/rolebindings/k8s_restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package rolebindings

import (
"encoding/json"

"github.com/sirupsen/logrus"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
rbacv1 "k8s.io/api/rbac/v1"
)

// K8sRestorePlugin is a restore item action plugin for k8s RBAC rolebindings
type K8sRestorePlugin struct {
Log logrus.FieldLogger
}

// AppliesTo returns a velero.ResourceSelector that applies to k8s RBAC rolebindings
func (p *K8sRestorePlugin) AppliesTo() (velero.ResourceSelector, error) {
return velero.ResourceSelector{
IncludedResources: []string{"rolebindings"},
}, nil
}

// Execute skips system rolebindings that OpenShift auto-creates for new namespaces
func (p *K8sRestorePlugin) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
p.Log.Info("[rbac-rolebinding-restore] Entering RBAC Role Bindings restore plugin")

roleBinding := rbacv1.RoleBinding{}
itemMarshal, err := json.Marshal(input.Item)
if err != nil {
return nil, err
}
if err = json.Unmarshal(itemMarshal, &roleBinding); err != nil {
return nil, err
}

p.Log.Infof("[rbac-rolebinding-restore] role binding - %s, API version %s", roleBinding.Name, roleBinding.APIVersion)

if SystemRoleBindings[roleBinding.Name] {
p.Log.Infof("[rbac-rolebinding-restore] Skipping system rolebinding %s as it will be automatically created", roleBinding.Name)
return &velero.RestoreItemActionExecuteOutput{
SkipRestore: true,
}, nil
}

return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
}
Comment on lines +24 to +46

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for namespace mapping patterns in rolebinding restore plugins

echo "=== Checking if authorization.openshift.io rolebindings are also backed up as rbac.authorization.k8s.io ==="
rg -n -C3 'authorization\.openshift\.io' velero-plugins/rolebindings/

echo -e "\n=== Checking NamespaceMapping usage in restore plugins ==="
rg -n 'NamespaceMapping' velero-plugins/ --type go -A3 -B3

echo -e "\n=== Checking AppliesTo resource selectors for rolebindings ==="
ast-grep --pattern $'func ($) AppliesTo() $_ {
  $$$
  IncludedResources: $$$
  $$$
}'

Repository: openshift/openshift-velero-plugin

Length of output: 15391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== rolebindings directory ==="
ls -la velero-plugins/rolebindings

echo -e "\n=== AppliesTo implementations in rolebindings plugins ==="
rg -n "func \\(p \\*.*\\) AppliesTo\\(" velero-plugins/rolebindings/*.go
rg -n "IncludedResources" velero-plugins/rolebindings/*.go

echo -e "\n=== NamespaceMapping usage in rolebindings plugins ==="
rg -n "NamespaceMapping" velero-plugins/rolebindings/*.go

echo -e "\n=== k8s_restore.go Execute() details around unmarshalling/skip logic ==="
sed -n '1,120p' velero-plugins/rolebindings/k8s_restore.go

echo -e "\n=== ocp_restore.go Execute() details around namespace mapping ==="
sed -n '1,160p' velero-plugins/rolebindings/ocp_restore.go

echo -e "\n=== k8s_restore_test.go (look for NamespaceMapping / subject mapping behavior) ==="
sed -n '1,220p' velero-plugins/rolebindings/k8s_restore_test.go

Repository: openshift/openshift-velero-plugin

Length of output: 10563


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== clusterrolebindings/restore.go (namespace mapping logic) ==="
sed -n '1,220p' velero-plugins/clusterrolebindings/restore.go

Repository: openshift/openshift-velero-plugin

Length of output: 2255


Add NamespaceMapping handling for Kubernetes rolebindings restore

  • velero-plugins/rolebindings/k8s_restore.go ignores input.Restore.Spec.NamespaceMapping and restores custom rbacv1.RoleBinding unchanged (only skips SystemRoleBindings by name), so subject namespace references can remain stale.
  • velero-plugins/rolebindings/ocp_restore.go and velero-plugins/clusterrolebindings/restore.go both apply NamespaceMapping by rewriting subject/identity namespace fields; k8s_restore.go should implement the equivalent namespace swapping for rbacv1.RoleBinding.Subjects (and any subject.Name serviceaccount-group namespace portions) when NamespaceMapping is set.
  • Skipping custom K8s rolebindings isn’t necessary: k8s_restore.go targets rolebindings, while ocp_restore.go targets rolebinding.authorization.openshift.io (different resource selectors).
🤖 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/rolebindings/k8s_restore.go` around lines 24 - 41, The Execute
method in K8sRestorePlugin currently ignores input.Restore.Spec.NamespaceMapping
and simply returns the RoleBinding unchanged (besides skipping
SystemRoleBindings), which can leave subject namespaces stale; update
K8sRestorePlugin.Execute to detect input.Restore.Spec.NamespaceMapping, iterate
over the deserialized rbacv1.RoleBinding.Subjects and rewrite any
Subject.Namespace (and serviceaccount-style Subject.Name portions like
"system:serviceaccount:<ns>:<name>" if present) using the mapping, then marshal
the modified roleBinding back into the output item returned by
velero.NewRestoreItemActionExecuteOutput; keep the existing SystemRoleBindings
name check but do not skip non-system rolebindings based solely on name.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not needed — Velero's built-in RoleBindingAction already handles Subject.Namespace remapping for k8s RBAC rolebindings. Multiple RestoreItemActions chain — our plugin runs first (skip system ones), then Velero's built-in runs on non-system ones and handles namespace mapping.

For system rolebindings, we skip them entirely (SkipRestore: true) so OpenShift creates fresh ones with correct subjects — no namespace remapping needed.

The embedded namespace in Subject.Name (e.g. system:serviceaccounts:<ns>) is an OpenShift-specific pattern that only appears in the system rolebindings we're skipping. Custom k8s RBAC rolebindings use Subject.Namespace field which Velero's built-in handles.

Note

Responses generated with Claude

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@kaovilai, thanks for the detailed explanation — that makes sense. Velero's built-in RoleBindingAction chaining after this plugin, handling Subject.Namespace remapping for non-system rolebindings, fully covers the namespace mapping concern. And since system rolebindings are skipped entirely, there's nothing to remap for them. I'll withdraw the comment.


✏️ Learnings added
Learnt from: kaovilai
Repo: openshift/openshift-velero-plugin PR: 402
File: velero-plugins/rolebindings/k8s_restore.go:24-46
Timestamp: 2026-06-08T16:56:41.949Z
Learning: In `openshift/openshift-velero-plugin`, the `K8sRestorePlugin` in `velero-plugins/rolebindings/k8s_restore.go` does NOT need to handle `NamespaceMapping` / `Subject.Namespace` remapping for k8s RBAC rolebindings (`rbac.authorization.k8s.io/v1/RoleBinding`). Velero's built-in `RoleBindingAction` (at `pkg/restore/actions/rolebinding_action.go`) already handles `Subject.Namespace` remapping. Multiple RestoreItemActions chain: the OpenShift plugin runs first and skips system rolebindings (`system:image-pullers`, `system:image-builders`, `system:deployers`) with `SkipRestore: true`, then Velero's built-in action runs on the remaining non-system ones and applies namespace mapping. System rolebindings are skipped entirely so namespace remapping is irrelevant for them. The `system:serviceaccounts:<ns>` embedded namespace pattern in `Subject.Name` is OpenShift-specific and only appears in the system rolebindings that are being skipped.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

101 changes: 101 additions & 0 deletions velero-plugins/rolebindings/k8s_restore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package rolebindings

import (
"testing"

"github.com/konveyor/openshift-velero-plugin/velero-plugins/util/test"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

func TestK8sRestorePluginAppliesTo(t *testing.T) {
restorePlugin := &K8sRestorePlugin{Log: test.NewLogger()}

expectedResources := []string{"rolebindings"}

selectedResources, err := restorePlugin.AppliesTo()
require.NoError(t, err)

assert.Equal(t, expectedResources, selectedResources.IncludedResources)
}

func TestK8sExecuteSystemRoleBindings(t *testing.T) {
restorePlugin := &K8sRestorePlugin{Log: logrus.New()}

tests := []struct {
name string
rbName string
shouldSkip bool
}{
{
name: "Skip system:image-pullers",
rbName: "system:image-pullers",
shouldSkip: true,
},
{
name: "Skip system:image-builders",
rbName: "system:image-builders",
shouldSkip: true,
},
{
name: "Skip system:deployers",
rbName: "system:deployers",
shouldSkip: true,
},
{
name: "Don't skip regular rolebinding",
rbName: "my-custom-rolebinding",
shouldSkip: false,
},
{
name: "Don't skip rolebinding with system: prefix but not in list",
rbName: "system:custom-role",
shouldSkip: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
roleBinding := rbacv1.RoleBinding{
TypeMeta: metav1.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "RoleBinding",
},
ObjectMeta: metav1.ObjectMeta{
Name: tt.rbName,
Namespace: "test-namespace",
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: "test-role",
},
}

unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&roleBinding)
require.NoError(t, err)

item := &unstructured.Unstructured{Object: unstructuredObj}

input := &velero.RestoreItemActionExecuteInput{
Item: item,
Restore: &velerov1.Restore{
Spec: velerov1.RestoreSpec{
NamespaceMapping: map[string]string{},
},
},
}

output, err := restorePlugin.Execute(input)
require.NoError(t, err)
assert.Equal(t, tt.shouldSkip, output.SkipRestore)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// systemRoleBindings contains rolebindings that are automatically created by OpenShift
// These rolebindings are expected to be created by the system and don't need restoring
// SystemRoleBindings contains rolebindings that are automatically created by OpenShift
// when a new project/namespace is created. These don't need restoring.
// Reference: https://github.com/openshift/openshift-apiserver/blob/eefb161cffdc97a949d6e9cc81aa900005912a97/pkg/project/apiserver/registry/projectrequest/delegated/delegated.go#L111
var systemRoleBindings = map[string]bool{
var SystemRoleBindings = map[string]bool{
"system:image-pullers": true,
"system:image-builders": true,
"system:deployers": true,
Expand Down Expand Up @@ -42,7 +42,7 @@ func (p *RestorePlugin) Execute(input *velero.RestoreItemActionExecuteInput) (*v

p.Log.Infof("[rolebinding-restore] role binding - %s, API version", roleBinding.Name, roleBinding.APIVersion)

if systemRoleBindings[roleBinding.Name] {
if SystemRoleBindings[roleBinding.Name] {
p.Log.Infof("[rolebinding-restore] Skipping system rolebinding %s as it will be automatically created", roleBinding.Name)
return &velero.RestoreItemActionExecuteOutput{
SkipRestore: true,
Expand Down