fix: skip namespace check on cluster scoped rbac resources for auth reconcile (#26403)

Signed-off-by: Christopher Coco <ccoco@redhat.com>
This commit is contained in:
Christopher Coco
2026-02-18 10:55:54 -05:00
committed by GitHub
parent 650fa6a10e
commit 043544c197
3 changed files with 141 additions and 1 deletions

View File

@@ -603,11 +603,24 @@ func (k *kubectlResourceOperations) authReconcile(ctx context.Context, obj *unst
if err != nil {
return "", fmt.Errorf("error creating kube client: %w", err)
}
clusterScoped := obj.GetKind() == "ClusterRole" || obj.GetKind() == "ClusterRoleBinding"
// `kubectl auth reconcile` has a side effect of auto-creating namespaces if it doesn't exist.
// See: https://github.com/kubernetes/kubernetes/issues/71185. This is behavior which we do
// not want. We need to check if the namespace exists, before know if it is safe to run this
// command. Skip this for dryRuns.
if dryRunStrategy == cmdutil.DryRunNone && obj.GetNamespace() != "" {
// When an Argo CD Application specifies destination.namespace, that namespace
// may be propagated even for cluster-scoped resources. Passing a namespace in
// this case causes `kubectl auth reconcile` to fail with:
// "namespaces <ns> not found"
// or may trigger unintended namespace handling behavior.
// Therefore, we skip namespace existence checks for cluster-scoped RBAC
// resources and allow reconcile to run without a namespace.
//
// https://github.com/argoproj/argo-cd/issues/24833
if dryRunStrategy == cmdutil.DryRunNone && obj.GetNamespace() != "" && !clusterScoped {
_, err = kubeClient.CoreV1().Namespaces().Get(ctx, obj.GetNamespace(), metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("error getting namespace %s: %w", obj.GetNamespace(), err)

View File

@@ -0,0 +1,75 @@
package kube
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
testingutils "github.com/argoproj/argo-cd/gitops-engine/pkg/utils/testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func TestAuthReconcileWithMissingNamespace(t *testing.T) {
namespace := "test-ns"
fakeBearer := "fake-bearer"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
status := &metav1.Status{
Status: "Failure",
Message: fmt.Sprintf("namespace \"%s\" not found", namespace),
Reason: metav1.StatusReasonNotFound,
Code: http.StatusNotFound,
}
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(status)
}))
defer server.Close()
kubeConfigFlags := genericclioptions.NewConfigFlags(true)
kubeConfigFlags.Namespace = &namespace
kubeConfigFlags.APIServer = &server.URL
kubeConfigFlags.BearerToken = &fakeBearer
matchFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
fact := cmdutil.NewFactory(matchFlags)
config := &rest.Config{Host: server.URL}
k := &kubectlResourceOperations{
config: config,
fact: fact,
}
role := testingutils.NewRole()
role.SetNamespace(namespace)
_, err := k.authReconcile(context.Background(), role, "/dev/null", cmdutil.DryRunNone)
assert.Error(t, err)
assert.True(t, errors.IsNotFound(err), "returned error wasn't not found")
roleBinding := testingutils.NewRoleBinding()
roleBinding.SetNamespace(namespace)
_, err = k.authReconcile(context.Background(), roleBinding, "/dev/null", cmdutil.DryRunNone)
assert.Error(t, err)
assert.True(t, errors.IsNotFound(err), "returned error wasn't not found")
clusterRole := testingutils.NewClusterRole()
clusterRole.SetNamespace(namespace)
_, err = k.authReconcile(context.Background(), clusterRole, "/dev/null", cmdutil.DryRunNone)
assert.NoError(t, err)
clusterRoleBinding := testingutils.NewClusterRoleBinding()
clusterRoleBinding.SetNamespace(namespace)
_, err = k.authReconcile(context.Background(), clusterRoleBinding, "/dev/null", cmdutil.DryRunNone)
assert.NoError(t, err)
}

View File

@@ -97,3 +97,55 @@ metadata:
name: testnamespace
spec:`)
}
func NewRole() *unstructured.Unstructured {
return Unstructured(`apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]`)
}
func NewRoleBinding() *unstructured.Unstructured {
return Unstructured(`apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-role-binding
subjects:
- kind: User
name: user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: my-role
apiGroup: rbac.authorization.k8s.io`)
}
func NewClusterRole() *unstructured.Unstructured {
return Unstructured(`apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-cluster-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]`)
}
func NewClusterRoleBinding() *unstructured.Unstructured {
return Unstructured(`apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-cluster-role-binding
subjects:
- kind: Group
name: group
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: my-cluster-role
apiGroup: rbac.authorization.k8s.io`)
}