feat: upstream enterprise preview (#1841)

feat: upstream enterprise preview

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
This commit is contained in:
Oliver Bähler
2026-05-28 00:58:58 +02:00
committed by GitHub
co-authored by CorentinPtrl
parent 7a65ab7afc
commit cc4fb45d70
462 changed files with 48456 additions and 10529 deletions
+19 -2
View File
@@ -4,7 +4,8 @@
package utils
import (
"github.com/pkg/errors"
gherrors "github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -13,5 +14,21 @@ import (
func IsUnsupportedAPI(err error) bool {
missingAPIError, discoveryGropuError, discoveryResourceError := &meta.NoKindMatchError{}, &discovery.ErrGroupDiscoveryFailed{}, &apiutil.ErrResourceDiscoveryFailed{}
return errors.As(err, &missingAPIError) || errors.As(err, &discoveryGropuError) || errors.As(err, &discoveryResourceError)
return gherrors.As(err, &missingAPIError) || gherrors.As(err, &discoveryGropuError) || gherrors.As(err, &discoveryResourceError)
}
func IgnoreWrappedNotFound(err error) error {
if err == nil {
return nil
}
if apierrors.IsNotFound(err) {
return nil
}
if apierrors.IsNotFound(gherrors.Cause(err)) {
return nil
}
return err
}
+2 -2
View File
@@ -7,10 +7,10 @@ import (
"fmt"
"hash/fnv"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rbac"
)
func RoleBindingHashFunc(binding api.AdditionalRoleBindingsSpec) string {
func RoleBindingHashFunc(binding rbac.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(binding.ClusterRoleName))
+11 -11
View File
@@ -8,12 +8,12 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/utils"
)
func TestRoleBindingHashFunc_Deterministic(t *testing.T) {
b := api.AdditionalRoleBindingsSpec{
b := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{
{Kind: "User", Name: "alice"},
@@ -33,11 +33,11 @@ func TestRoleBindingHashFunc_Deterministic(t *testing.T) {
}
func TestRoleBindingHashFunc_ChangesWhenClusterRoleChanges(t *testing.T) {
b1 := api.AdditionalRoleBindingsSpec{
b1 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
}
b2 := api.AdditionalRoleBindingsSpec{
b2 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "view",
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
}
@@ -51,11 +51,11 @@ func TestRoleBindingHashFunc_ChangesWhenClusterRoleChanges(t *testing.T) {
}
func TestRoleBindingHashFunc_ChangesWhenSubjectKindChanges(t *testing.T) {
b1 := api.AdditionalRoleBindingsSpec{
b1 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
}
b2 := api.AdditionalRoleBindingsSpec{
b2 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{{Kind: "Group", Name: "alice"}},
}
@@ -69,11 +69,11 @@ func TestRoleBindingHashFunc_ChangesWhenSubjectKindChanges(t *testing.T) {
}
func TestRoleBindingHashFunc_ChangesWhenSubjectNameChanges(t *testing.T) {
b1 := api.AdditionalRoleBindingsSpec{
b1 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
}
b2 := api.AdditionalRoleBindingsSpec{
b2 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{{Kind: "User", Name: "bob"}},
}
@@ -87,7 +87,7 @@ func TestRoleBindingHashFunc_ChangesWhenSubjectNameChanges(t *testing.T) {
}
func TestRoleBindingHashFunc_EmptyInputsStillProduceHash(t *testing.T) {
b := api.AdditionalRoleBindingsSpec{
b := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "",
Subjects: nil,
}
@@ -101,14 +101,14 @@ func TestRoleBindingHashFunc_EmptyInputsStillProduceHash(t *testing.T) {
func TestRoleBindingHashFunc_SubjectOrderMatters_CurrentBehavior(t *testing.T) {
// This test documents the CURRENT behavior:
// the hash is order-dependent because subjects are written in slice order.
b1 := api.AdditionalRoleBindingsSpec{
b1 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{
{Kind: "User", Name: "alice"},
{Kind: "Group", Name: "devops"},
},
}
b2 := api.AdditionalRoleBindingsSpec{
b2 := rbac.AdditionalRoleBindingsSpec{
ClusterRoleName: "admin",
Subjects: []rbacv1.Subject{
{Kind: "Group", Name: "devops"},
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package utils
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/client-go/discovery"
)
var versionsWithNodeFix = []string{"v1.18.18", "v1.19.10", "v1.20.6", "v1.21.0"}
func NodeWebhookSupported(currentVersion *version.Version) (bool, error) {
versions := make([]*version.Version, 0, len(versionsWithNodeFix))
for _, v := range versionsWithNodeFix {
ver, err := version.ParseGeneric(v)
if err != nil {
return false, err
}
versions = append(versions, ver)
}
for _, v := range versions {
if currentVersion.Major() == v.Major() {
if currentVersion.Minor() < v.Minor() {
return false, nil
}
if currentVersion.Minor() == v.Minor() && currentVersion.Patch() < v.Patch() {
return false, nil
}
}
}
return true, nil
}
func GetK8sVersionFromConfig(dc discovery.DiscoveryInterface) (*version.Version, error) {
sv, err := dc.ServerVersion()
if err != nil {
return nil, err
}
return version.ParseGeneric(sv.String())
}
+79
View File
@@ -3,6 +3,13 @@
package utils
import (
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/runtime"
)
func MapMergeNoOverrite(dst, src map[string]string) {
if len(src) == 0 {
return
@@ -28,3 +35,75 @@ func MapEqual(a, b map[string]string) bool {
return true
}
func ToUnstructuredMap(obj any) (map[string]any, error) {
m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return m, nil
}
func Mapify(data any) map[string]any {
result := make(map[string]any)
v := reflect.ValueOf(data)
// If the provided data is a pointer, resolve to the underlying value
if v.Kind() == reflect.Pointer {
if v.IsNil() {
return result // Return empty map for nil pointers
}
v = v.Elem()
}
// Ensure we're working with a struct
if v.Kind() == reflect.Struct {
for i := range v.NumField() {
field := v.Type().Field(i)
// Skip unexported fields
if field.PkgPath != "" {
continue
}
value := v.Field(i)
// Handle different types with recursive or base handling
//nolint:exhaustive
switch value.Kind() {
case reflect.Pointer:
if !value.IsNil() {
result[field.Name] = Mapify(value.Interface())
}
case reflect.Struct:
result[field.Name] = Mapify(value.Interface())
case reflect.Slice:
var slice []any
for j := range value.Len() {
item := value.Index(j)
if item.Kind() == reflect.Struct {
slice = append(slice, Mapify(item.Interface()))
} else {
slice = append(slice, item.Interface())
}
}
result[field.Name] = slice
case reflect.Map:
mapResult := make(map[string]any)
for _, key := range value.MapKeys() {
mapResult[fmt.Sprint(key)] = value.MapIndex(key).Interface()
}
result[field.Name] = mapResult
default:
result[field.Name] = value.Interface()
}
}
}
return result
}