mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
fix: metric recorders for all conditionals (#1998)
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix: preserve ca-bundles injected from external providers Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
8e1cc910bd
commit
4b07463705
@@ -6,6 +6,7 @@ package e2e
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -182,6 +184,46 @@ var _ = Describe("GlobalTenantResource", Ordered, Label("replications", "global"
|
||||
})
|
||||
|
||||
Context("cluster-scoped objects", func() {
|
||||
const (
|
||||
controllerClusterResourceRoleName = "gtr-controller-cluster-resource-writer"
|
||||
controllerClusterResourceBindingName = "gtr-controller-cluster-resource-writer-binding"
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
bindServiceAccountToClusterResources(
|
||||
ControllerNamespace,
|
||||
ControllerServiceAccount,
|
||||
controllerClusterResourceRoleName,
|
||||
controllerClusterResourceBindingName,
|
||||
[]rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{rbacv1.GroupName},
|
||||
Resources: []string{"clusterroles"},
|
||||
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"configmaps"},
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
ignoreNotFound(k8sClient.Delete(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: controllerClusterResourceBindingName},
|
||||
}))
|
||||
ignoreNotFound(k8sClient.Delete(ctx, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: controllerClusterResourceRoleName},
|
||||
}))
|
||||
})
|
||||
|
||||
It("applies raw cluster-scoped items", func() {
|
||||
gtr := newRawClusterRoleGlobalTenantResource("gtr-cluster-raw", "gtr-cluster-raw-role")
|
||||
|
||||
@@ -209,6 +251,101 @@ var _ = Describe("GlobalTenantResource", Ordered, Label("replications", "global"
|
||||
}})
|
||||
expectGlobalTenantResourceProcessedClusterRole("gtr-cluster-generator", "gtr-cluster-generator-role")
|
||||
})
|
||||
|
||||
It("only allows the managing GlobalTenantResource service account to update created cluster-scoped objects", func() {
|
||||
saName := "gtr-cluster-update-guard"
|
||||
clusterRoleName := "gtr-cluster-admission-role"
|
||||
writerRoleName := "gtr-cluster-admission-writer"
|
||||
writerBindingName := "gtr-cluster-admission-writer-binding"
|
||||
|
||||
defer ignoreNotFound(k8sClient.Delete(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: writerBindingName},
|
||||
}))
|
||||
defer ignoreNotFound(k8sClient.Delete(ctx, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: writerRoleName},
|
||||
}))
|
||||
|
||||
ensureServiceAccount("capsule-system", saName)
|
||||
bindServiceAccountToClusterResources(
|
||||
"capsule-system",
|
||||
saName,
|
||||
writerRoleName,
|
||||
writerBindingName,
|
||||
[]rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{rbacv1.GroupName},
|
||||
Resources: []string{"clusterroles"},
|
||||
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"configmaps"},
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
gtr := newRawClusterRoleGlobalTenantResource("gtr-cluster-admission", clusterRoleName)
|
||||
gtr.Spec.ServiceAccount = &apimeta.NamespacedRFC1123ObjectReferenceWithNamespace{
|
||||
Name: apimeta.RFC1123Name(saName),
|
||||
Namespace: apimeta.RFC1123SubdomainName("capsule-system"),
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error { return k8sClient.Create(ctx, gtr) }).Should(Succeed())
|
||||
expectGlobalTenantResourceReady(gtr.Name)
|
||||
expectClusterRoleRules(clusterRoleName, []rbacv1.PolicyRule{{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"configmaps"},
|
||||
Verbs: []string{"get", "list"},
|
||||
}})
|
||||
expectGlobalTenantResourceProcessedClusterRole(gtr.Name, clusterRoleName)
|
||||
|
||||
Eventually(func() error {
|
||||
clusterRole := &rbacv1.ClusterRole{}
|
||||
if err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterRoleName}, clusterRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if clusterRole.Annotations == nil {
|
||||
clusterRole.Annotations = map[string]string{}
|
||||
}
|
||||
clusterRole.Annotations["e2e.capsule.dev/update-attempt"] = "admin"
|
||||
|
||||
err := k8sClient.Update(ctx, clusterRole)
|
||||
if err == nil {
|
||||
return fmt.Errorf("expected cluster role update to be denied")
|
||||
}
|
||||
if apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
if !apierrors.IsForbidden(err) {
|
||||
return fmt.Errorf("expected forbidden error, got: %w", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "managed by a global capsule replication") {
|
||||
return fmt.Errorf("expected global replication admission denial, got: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
replicationClient := impersonationClient(
|
||||
serviceAccountUsername("capsule-system", saName),
|
||||
serviceAccountGroups("capsule-system"),
|
||||
)
|
||||
Eventually(func() error {
|
||||
clusterRole := &rbacv1.ClusterRole{}
|
||||
if err := replicationClient.Get(ctx, types.NamespacedName{Name: clusterRoleName}, clusterRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if clusterRole.Annotations == nil {
|
||||
clusterRole.Annotations = map[string]string{}
|
||||
}
|
||||
clusterRole.Annotations["e2e.capsule.dev/updated-by"] = "manager"
|
||||
|
||||
return replicationClient.Update(ctx, clusterRole)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
It("skips applying resources to terminating namespaces and removes them from processedItems", func() {
|
||||
@@ -697,6 +834,75 @@ data:
|
||||
}
|
||||
})
|
||||
|
||||
It("only allows the managing GlobalTenantResource service account to update created objects", func() {
|
||||
saName := "gtr-update-guard"
|
||||
targetNamespace := tenantANamespaces[0]
|
||||
configMapName := "gtr-admission-protected"
|
||||
|
||||
ensureServiceAccount("capsule-system", saName)
|
||||
for _, ns := range tenantANamespaces {
|
||||
bindServiceAccountToConfigMapWriter("capsule-system", saName, ns)
|
||||
}
|
||||
|
||||
gtr := newRawConfigMapGlobalTenantResource("gtr-sa-update-guard", map[string]string{
|
||||
"mode": "managed",
|
||||
})
|
||||
gtr.Spec.ServiceAccount = &apimeta.NamespacedRFC1123ObjectReferenceWithNamespace{
|
||||
Name: apimeta.RFC1123Name(saName),
|
||||
Namespace: apimeta.RFC1123SubdomainName("capsule-system"),
|
||||
}
|
||||
gtr.Spec.TenantSelector = metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"energy": "solar"},
|
||||
}
|
||||
renameFirstRawConfigMap(gtr, configMapName)
|
||||
|
||||
EventuallyCreation(func() error { return k8sClient.Create(ctx, gtr) }).Should(Succeed())
|
||||
expectGlobalTenantResourceReady(gtr.Name)
|
||||
expectConfigMapData(targetNamespace, configMapName, map[string]string{"mode": "managed"})
|
||||
expectManagedLabelsOnConfigMap(targetNamespace, configMapName, true)
|
||||
|
||||
tenantOwnerClient := impersonationClient(tenantAOwner.Name, withDefaultGroups([]string{tenantAOwner.Name}))
|
||||
Eventually(func() error {
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := tenantOwnerClient.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: targetNamespace}, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.Data["mode"] = "tenant-owner"
|
||||
|
||||
err := tenantOwnerClient.Update(ctx, cm)
|
||||
if err == nil {
|
||||
return fmt.Errorf("expected tenant owner update to be denied")
|
||||
}
|
||||
if apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
if !apierrors.IsForbidden(err) {
|
||||
return fmt.Errorf("expected forbidden error, got: %w", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "managed by a global capsule replication") {
|
||||
return fmt.Errorf("expected global replication admission denial, got: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
replicationClient := impersonationClient(
|
||||
serviceAccountUsername("capsule-system", saName),
|
||||
serviceAccountGroups("capsule-system"),
|
||||
)
|
||||
Eventually(func() error {
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := replicationClient.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: targetNamespace}, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.Data["mode"] = "service-account"
|
||||
|
||||
return replicationClient.Update(ctx, cm)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("fails to prune replicated resources when the impersonated service account cannot delete them", func() {
|
||||
saCreate := "gtr-creator-ok"
|
||||
saNoDelete := "gtr-creator-no-delete"
|
||||
@@ -1479,6 +1685,7 @@ func expectClusterRoleRules(name string, expected []rbacv1.PolicyRule) {
|
||||
g.Expect(clusterRole.Namespace).To(BeEmpty())
|
||||
g.Expect(clusterRole.Labels).To(HaveKeyWithValue("e2e.capsule.dev/test-suite", "true"))
|
||||
g.Expect(clusterRole.Labels).To(HaveKeyWithValue(managedByLabel, meta.ValueControllerReplications))
|
||||
g.Expect(clusterRole.Labels).To(HaveKeyWithValue(createdByLabel, meta.ValueControllerReplications))
|
||||
g.Expect(clusterRole.Rules).To(ConsistOf(expected))
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
@@ -1495,3 +1702,85 @@ func expectGlobalTenantResourceProcessedClusterRole(gtrName, clusterRoleName str
|
||||
)))
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
|
||||
func bindServiceAccountToClusterResource(
|
||||
saNamespace, saName, clusterRoleName, clusterRoleBindingName string,
|
||||
resources, verbs []string,
|
||||
) {
|
||||
bindServiceAccountToClusterResources(
|
||||
saNamespace,
|
||||
saName,
|
||||
clusterRoleName,
|
||||
clusterRoleBindingName,
|
||||
[]rbacv1.PolicyRule{{
|
||||
APIGroups: []string{rbacv1.GroupName},
|
||||
Resources: resources,
|
||||
Verbs: verbs,
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func bindServiceAccountToClusterResources(
|
||||
saNamespace, saName, clusterRoleName, clusterRoleBindingName string,
|
||||
rules []rbacv1.PolicyRule,
|
||||
) {
|
||||
ctx := context.Background()
|
||||
|
||||
clusterRole := &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
Labels: map[string]string{
|
||||
"e2e.capsule.dev/test-suite": "true",
|
||||
},
|
||||
},
|
||||
Rules: rules,
|
||||
}
|
||||
|
||||
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleBindingName,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{{
|
||||
Kind: "ServiceAccount",
|
||||
Name: saName,
|
||||
Namespace: saNamespace,
|
||||
}},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
|
||||
Eventually(func() error {
|
||||
current := &rbacv1.ClusterRole{}
|
||||
err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterRoleName}, current)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return k8sClient.Create(ctx, clusterRole)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
current.Labels = clusterRole.Labels
|
||||
current.Rules = clusterRole.Rules
|
||||
|
||||
return k8sClient.Update(ctx, current)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
current := &rbacv1.ClusterRoleBinding{}
|
||||
err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterRoleBindingName}, current)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return k8sClient.Create(ctx, clusterRoleBinding)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
current.Subjects = clusterRoleBinding.Subjects
|
||||
current.RoleRef = clusterRoleBinding.RoleRef
|
||||
|
||||
return k8sClient.Update(ctx, current)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
|
||||
@@ -1417,6 +1417,79 @@ data:
|
||||
}
|
||||
})
|
||||
|
||||
It("only allows the managing TenantResource service account to update created objects", func() {
|
||||
saName := "tr-update-guard"
|
||||
targetNamespace := targetNamespaces[0]
|
||||
configMapName := "tr-admission-protected"
|
||||
|
||||
ensureServiceAccount(baseNamespace, saName)
|
||||
for _, ns := range append(targetNamespaces, baseNamespace) {
|
||||
bindServiceAccountToConfigMapWriter(baseNamespace, saName, ns)
|
||||
}
|
||||
|
||||
tr := newRawConfigMapTenantResource(baseNamespace, "sa-update-guard", map[string]string{
|
||||
"mode": "managed",
|
||||
})
|
||||
tr.Spec.ServiceAccount = &apimeta.LocalRFC1123ObjectReference{
|
||||
Name: apimeta.RFC1123Name(saName),
|
||||
}
|
||||
renameFirstTenantResourceRawConfigMap(tr, configMapName)
|
||||
|
||||
EventuallyCreation(func() error { return k8sClient.Create(ctx, tr) }).Should(Succeed())
|
||||
expectTenantResourceReady(baseNamespace, tr.Name)
|
||||
expectConfigMapData(targetNamespace, configMapName, map[string]string{"mode": "managed"})
|
||||
expectManagedLabelsOnConfigMap(targetNamespace, configMapName, true)
|
||||
expectProcessedItemStatus(
|
||||
baseNamespace,
|
||||
tr.Name,
|
||||
configMapRID(tnt.Name, targetNamespace, configMapName, "0/raw-0"),
|
||||
metav1.ConditionTrue,
|
||||
true,
|
||||
"",
|
||||
)
|
||||
|
||||
tenantOwnerClient := impersonationClient(tenantOwner.Name, withDefaultGroups([]string{tenantOwner.Name}))
|
||||
Eventually(func() error {
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := tenantOwnerClient.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: targetNamespace}, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.Data["mode"] = "tenant-owner"
|
||||
|
||||
err := tenantOwnerClient.Update(ctx, cm)
|
||||
if err == nil {
|
||||
return fmt.Errorf("expected tenant owner update to be denied")
|
||||
}
|
||||
if apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
if !apierrors.IsForbidden(err) {
|
||||
return fmt.Errorf("expected forbidden error, got: %w", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "managed by a tenant capsule replication") {
|
||||
return fmt.Errorf("expected tenant replication admission denial, got: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
replicationClient := impersonationClient(
|
||||
serviceAccountUsername(baseNamespace, saName),
|
||||
serviceAccountGroups(baseNamespace),
|
||||
)
|
||||
Eventually(func() error {
|
||||
cm := &corev1.ConfigMap{}
|
||||
if err := replicationClient.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: targetNamespace}, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm.Data["mode"] = "service-account"
|
||||
|
||||
return replicationClient.Update(ctx, cm)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("fails to prune replicated resources when the impersonated service account cannot delete them", func() {
|
||||
saCreate := "creator-ok"
|
||||
saNoDelete := "creator-no-delete"
|
||||
@@ -2109,3 +2182,15 @@ func ensureServiceAccount(namespace, name string) {
|
||||
return err
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
|
||||
func serviceAccountUsername(namespace, name string) string {
|
||||
return fmt.Sprintf("system:serviceaccount:%s:%s", namespace, name)
|
||||
}
|
||||
|
||||
func serviceAccountGroups(namespace string) []string {
|
||||
return []string{
|
||||
"system:authenticated",
|
||||
"system:serviceaccounts",
|
||||
fmt.Sprintf("system:serviceaccounts:%s", namespace),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user