mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 09:59:57 +00:00
feat: add dynamic capsule user evaluation (#1811)
* chore: improve dev targets Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(controller): implement deterministic rolebinding reflection Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(controller): capsule users are determined from configuration status Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(tenantowners): added agreggate option - tenantowners are always considered capsule users Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(tenantowner): add implicit aggregation for tenants Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: remove helm flags Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix(config): remove usergroups default Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
@@ -63,9 +63,8 @@ So the TL;DR answer is:
|
|||||||
**Make sure a *KinD* cluster is running on your laptop, and then run `make dev-setup` to setup the dev environment.**. This is not done in the `make dev-setup` setup.
|
**Make sure a *KinD* cluster is running on your laptop, and then run `make dev-setup` to setup the dev environment.**. This is not done in the `make dev-setup` setup.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If you haven't installed or run `make deploy` before, do it first
|
# Create a KinD cluster if not already created
|
||||||
# Note: please retry if you saw errors
|
$ make dev-cluster
|
||||||
$ make deploy
|
|
||||||
|
|
||||||
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
|
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
|
||||||
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup
|
||||||
|
|||||||
49
Makefile
49
Makefile
@@ -105,6 +105,20 @@ helm-test-exec: ct helm-controller-version ko-build-all
|
|||||||
@$(CT) install --config $(SRC_ROOT)/.github/configs/ct.yaml --namespace=capsule-system --all --debug
|
@$(CT) install --config $(SRC_ROOT)/.github/configs/ct.yaml --namespace=capsule-system --all --debug
|
||||||
|
|
||||||
# Setup development env
|
# Setup development env
|
||||||
|
dev-build: kind
|
||||||
|
$(KIND) create cluster --wait=60s --name $(CLUSTER_NAME) --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION)
|
||||||
|
$(MAKE) dev-install-deps
|
||||||
|
|
||||||
|
.PHONY: dev-destroy
|
||||||
|
dev-destroy: kind
|
||||||
|
$(KIND) delete cluster --name capsule
|
||||||
|
|
||||||
|
API_GW := none
|
||||||
|
API_GW_VERSION := v1.3.0
|
||||||
|
API_GW_LOOKUP := kubernetes-sigs/gateway-api
|
||||||
|
dev-install-deps:
|
||||||
|
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/$(API_GW_LOOKUP)/releases/download/$(API_GW_VERSION)/standard-install.yaml
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
|
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
|
||||||
# For example:
|
# For example:
|
||||||
@@ -127,6 +141,7 @@ IP.1 = $(LAPTOP_HOST_IP)
|
|||||||
endef
|
endef
|
||||||
export TLS_CNF
|
export TLS_CNF
|
||||||
dev-setup:
|
dev-setup:
|
||||||
|
$(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true
|
||||||
mkdir -p /tmp/k8s-webhook-server/serving-certs
|
mkdir -p /tmp/k8s-webhook-server/serving-certs
|
||||||
echo "$${TLS_CNF}" > _tls.cnf
|
echo "$${TLS_CNF}" > _tls.cnf
|
||||||
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
|
||||||
@@ -156,8 +171,7 @@ dev-setup:
|
|||||||
--set "webhooks.service.url=$${WEBHOOK_URL}" \
|
--set "webhooks.service.url=$${WEBHOOK_URL}" \
|
||||||
--set "webhooks.service.caBundle=$${CA_BUNDLE}" \
|
--set "webhooks.service.caBundle=$${CA_BUNDLE}" \
|
||||||
capsule \
|
capsule \
|
||||||
./charts/capsule
|
./charts/capsule || true
|
||||||
$(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true
|
|
||||||
|
|
||||||
setup-monitoring: dev-setup-fluxcd
|
setup-monitoring: dev-setup-fluxcd
|
||||||
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring | envsubst | kubectl apply -f -
|
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring | envsubst | kubectl apply -f -
|
||||||
@@ -180,6 +194,22 @@ dev-setup-argocd: dev-setup-fluxcd
|
|||||||
dev-setup-fluxcd:
|
dev-setup-fluxcd:
|
||||||
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f -
|
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f -
|
||||||
|
|
||||||
|
# Here to setup the current capsule version
|
||||||
|
# Intended to test updates to new version
|
||||||
|
dev-setup-capsule: dev-setup-fluxcd
|
||||||
|
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/capsule | envsubst | kubectl apply -f -
|
||||||
|
@$(MAKE) wait-for-helmreleases
|
||||||
|
@$(MAKE) dev-setup-capsule-example
|
||||||
|
|
||||||
|
dev-setup-capsule-example: dev-setup-fluxcd
|
||||||
|
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/capsule/example-setup | envsubst | kubectl apply -f -
|
||||||
|
@$(KUBECTL) create ns wind-test --as joe --as-group projectcapsule.dev
|
||||||
|
@$(KUBECTL) create ns wind-prod --as joe --as-group projectcapsule.dev
|
||||||
|
@$(KUBECTL) create ns green-test --as bob --as-group projectcapsule.dev
|
||||||
|
@$(KUBECTL) create ns green-prod --as bob --as-group projectcapsule.dev
|
||||||
|
@$(KUBECTL) create ns solar-test --as alice --as-group projectcapsule.dev
|
||||||
|
@$(KUBECTL) create ns solar-prod --as alice --as-group projectcapsule.dev
|
||||||
|
|
||||||
wait-for-helmreleases:
|
wait-for-helmreleases:
|
||||||
@ echo "Waiting for all HelmReleases to have observedGeneration >= 0..."
|
@ echo "Waiting for all HelmReleases to have observedGeneration >= 0..."
|
||||||
@while [ "$$($(KUBECTL) get helmrelease -A -o jsonpath='{range .items[?(@.status.observedGeneration<0)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | wc -l)" -ne 0 ]; do \
|
@while [ "$$($(KUBECTL) get helmrelease -A -o jsonpath='{range .items[?(@.status.observedGeneration<0)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | wc -l)" -ne 0 ]; do \
|
||||||
@@ -264,20 +294,10 @@ golint-fix: golangci-lint
|
|||||||
e2e: ginkgo
|
e2e: ginkgo
|
||||||
$(MAKE) e2e-build && $(MAKE) e2e-exec && $(MAKE) e2e-destroy
|
$(MAKE) e2e-build && $(MAKE) e2e-exec && $(MAKE) e2e-destroy
|
||||||
|
|
||||||
API_GW := none
|
|
||||||
API_GW_VERSION := v1.3.0
|
|
||||||
API_GW_LOOKUP := kubernetes-sigs/gateway-api/
|
|
||||||
e2e-install-deps:
|
|
||||||
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/$(API_GW_LOOKUP)/releases/download/$(API_GW_VERSION)/standard-install.yaml
|
|
||||||
|
|
||||||
e2e-build: kind
|
e2e-build: kind
|
||||||
$(MAKE) e2e-build-cluster
|
$(MAKE) dev-build
|
||||||
$(MAKE) e2e-install
|
$(MAKE) e2e-install
|
||||||
|
|
||||||
e2e-build-cluster: kind
|
|
||||||
$(KIND) create cluster --wait=60s --name $(CLUSTER_NAME) --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION)
|
|
||||||
$(MAKE) e2e-install-deps
|
|
||||||
|
|
||||||
.PHONY: e2e-install
|
.PHONY: e2e-install
|
||||||
e2e-install: ko-build-all
|
e2e-install: ko-build-all
|
||||||
$(MAKE) e2e-load-image CLUSTER_NAME=$(CLUSTER_NAME) IMAGE=$(CAPSULE_IMG) VERSION=$(VERSION)
|
$(MAKE) e2e-load-image CLUSTER_NAME=$(CLUSTER_NAME) IMAGE=$(CAPSULE_IMG) VERSION=$(VERSION)
|
||||||
@@ -339,8 +359,7 @@ e2e-exec: ginkgo
|
|||||||
$(GINKGO) -v -tags e2e ./e2e
|
$(GINKGO) -v -tags e2e ./e2e
|
||||||
|
|
||||||
.PHONY: e2e-destroy
|
.PHONY: e2e-destroy
|
||||||
e2e-destroy: kind
|
e2e-destroy: dev-destroy
|
||||||
$(KIND) delete cluster --name capsule
|
|
||||||
|
|
||||||
SPELL_CHECKER = npx spellchecker-cli
|
SPELL_CHECKER = npx spellchecker-cli
|
||||||
docs-lint:
|
docs-lint:
|
||||||
|
|||||||
14
api/v1beta2/capsuleconfiguration_status.go
Normal file
14
api/v1beta2/capsuleconfiguration_status.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2020-2025 Project Capsule Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package v1beta2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CapsuleConfigurationStatus defines the Capsule configuration status.
|
||||||
|
type CapsuleConfigurationStatus struct {
|
||||||
|
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
|
||||||
|
Users api.UserListSpec `json:"users,omitempty"`
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ type CapsuleConfigurationSpec struct {
|
|||||||
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||||
//
|
//
|
||||||
// Names of the groups considered as Capsule users.
|
// Names of the groups considered as Capsule users.
|
||||||
// +kubebuilder:default={capsule.clastix.io}
|
|
||||||
UserGroups []string `json:"userGroups,omitempty"`
|
UserGroups []string `json:"userGroups,omitempty"`
|
||||||
// Define groups which when found in the request of a user will be ignored by the Capsule
|
// Define groups which when found in the request of a user will be ignored by the Capsule
|
||||||
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||||
@@ -79,6 +78,7 @@ type CapsuleResources struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
// +kubebuilder:resource:scope=Cluster
|
// +kubebuilder:resource:scope=Cluster
|
||||||
// +kubebuilder:storageversion
|
// +kubebuilder:storageversion
|
||||||
|
|
||||||
@@ -90,6 +90,9 @@ type CapsuleConfiguration struct {
|
|||||||
metav1.ObjectMeta `json:"metadata,omitzero"`
|
metav1.ObjectMeta `json:"metadata,omitzero"`
|
||||||
|
|
||||||
Spec CapsuleConfigurationSpec `json:"spec"`
|
Spec CapsuleConfigurationSpec `json:"spec"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Status CapsuleConfigurationStatus `json:"status,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (in *Tenant) CollectOwners(ctx context.Context, c client.Client, allowPromo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dedicated Owner Objects
|
// Dedicated Owner Objects
|
||||||
listed, err := in.Spec.Permissions.ListMatchingOwners(ctx, c)
|
listed, err := in.Spec.Permissions.ListMatchingOwners(ctx, c, in.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,18 @@ func (in *Tenant) CollectOwners(ctx context.Context, c client.Client, allowPromo
|
|||||||
return owners, nil
|
return owners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (in *Tenant) GetRoleBindings() []api.AdditionalRoleBindingsSpec {
|
||||||
|
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0)
|
||||||
|
|
||||||
|
for _, owner := range in.Status.Owners {
|
||||||
|
roleBindings = append(roleBindings, owner.ToAdditionalRolebindings()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleBindings = append(roleBindings, in.Spec.AdditionalRoleBindings...)
|
||||||
|
|
||||||
|
return roleBindings
|
||||||
|
}
|
||||||
|
|
||||||
func (in *Tenant) IsFull() bool {
|
func (in *Tenant) IsFull() bool {
|
||||||
// we don't have limits on assigned Namespaces
|
// we don't have limits on assigned Namespaces
|
||||||
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {
|
if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||||
"github.com/projectcapsule/capsule/pkg/api/misc"
|
"github.com/projectcapsule/capsule/pkg/api/misc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,9 +99,16 @@ type Permissions struct {
|
|||||||
func (p *Permissions) ListMatchingOwners(
|
func (p *Permissions) ListMatchingOwners(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
c client.Client,
|
c client.Client,
|
||||||
|
tnt string,
|
||||||
opts ...client.ListOption,
|
opts ...client.ListOption,
|
||||||
) ([]*TenantOwner, error) {
|
) ([]*TenantOwner, error) {
|
||||||
return misc.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, p.MatchOwners)
|
defaultSelector := &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
meta.NewTenantLabel: tnt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return misc.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, append(p.MatchOwners, defaultSelector))
|
||||||
}
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import (
|
|||||||
|
|
||||||
// TenantOwnerSpec defines the desired state of TenantOwner.
|
// TenantOwnerSpec defines the desired state of TenantOwner.
|
||||||
type TenantOwnerSpec struct {
|
type TenantOwnerSpec struct {
|
||||||
|
// Subject
|
||||||
api.CoreOwnerSpec `json:",inline"`
|
api.CoreOwnerSpec `json:",inline"`
|
||||||
|
|
||||||
|
// Adds the given subject as capsule user. When enabled this subject does not have to be
|
||||||
|
// mentioned in the CapsuleConfiguration as Capsule User. In almost all scenarios Tenant Owners
|
||||||
|
// must be Capsule Users.
|
||||||
|
//+kubebuilder:default:=true
|
||||||
|
Aggregate bool `json:"aggregate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TenantOwnerStatus defines the observed state of TenantOwner.
|
// TenantOwnerStatus defines the observed state of TenantOwner.
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func (in *CapsuleConfiguration) DeepCopyInto(out *CapsuleConfiguration) {
|
|||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
in.Spec.DeepCopyInto(&out.Spec)
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfiguration.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfiguration.
|
||||||
@@ -141,6 +142,26 @@ func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *CapsuleConfigurationStatus) DeepCopyInto(out *CapsuleConfigurationStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Users != nil {
|
||||||
|
in, out := &in.Users, &out.Users
|
||||||
|
*out = make(api.UserListSpec, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfigurationStatus.
|
||||||
|
func (in *CapsuleConfigurationStatus) DeepCopy() *CapsuleConfigurationStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(CapsuleConfigurationStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *CapsuleResources) DeepCopyInto(out *CapsuleResources) {
|
func (in *CapsuleResources) DeepCopyInto(out *CapsuleResources) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|||||||
@@ -153,8 +153,6 @@ spec:
|
|||||||
regexp
|
regexp
|
||||||
type: string
|
type: string
|
||||||
userGroups:
|
userGroups:
|
||||||
default:
|
|
||||||
- capsule.clastix.io
|
|
||||||
description: |-
|
description: |-
|
||||||
Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
|
||||||
|
|
||||||
@@ -195,8 +193,36 @@ spec:
|
|||||||
required:
|
required:
|
||||||
- enableTLSReconciler
|
- enableTLSReconciler
|
||||||
type: object
|
type: object
|
||||||
|
status:
|
||||||
|
description: CapsuleConfigurationStatus defines the Capsule configuration
|
||||||
|
status.
|
||||||
|
properties:
|
||||||
|
users:
|
||||||
|
description: Users which are considered Capsule Users and are bound
|
||||||
|
to the Capsule Tenant construct.
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
kind:
|
||||||
|
description: Kind of entity. Possible values are "User", "Group",
|
||||||
|
and "ServiceAccount"
|
||||||
|
enum:
|
||||||
|
- User
|
||||||
|
- Group
|
||||||
|
- ServiceAccount
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: Name of the entity.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- kind
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- spec
|
- spec
|
||||||
type: object
|
type: object
|
||||||
served: true
|
served: true
|
||||||
storage: true
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
description: spec defines the desired state of TenantOwner.
|
description: spec defines the desired state of TenantOwner.
|
||||||
properties:
|
properties:
|
||||||
|
aggregate:
|
||||||
|
default: true
|
||||||
|
description: |-
|
||||||
|
Adds the given subject as capsule user. When enabled this subject does not have to be
|
||||||
|
mentioned in the CapsuleConfiguration as Capsule User. In almost all scenarios Tenant Owners
|
||||||
|
must be Capsule Users.
|
||||||
|
type: boolean
|
||||||
clusterRoles:
|
clusterRoles:
|
||||||
default:
|
default:
|
||||||
- admin
|
- admin
|
||||||
@@ -59,6 +66,7 @@ spec:
|
|||||||
description: Name of the entity.
|
description: Name of the entity.
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- aggregate
|
||||||
- kind
|
- kind
|
||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -374,7 +374,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = (&configcontroller.Manager{
|
if err = (&configcontroller.Manager{
|
||||||
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
|
Client: manager.GetClient(),
|
||||||
|
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
|
||||||
}).SetupWithManager(manager, controllerConfig); err != nil {
|
}).SetupWithManager(manager, controllerConfig); err != nil {
|
||||||
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
|
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
@@ -58,20 +58,10 @@ var _ = Describe("creating a Namespace with an additional Role Binding", Label("
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("should be assigned to each Namespace", func() {
|
It("should be assigned to each Namespace", func() {
|
||||||
for _, ns := range []string{"rb-1", "rb-2", "rb-3"} {
|
|
||||||
ns := NewNamespace(ns)
|
|
||||||
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
||||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
|
||||||
|
|
||||||
var rb *rbacv1.RoleBinding
|
t := &capsulev1beta2.Tenant{}
|
||||||
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||||
|
|
||||||
Eventually(func() (err error) {
|
VerifyTenantRoleBindings(t)
|
||||||
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
|
|
||||||
rb, err = cs.RbacV1().RoleBindings(ns.Name).Get(context.Background(), fmt.Sprintf("capsule-%s-2-%s", tnt.Name, "crds-rolebinding"), metav1.GetOptions{})
|
|
||||||
return err
|
|
||||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
||||||
Expect(rb.RoleRef.Name).Should(Equal(tnt.Spec.AdditionalRoleBindings[0].ClusterRoleName))
|
|
||||||
Expect(rb.Subjects).Should(Equal(tnt.Spec.AdditionalRoleBindings[0].Subjects))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
||||||
@@ -123,6 +124,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: capsulev1beta2.TenantOwnerSpec{
|
Spec: capsulev1beta2.TenantOwnerSpec{
|
||||||
|
Aggregate: true,
|
||||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||||
UserSpec: api.UserSpec{
|
UserSpec: api.UserSpec{
|
||||||
Kind: api.GroupOwner,
|
Kind: api.GroupOwner,
|
||||||
@@ -143,6 +145,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: capsulev1beta2.TenantOwnerSpec{
|
Spec: capsulev1beta2.TenantOwnerSpec{
|
||||||
|
Aggregate: true,
|
||||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||||
UserSpec: api.UserSpec{
|
UserSpec: api.UserSpec{
|
||||||
Kind: api.GroupOwner,
|
Kind: api.GroupOwner,
|
||||||
@@ -164,6 +167,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: capsulev1beta2.TenantOwnerSpec{
|
Spec: capsulev1beta2.TenantOwnerSpec{
|
||||||
|
Aggregate: true,
|
||||||
CoreOwnerSpec: api.CoreOwnerSpec{
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||||
UserSpec: api.UserSpec{
|
UserSpec: api.UserSpec{
|
||||||
Kind: api.ServiceAccountOwner,
|
Kind: api.ServiceAccountOwner,
|
||||||
@@ -176,6 +180,49 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tnt1Owner := &capsulev1beta2.TenantOwner{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "e2e-owners-tnt",
|
||||||
|
Labels: map[string]string{
|
||||||
|
meta.NewTenantLabel: tnt1.GetName(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: capsulev1beta2.TenantOwnerSpec{
|
||||||
|
Aggregate: true,
|
||||||
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "tnt-1-user",
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{
|
||||||
|
"service-admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userOwnersCommon := &capsulev1beta2.TenantOwner{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "e2e-owners-common-user",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"team": "infrastructure",
|
||||||
|
"customer": "x",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: capsulev1beta2.TenantOwnerSpec{
|
||||||
|
Aggregate: false,
|
||||||
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "some-user",
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{
|
||||||
|
"service-admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
JustBeforeEach(func() {
|
JustBeforeEach(func() {
|
||||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||||
|
|
||||||
@@ -187,7 +234,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
}).Should(Succeed())
|
}).Should(Succeed())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
|
for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon, userOwnersCommon, tnt1Owner} {
|
||||||
EventuallyCreation(func() error {
|
EventuallyCreation(func() error {
|
||||||
tnt.ResourceVersion = ""
|
tnt.ResourceVersion = ""
|
||||||
|
|
||||||
@@ -202,13 +249,39 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
Expect(client.IgnoreNotFound(err)).To(Succeed())
|
Expect(client.IgnoreNotFound(err)).To(Succeed())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
|
for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon, userOwnersCommon, tnt1Owner} {
|
||||||
err := k8sClient.Delete(context.TODO(), owners)
|
err := k8sClient.Delete(context.TODO(), owners)
|
||||||
Expect(client.IgnoreNotFound(err)).To(Succeed())
|
Expect(client.IgnoreNotFound(err)).To(Succeed())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Verify owners for", func() {
|
It("Verify owners for", func() {
|
||||||
|
By("checking configuration", func() {
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
cfg := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
err := k8sClient.Get(
|
||||||
|
context.Background(),
|
||||||
|
client.ObjectKey{Name: defaultConfigurationName},
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
expected := api.UserListSpec{
|
||||||
|
{Kind: ownersInfra.Spec.Kind, Name: ownersInfra.Spec.Name},
|
||||||
|
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
|
||||||
|
{Kind: ownersCommon.Spec.Kind, Name: ownersCommon.Spec.Name},
|
||||||
|
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
|
||||||
|
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
|
||||||
|
|
||||||
|
g.Expect(cfg.Status.Users).NotTo(ContainElement(
|
||||||
|
api.UserSpec{Kind: userOwnersCommon.Spec.Kind, Name: userOwnersCommon.Spec.Name},
|
||||||
|
))
|
||||||
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
By("checking owners (e2e-owners-1)", func() {
|
By("checking owners (e2e-owners-1)", func() {
|
||||||
t := &capsulev1beta2.Tenant{}
|
t := &capsulev1beta2.Tenant{}
|
||||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
||||||
@@ -242,6 +315,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: tnt1Owner.Spec.Kind,
|
||||||
|
Name: tnt1Owner.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -250,6 +337,41 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
VerifyTenantRoleBindings(t)
|
VerifyTenantRoleBindings(t)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
By("creating namespaces (e2e-owners-1)", func() {
|
||||||
|
for _, u := range []api.UserSpec{
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "e2e-owners-1-group",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "oidc:comp:devops",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "system:serviceaccount:capsule-system:capsule",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "e2e-owners-1",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: tnt1Owner.Spec.Kind,
|
||||||
|
Name: tnt1Owner.Spec.Name,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
ns := NewNamespace("", map[string]string{
|
||||||
|
meta.TenantLabel: tnt1.GetName(),
|
||||||
|
})
|
||||||
|
NamespaceCreation(ns, u, defaultTimeoutInterval).Should(Succeed())
|
||||||
|
TenantNamespaceList(tnt1, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
By("checking owners (e2e-owners-2)", func() {
|
By("checking owners (e2e-owners-2)", func() {
|
||||||
t := &capsulev1beta2.Tenant{}
|
t := &capsulev1beta2.Tenant{}
|
||||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
|
||||||
@@ -283,6 +405,14 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -291,10 +421,62 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
VerifyTenantRoleBindings(t)
|
VerifyTenantRoleBindings(t)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
By("creating namespaces (e2e-owners-2)", func() {
|
||||||
|
for _, u := range []api.UserSpec{
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "e2e-owners-2-group",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "oidc:comp:administrators",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "system:serviceaccount:capsule-system:capsule",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "e2e-owners-2",
|
||||||
|
},
|
||||||
|
api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
ns := NewNamespace("", map[string]string{
|
||||||
|
meta.TenantLabel: tnt2.GetName(),
|
||||||
|
})
|
||||||
|
NamespaceCreation(ns, u, defaultTimeoutInterval).Should(Succeed())
|
||||||
|
TenantNamespaceList(tnt2, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
By("remove common tenant-owners", func() {
|
By("remove common tenant-owners", func() {
|
||||||
Expect(k8sClient.Delete(context.TODO(), ownersCommon)).Should(Succeed())
|
Expect(k8sClient.Delete(context.TODO(), ownersCommon)).Should(Succeed())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
By("checking configuration", func() {
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
cfg := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
err := k8sClient.Get(
|
||||||
|
context.Background(),
|
||||||
|
client.ObjectKey{Name: defaultConfigurationName},
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
expected := api.UserListSpec{
|
||||||
|
{Kind: ownersInfra.Spec.Kind, Name: ownersInfra.Spec.Name},
|
||||||
|
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
|
||||||
|
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
|
||||||
|
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
|
||||||
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
By("checking owners (e2e-owners-1)", func() {
|
By("checking owners (e2e-owners-1)", func() {
|
||||||
t := &capsulev1beta2.Tenant{}
|
t := &capsulev1beta2.Tenant{}
|
||||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
||||||
@@ -328,6 +510,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: tnt1Owner.Spec.Kind,
|
||||||
|
Name: tnt1Owner.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -369,6 +565,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -381,6 +584,26 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
Expect(k8sClient.Delete(context.TODO(), ownersInfra)).Should(Succeed())
|
Expect(k8sClient.Delete(context.TODO(), ownersInfra)).Should(Succeed())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
By("checking configuration", func() {
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
cfg := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
err := k8sClient.Get(
|
||||||
|
context.Background(),
|
||||||
|
client.ObjectKey{Name: defaultConfigurationName},
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
expected := api.UserListSpec{
|
||||||
|
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
|
||||||
|
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
|
||||||
|
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
|
||||||
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
By("checking owners (e2e-owners-1)", func() {
|
By("checking owners (e2e-owners-1)", func() {
|
||||||
t := &capsulev1beta2.Tenant{}
|
t := &capsulev1beta2.Tenant{}
|
||||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
||||||
@@ -414,6 +637,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: tnt1Owner.Spec.Kind,
|
||||||
|
Name: tnt1Owner.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -448,6 +685,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -460,6 +704,25 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
Expect(k8sClient.Delete(context.TODO(), ownersDevops)).Should(Succeed())
|
Expect(k8sClient.Delete(context.TODO(), ownersDevops)).Should(Succeed())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
By("checking configuration", func() {
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
cfg := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
err := k8sClient.Get(
|
||||||
|
context.Background(),
|
||||||
|
client.ObjectKey{Name: defaultConfigurationName},
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
expected := api.UserListSpec{
|
||||||
|
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
|
||||||
|
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
|
||||||
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
By("checking owners (e2e-owners-1)", func() {
|
By("checking owners (e2e-owners-1)", func() {
|
||||||
t := &capsulev1beta2.Tenant{}
|
t := &capsulev1beta2.Tenant{}
|
||||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
|
||||||
@@ -486,6 +749,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: tnt1Owner.Spec.Kind,
|
||||||
|
Name: tnt1Owner.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
@@ -520,6 +797,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
|
|||||||
},
|
},
|
||||||
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: userOwnersCommon.Spec.Kind,
|
||||||
|
Name: userOwnersCommon.Spec.Name,
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"service-admin"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(normalizeOwners(t.Status.Owners)).
|
Expect(normalizeOwners(t.Status.Owners)).
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||||
@@ -65,6 +66,11 @@ var _ = BeforeSuite(func() {
|
|||||||
Expect(ctrlClient).ToNot(BeNil())
|
Expect(ctrlClient).ToNot(BeNil())
|
||||||
|
|
||||||
k8sClient = &e2eClient{Client: ctrlClient}
|
k8sClient = &e2eClient{Client: ctrlClient}
|
||||||
|
|
||||||
|
ModifyCapsuleConfigurationOpts(func(cfg *capsulev1beta2.CapsuleConfiguration) {
|
||||||
|
cfg.Spec = configuration.DefaultCapsuleConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var _ = AfterSuite(func() {
|
var _ = AfterSuite(func() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -332,43 +333,38 @@ func VerifyTenantRoleBindings(
|
|||||||
tnt *capsulev1beta2.Tenant,
|
tnt *capsulev1beta2.Tenant,
|
||||||
) {
|
) {
|
||||||
Eventually(func(g Gomega) {
|
Eventually(func(g Gomega) {
|
||||||
|
roles := tnt.GetRoleBindings()
|
||||||
|
|
||||||
// List all RoleBindings once per namespace to avoid repeated API calls.
|
// List all RoleBindings once per namespace to avoid repeated API calls.
|
||||||
for _, ns := range tnt.Status.Namespaces {
|
for _, ns := range tnt.Status.Namespaces {
|
||||||
for i, owner := range tnt.Status.Owners {
|
for _, role := range roles {
|
||||||
for _, role := range owner.ClusterRoles {
|
rbName := meta.NameForManagedRoleBindings(utils.RoleBindingHashFunc(role))
|
||||||
rbName := fmt.Sprintf("capsule-%s-%d-%s", tnt.Name, i, role)
|
|
||||||
|
|
||||||
rb := &rbacv1.RoleBinding{}
|
rb := &rbacv1.RoleBinding{}
|
||||||
err := k8sClient.Get(context.Background(), client.ObjectKey{
|
err := k8sClient.Get(context.Background(), client.ObjectKey{
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Name: rbName,
|
Name: rbName,
|
||||||
}, rb)
|
}, rb)
|
||||||
|
|
||||||
g.Expect(err).ToNot(HaveOccurred(),
|
g.Expect(err).ToNot(HaveOccurred(),
|
||||||
"expected RoleBinding %s/%s to exist", ns, rbName)
|
"expected RoleBinding %s/%s to exist (Owner: %s)", ns, rbName, role.Subjects,
|
||||||
|
)
|
||||||
|
|
||||||
g.Expect(rb.RoleRef.Name).To(Equal(role),
|
g.Expect(rb.RoleRef.Name).To(Equal(role.ClusterRoleName),
|
||||||
"expected RoleBinding %s/%s to have RoleRef.Name=%q",
|
"expected RoleBinding %s/%s to have RoleRef.Name=%q",
|
||||||
ns, rbName, role)
|
ns, rbName, role.ClusterRoleName)
|
||||||
|
|
||||||
g.Expect(rb.Subjects).ToNot(BeEmpty(),
|
g.Expect(rb.Subjects).ToNot(BeEmpty(),
|
||||||
"expected RoleBinding %s/%s to have at least one subject", ns, rbName)
|
"expected RoleBinding %s/%s to have at least one subject", ns, rbName)
|
||||||
|
|
||||||
foundSubject := false
|
g.Expect(rb.Subjects).To(ConsistOf(role.Subjects),
|
||||||
for _, s := range rb.Subjects {
|
"expected RoleBinding %s/%s to have exact subjects",
|
||||||
if s.Kind == string(owner.Kind) && s.Name == owner.Name {
|
ns, rb.Name,
|
||||||
foundSubject = true
|
)
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Expect(foundSubject).To(BeTrue(),
|
|
||||||
"expected RoleBinding %s/%s to contain subject %s/%s",
|
|
||||||
ns, rb.Name, owner.Kind, owner.Name)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}).WithTimeout(30 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed())
|
}).WithTimeout(30 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -19,7 +19,6 @@ require (
|
|||||||
k8s.io/apimachinery v0.35.0
|
k8s.io/apimachinery v0.35.0
|
||||||
k8s.io/apiserver v0.35.0
|
k8s.io/apiserver v0.35.0
|
||||||
k8s.io/client-go v0.35.0
|
k8s.io/client-go v0.35.0
|
||||||
k8s.io/dynamic-resource-allocation v0.35.0
|
|
||||||
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2
|
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2
|
||||||
sigs.k8s.io/cluster-api v1.12.1
|
sigs.k8s.io/cluster-api v1.12.1
|
||||||
sigs.k8s.io/controller-runtime v0.22.4
|
sigs.k8s.io/controller-runtime v0.22.4
|
||||||
@@ -53,7 +52,6 @@ require (
|
|||||||
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
|
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/gobuffalo/flect v1.0.3 // indirect
|
github.com/gobuffalo/flect v1.0.3 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/gnostic-models v0.7.0 // indirect
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
|||||||
115
go.sum
115
go.sum
@@ -95,8 +95,6 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4
|
|||||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
|
||||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||||
@@ -108,8 +106,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -124,8 +120,6 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE
|
|||||||
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -150,12 +144,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
|
|||||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||||
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
|
||||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
|
||||||
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||||
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -179,10 +169,10 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||||
@@ -212,24 +202,22 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
|||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
@@ -244,55 +232,28 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
|||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||||
@@ -313,68 +274,32 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
|
||||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
|
||||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
|
||||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
|
||||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||||
k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo=
|
|
||||||
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
|
|
||||||
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
|
|
||||||
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
|
|
||||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
|
||||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
|
||||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
|
||||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
|
||||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||||
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
|
|
||||||
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
|
|
||||||
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
|
|
||||||
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
|
|
||||||
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
||||||
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
||||||
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
|
||||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
|
||||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
|
||||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
|
||||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||||
k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI=
|
k8s.io/cluster-bootstrap v0.34.2 h1:oKckPeunVCns37BntcsxaOesDul32yzGd3DFLjW2fc8=
|
||||||
k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds=
|
k8s.io/cluster-bootstrap v0.34.2/go.mod h1:f21byPR7X5nt12ivZi+J3pb4sG4SH6VySX8KAAJA8BY=
|
||||||
k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ=
|
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||||
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
|
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||||
k8s.io/dynamic-resource-allocation v0.34.2 h1:SjlRGSWl6CZXoJwQNL+Y0wRfdH8PkJ4mHRNK6MMj0bY=
|
|
||||||
k8s.io/dynamic-resource-allocation v0.34.2/go.mod h1:ul6I+gfrCmC+OCuVdN0/iykyB2sPrIqh2WyKQ3RQPCU=
|
|
||||||
k8s.io/dynamic-resource-allocation v0.34.3/go.mod h1:eYjQqNaHLfqXT94lbSXEy8ZLaUg1mGJ2JCEtNWM7e7M=
|
|
||||||
k8s.io/dynamic-resource-allocation v0.35.0/go.mod h1:uaFga3VJtwyfpfZwpuJG7mlurWGQaaiGUa+QZmooz2U=
|
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
|
||||||
k8s.io/utils v0.0.0-20251218160917-61b37f7a4624 h1:wadElzGW3vTZ1Et18CImPEErLaXvMSU5369b0to32+0=
|
|
||||||
k8s.io/utils v0.0.0-20251218160917-61b37f7a4624/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
|
||||||
k8s.io/utils v0.0.0-20251219084037-98d557b7f1e7 h1:H6xtwB5tC+KFSHoEhA1o7DnOtHDEo+n9OBSHjlajVKc=
|
|
||||||
k8s.io/utils v0.0.0-20251219084037-98d557b7f1e7/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
|
||||||
k8s.io/utils v0.0.0-20251222190033-383b50a9004e h1:cdvXqyVPudW9BZL5+lPjMedlEHJDVMnE6lzvcQaC5UE=
|
|
||||||
k8s.io/utils v0.0.0-20251222190033-383b50a9004e/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
|
||||||
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 h1:OfgiEo21hGiwx1oJUU5MpEaeOEg6coWndBkZF/lkFuE=
|
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 h1:OfgiEo21hGiwx1oJUU5MpEaeOEg6coWndBkZF/lkFuE=
|
||||||
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||||
sigs.k8s.io/cluster-api v1.11.3 h1:apxfugbP1X8AG7THCM74CTarCOW4H2oOc6hlbm1hY80=
|
|
||||||
sigs.k8s.io/cluster-api v1.11.3/go.mod h1:CA471SACi81M8DzRKTlWpHV33G0cfWEj7sC4fALFVok=
|
|
||||||
sigs.k8s.io/cluster-api v1.12.1 h1:s3DivSZjXdu2HPyOtV/n6XwSZBaIycZdKNs4y8X+3lY=
|
sigs.k8s.io/cluster-api v1.12.1 h1:s3DivSZjXdu2HPyOtV/n6XwSZBaIycZdKNs4y8X+3lY=
|
||||||
sigs.k8s.io/cluster-api v1.12.1/go.mod h1:+S6WJdi8UPdqv5q9nka5al3ed/Qa0zAcSBgzTaa9VKA=
|
sigs.k8s.io/cluster-api v1.12.1/go.mod h1:+S6WJdi8UPdqv5q9nka5al3ed/Qa0zAcSBgzTaa9VKA=
|
||||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||||
sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
|
|
||||||
sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
|
||||||
sigs.k8s.io/gateway-api v1.4.1 h1:NPxFutNkKNa8UfLd2CMlEuhIPMQgDQ6DXNKG9sHbJU8=
|
sigs.k8s.io/gateway-api v1.4.1 h1:NPxFutNkKNa8UfLd2CMlEuhIPMQgDQ6DXNKG9sHbJU8=
|
||||||
sigs.k8s.io/gateway-api v1.4.1/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
sigs.k8s.io/gateway-api v1.4.1/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
|||||||
6
hack/distro/capsule/example-setup/kustomization.yaml
Normal file
6
hack/distro/capsule/example-setup/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- owners.yaml
|
||||||
|
- tenants.yaml
|
||||||
|
- resource.yaml
|
||||||
20
hack/distro/capsule/example-setup/owners.yaml
Normal file
20
hack/distro/capsule/example-setup/owners.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: TenantOwner
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
team: devops
|
||||||
|
name: devops
|
||||||
|
spec:
|
||||||
|
kind: Group
|
||||||
|
name: "oidc:org:devops"
|
||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: TenantOwner
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
team: platform
|
||||||
|
name: platform
|
||||||
|
spec:
|
||||||
|
kind: Group
|
||||||
|
name: "oidc:org:platform"
|
||||||
21
hack/distro/capsule/example-setup/resource.yaml
Normal file
21
hack/distro/capsule/example-setup/resource.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: GlobalTenantResource
|
||||||
|
metadata:
|
||||||
|
name: custom-cm
|
||||||
|
namespace: solar-system
|
||||||
|
spec:
|
||||||
|
resyncPeriod: 60s
|
||||||
|
resources:
|
||||||
|
- additionalMetadata:
|
||||||
|
labels:
|
||||||
|
"replicated-by": "capsule"
|
||||||
|
rawItems:
|
||||||
|
- apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: game-demo
|
||||||
|
data:
|
||||||
|
# property-like keys; each key maps to a simple value
|
||||||
|
player_initial_lives: "3"
|
||||||
|
ui_properties_file_name: "user-interface.properties"
|
||||||
63
hack/distro/capsule/example-setup/tenants.yaml
Normal file
63
hack/distro/capsule/example-setup/tenants.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: Tenant
|
||||||
|
metadata:
|
||||||
|
name: solar
|
||||||
|
spec:
|
||||||
|
permissions:
|
||||||
|
matchOwners:
|
||||||
|
- matchLabels:
|
||||||
|
team: platform
|
||||||
|
- matchLabels:
|
||||||
|
tenant: solar
|
||||||
|
owners:
|
||||||
|
- name: alice
|
||||||
|
kind: User
|
||||||
|
additionalRoleBindings:
|
||||||
|
- clusterRoleName: 'view'
|
||||||
|
subjects:
|
||||||
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: User
|
||||||
|
name: joe
|
||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: Tenant
|
||||||
|
metadata:
|
||||||
|
name: green
|
||||||
|
spec:
|
||||||
|
permissions:
|
||||||
|
matchOwners:
|
||||||
|
- matchLabels:
|
||||||
|
team: devops
|
||||||
|
- matchLabels:
|
||||||
|
tenant: green
|
||||||
|
owners:
|
||||||
|
- name: bob
|
||||||
|
kind: User
|
||||||
|
additionalRoleBindings:
|
||||||
|
- clusterRoleName: 'view'
|
||||||
|
subjects:
|
||||||
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: User
|
||||||
|
name: alice
|
||||||
|
---
|
||||||
|
apiVersion: capsule.clastix.io/v1beta2
|
||||||
|
kind: Tenant
|
||||||
|
metadata:
|
||||||
|
name: wind
|
||||||
|
spec:
|
||||||
|
permissions:
|
||||||
|
matchOwners:
|
||||||
|
- matchLabels:
|
||||||
|
team: devops
|
||||||
|
- matchLabels:
|
||||||
|
tenant: wind
|
||||||
|
owners:
|
||||||
|
- name: joe
|
||||||
|
kind: User
|
||||||
|
additionalRoleBindings:
|
||||||
|
- clusterRoleName: 'view'
|
||||||
|
subjects:
|
||||||
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Group
|
||||||
|
name: wind-users
|
||||||
4
hack/distro/capsule/kustomization.yaml
Normal file
4
hack/distro/capsule/kustomization.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- release.flux.yaml
|
||||||
42
hack/distro/capsule/release.flux.yaml
Normal file
42
hack/distro/capsule/release.flux.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||||
|
kind: HelmRelease
|
||||||
|
metadata:
|
||||||
|
name: capsule
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
serviceAccountName: kustomize-controller
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10m
|
||||||
|
targetNamespace: capsule-system
|
||||||
|
releaseName: "capsule"
|
||||||
|
chart:
|
||||||
|
spec:
|
||||||
|
chart: capsule
|
||||||
|
version: "0.12.4"
|
||||||
|
sourceRef:
|
||||||
|
kind: HelmRepository
|
||||||
|
name: capsule
|
||||||
|
interval: 24h
|
||||||
|
install:
|
||||||
|
createNamespace: true
|
||||||
|
remediation:
|
||||||
|
retries: -1
|
||||||
|
upgrade:
|
||||||
|
remediation:
|
||||||
|
remediateLastFailure: true
|
||||||
|
retries: -1
|
||||||
|
driftDetection:
|
||||||
|
mode: enabled
|
||||||
|
ignore:
|
||||||
|
- paths: ["/spec/replicas"]
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: HelmRepository
|
||||||
|
metadata:
|
||||||
|
name: capsule
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
type: "oci"
|
||||||
|
interval: 12h0m0s
|
||||||
|
url: oci://ghcr.io/projectcapsule/charts
|
||||||
@@ -5,42 +5,169 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/internal/controllers/utils"
|
"github.com/projectcapsule/capsule/internal/controllers/utils"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
client client.Client
|
client.Client
|
||||||
|
|
||||||
Log logr.Logger
|
Log logr.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
|
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
|
||||||
c.client = mgr.GetClient()
|
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName)).
|
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName)).
|
||||||
Complete(c)
|
Watches(
|
||||||
|
&capsulev1beta2.TenantOwner{},
|
||||||
|
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||||
|
return []reconcile.Request{
|
||||||
|
{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: ctrlConfig.ConfigurationName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
builder.WithPredicates(predicate.Funcs{
|
||||||
|
CreateFunc: func(e event.CreateEvent) bool {
|
||||||
|
to, ok := e.Object.(*capsulev1beta2.TenantOwner)
|
||||||
|
|
||||||
|
return ok && to.Spec.Aggregate
|
||||||
|
},
|
||||||
|
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||||
|
oldTo, ok1 := e.ObjectOld.(*capsulev1beta2.TenantOwner)
|
||||||
|
newTo, ok2 := e.ObjectNew.(*capsulev1beta2.TenantOwner)
|
||||||
|
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTo.Spec.Aggregate != newTo.Spec.Aggregate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTo.Spec.Name != newTo.Spec.Name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldTo.Spec.Kind != newTo.Spec.Kind {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||||
|
to, ok := e.Object.(*capsulev1beta2.TenantOwner)
|
||||||
|
|
||||||
|
return ok && to.Spec.Aggregate
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).
|
||||||
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
|
func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) {
|
||||||
c.Log.Info("CapsuleConfiguration reconciliation started", "request.name", request.Name)
|
r.Log.V(5).Info("CapsuleConfiguration reconciliation started", "request.name", request.Name)
|
||||||
|
|
||||||
|
cfg := configuration.NewCapsuleConfiguration(ctx, r.Client, request.Name)
|
||||||
|
|
||||||
|
instance := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
r.Log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||||
|
|
||||||
|
return reconcile.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Log.Error(err, "Error reading the object")
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if uerr := r.updateConfigStatus(ctx, instance); uerr != nil {
|
||||||
|
err = fmt.Errorf("cannot update config status: %w", uerr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
cfg := configuration.NewCapsuleConfiguration(ctx, c.client, request.Name)
|
|
||||||
// Validating the Capsule Configuration options
|
// Validating the Capsule Configuration options
|
||||||
if _, err = cfg.ProtectedNamespaceRegexp(); err != nil {
|
if _, err = cfg.ProtectedNamespaceRegexp(); err != nil {
|
||||||
panic(errors.Wrap(err, "Invalid configuration for protected Namespace regex"))
|
panic(errors.Wrap(err, "Invalid configuration for protected Namespace regex"))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Log.V(5).Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
|
r.Log.V(5).Info("Validated Regex")
|
||||||
|
|
||||||
|
if err := r.gatherCapsuleUsers(ctx, instance, cfg); err != nil {
|
||||||
|
return reconcile.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Log.V(5).Info("Gathered users", "users", len(instance.Status.Users))
|
||||||
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Manager) gatherCapsuleUsers(
|
||||||
|
ctx context.Context,
|
||||||
|
instance *capsulev1beta2.CapsuleConfiguration,
|
||||||
|
cfg configuration.Configuration,
|
||||||
|
) (err error) {
|
||||||
|
users := cfg.Users()
|
||||||
|
|
||||||
|
toList := &capsulev1beta2.TenantOwnerList{}
|
||||||
|
if err := r.List(ctx, toList); err != nil {
|
||||||
|
return fmt.Errorf("listing TenantOwner CRs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range toList.Items {
|
||||||
|
to := &toList.Items[i]
|
||||||
|
|
||||||
|
if !to.Spec.Aggregate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
users.Upsert(api.UserSpec{
|
||||||
|
Kind: to.Spec.Kind,
|
||||||
|
Name: to.Spec.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Status.Users = users
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Manager) updateConfigStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
instance *capsulev1beta2.CapsuleConfiguration,
|
||||||
|
) error {
|
||||||
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||||
|
latest := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
if err = r.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
latest.Status = instance.Status
|
||||||
|
|
||||||
|
return r.Status().Update(ctx, latest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,7 +114,12 @@ func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) erro
|
|||||||
crb.RoleRef = api.ProvisionerClusterRoleBinding.RoleRef
|
crb.RoleRef = api.ProvisionerClusterRoleBinding.RoleRef
|
||||||
crb.Subjects = nil
|
crb.Subjects = nil
|
||||||
|
|
||||||
for _, entity := range r.Configuration.Administrators() {
|
users := r.Configuration.GetUsersByStatus()
|
||||||
|
for _, u := range r.Configuration.Administrators() {
|
||||||
|
users.Upsert(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entity := range users {
|
||||||
switch entity.Kind {
|
switch entity.Kind {
|
||||||
case api.UserOwner:
|
case api.UserOwner:
|
||||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||||
@@ -140,20 +145,6 @@ func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range r.Configuration.UserGroups() {
|
|
||||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
|
||||||
Kind: rbacv1.GroupKind,
|
|
||||||
Name: group,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, user := range r.Configuration.UserNames() {
|
|
||||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
|
||||||
Kind: rbacv1.UserKind,
|
|
||||||
Name: user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Configuration.AllowServiceAccountPromotion() {
|
if r.Configuration.AllowServiceAccountPromotion() {
|
||||||
saList := &corev1.ServiceAccountList{}
|
saList := &corev1.ServiceAccountList{}
|
||||||
if err := r.Client.List(ctx, saList, client.MatchingLabels{
|
if err := r.Client.List(ctx, saList, client.MatchingLabels{
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ package tenant
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
@@ -17,64 +15,21 @@ import (
|
|||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
"github.com/projectcapsule/capsule/pkg/api"
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Manager) userClusterRoleBindings(owner api.CoreOwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
|
|
||||||
var subject rbacv1.Subject
|
|
||||||
|
|
||||||
if owner.Kind == "ServiceAccount" {
|
|
||||||
splitName := strings.Split(owner.Name, ":")
|
|
||||||
|
|
||||||
subject = rbacv1.Subject{
|
|
||||||
Kind: owner.Kind.String(),
|
|
||||||
Name: splitName[len(splitName)-1],
|
|
||||||
Namespace: splitName[len(splitName)-2],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
subject = rbacv1.Subject{
|
|
||||||
APIGroup: rbacv1.GroupName,
|
|
||||||
Kind: owner.Kind.String(),
|
|
||||||
Name: owner.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.AdditionalRoleBindingsSpec{
|
|
||||||
ClusterRoleName: clusterRole,
|
|
||||||
Subjects: []rbacv1.Subject{
|
|
||||||
subject,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync the dynamic Tenant Owner specific cluster-roles and additional Role Bindings, which can be used in many ways:
|
// Sync the dynamic Tenant Owner specific cluster-roles and additional Role Bindings, which can be used in many ways:
|
||||||
// applying Pod Security Policies or giving access to CRDs or specific API groups.
|
// applying Pod Security Policies or giving access to CRDs or specific API groups.
|
||||||
func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||||
// hashing the RoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
|
roleBindings := tenant.GetRoleBindings()
|
||||||
hashFn := func(binding api.AdditionalRoleBindingsSpec) string {
|
|
||||||
h := fnv.New64a()
|
|
||||||
|
|
||||||
_, _ = h.Write([]byte(binding.ClusterRoleName))
|
// Hashing
|
||||||
|
hashes := map[string]api.AdditionalRoleBindingsSpec{}
|
||||||
|
|
||||||
for _, sub := range binding.Subjects {
|
for _, binding := range roleBindings {
|
||||||
_, _ = h.Write([]byte(sub.Kind + sub.Name))
|
hash := utils.RoleBindingHashFunc(binding)
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%x", h.Sum64())
|
hashes[hash] = binding
|
||||||
}
|
|
||||||
|
|
||||||
// getting requested Role Binding keys
|
|
||||||
keys := make([]string, 0, len(tenant.Status.Owners))
|
|
||||||
// Generating for dynamic tenant owners cluster roles
|
|
||||||
for _, owner := range tenant.Status.Owners {
|
|
||||||
for _, clusterRoleName := range owner.ClusterRoles {
|
|
||||||
cr := r.userClusterRoleBindings(owner, clusterRoleName)
|
|
||||||
|
|
||||||
keys = append(keys, hashFn(cr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Generating hash of additional role bindings
|
|
||||||
for _, i := range tenant.Spec.AdditionalRoleBindings {
|
|
||||||
keys = append(keys, hashFn(i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group := new(errgroup.Group)
|
group := new(errgroup.Group)
|
||||||
@@ -83,34 +38,29 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
|
|||||||
namespace := ns
|
namespace := ns
|
||||||
|
|
||||||
group.Go(func() error {
|
group.Go(func() error {
|
||||||
return r.syncAdditionalRoleBinding(ctx, tenant, namespace, keys, hashFn)
|
return r.syncAdditionalRoleBinding(ctx, tenant, namespace, hashes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return group.Wait()
|
return group.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsulev1beta2.Tenant, ns string, keys []string, hashFn func(binding api.AdditionalRoleBindingsSpec) string) (err error) {
|
func (r *Manager) syncAdditionalRoleBinding(
|
||||||
if err = r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{}); err != nil {
|
ctx context.Context,
|
||||||
return err
|
tenant *capsulev1beta2.Tenant,
|
||||||
}
|
ns string,
|
||||||
|
bindings map[string]api.AdditionalRoleBindingsSpec,
|
||||||
|
) (err error) {
|
||||||
|
keys := []string{}
|
||||||
|
|
||||||
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0)
|
for hash, roleBinding := range bindings {
|
||||||
|
name := meta.NameForManagedRoleBindings(hash)
|
||||||
|
|
||||||
for _, owner := range tenant.Status.Owners {
|
keys = append(keys, hash)
|
||||||
for _, clusterRoleName := range owner.ClusterRoles {
|
|
||||||
roleBindings = append(roleBindings, r.userClusterRoleBindings(owner, clusterRoleName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
roleBindings = append(roleBindings, tenant.Spec.AdditionalRoleBindings...)
|
|
||||||
|
|
||||||
for i, roleBinding := range roleBindings {
|
|
||||||
roleBindingHashLabel := hashFn(roleBinding)
|
|
||||||
|
|
||||||
target := &rbacv1.RoleBinding{
|
target := &rbacv1.RoleBinding{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, roleBinding.ClusterRoleName),
|
Name: name,
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -126,7 +76,7 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
|
|||||||
}
|
}
|
||||||
|
|
||||||
target.Labels[meta.TenantLabel] = tenant.Name
|
target.Labels[meta.TenantLabel] = tenant.Name
|
||||||
target.Labels[meta.RolebindingLabel] = roleBindingHashLabel
|
target.Labels[meta.RolebindingLabel] = hash
|
||||||
|
|
||||||
if roleBinding.Annotations != nil {
|
if roleBinding.Annotations != nil {
|
||||||
target.Annotations = roleBinding.Annotations
|
target.Annotations = roleBinding.Annotations
|
||||||
@@ -156,5 +106,6 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Prune at finish to prevent gaps
|
||||||
|
return r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
TenantNameLabel = "kubernetes.io/metadata.name"
|
TenantNameLabel = "kubernetes.io/metadata.name"
|
||||||
TenantLabel = "capsule.clastix.io/tenant"
|
|
||||||
|
TenantLabel = "capsule.clastix.io/tenant"
|
||||||
|
NewTenantLabel = "projectcapsule.dev/tenant"
|
||||||
|
|
||||||
ResourcePoolLabel = "projectcapsule.dev/pool"
|
ResourcePoolLabel = "projectcapsule.dev/pool"
|
||||||
|
|
||||||
|
|||||||
10
pkg/api/meta/names.go
Normal file
10
pkg/api/meta/names.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2020-2025 Project Capsule Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package meta
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func NameForManagedRoleBindings(hash string) string {
|
||||||
|
return fmt.Sprintf("capsule:managed:%s", hash)
|
||||||
|
}
|
||||||
@@ -23,7 +23,10 @@ type NamespaceSelector struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector.
|
// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector.
|
||||||
func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client client.Client) ([]corev1.Namespace, error) {
|
func (s *NamespaceSelector) GetMatchingNamespaces(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
) ([]corev1.Namespace, error) {
|
||||||
if s.LabelSelector == nil {
|
if s.LabelSelector == nil {
|
||||||
return nil, nil // No namespace selector means all namespaces
|
return nil, nil // No namespace selector means all namespaces
|
||||||
}
|
}
|
||||||
@@ -34,7 +37,7 @@ func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client cl
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespaceList := &corev1.NamespaceList{}
|
namespaceList := &corev1.NamespaceList{}
|
||||||
if err := client.List(ctx, namespaceList); err != nil {
|
if err := c.List(ctx, namespaceList); err != nil {
|
||||||
return nil, fmt.Errorf("failed to list namespaces: %w", err)
|
return nil, fmt.Errorf("failed to list namespaces: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +52,76 @@ func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client cl
|
|||||||
return matchingNamespaces, nil
|
return matchingNamespaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selector for resources and their labels or selecting origin namespaces
|
||||||
|
// +kubebuilder:object:generate=true
|
||||||
|
type SelectorWithNamespaceSelector struct {
|
||||||
|
// Select Items based on their labels. If the namespaceSelector is also set, the selector is applied
|
||||||
|
// to items within the selected namespaces. Otherwise for all the items.
|
||||||
|
*metav1.LabelSelector `json:",inline"`
|
||||||
|
|
||||||
|
// NamespaceSelector for filtering namespaces by labels where items can be located in
|
||||||
|
NamespaceSelector *NamespaceSelector `json:"namespaceSelector,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SelectorWithNamespaceSelector) MatchObjects(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
objects []metav1.Object,
|
||||||
|
) ([]metav1.Object, error) {
|
||||||
|
if s == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var objSelector labels.Selector
|
||||||
|
|
||||||
|
if s.LabelSelector != nil {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
objSelector, err = metav1.LabelSelectorAsSelector(s.LabelSelector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid namespace selector: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelFilteredObjects := make([]metav1.Object, 0, len(objects))
|
||||||
|
|
||||||
|
for _, obj := range objects {
|
||||||
|
if objSelector != nil && !objSelector.Matches(labels.Set(obj.GetLabels())) {
|
||||||
|
continue // Skip non-matching objects
|
||||||
|
}
|
||||||
|
|
||||||
|
labelFilteredObjects = append(labelFilteredObjects, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.NamespaceSelector == nil {
|
||||||
|
return labelFilteredObjects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingNamespaces, err := s.NamespaceSelector.GetMatchingNamespaces(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetching matching namespaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceSet := make(map[string]struct{})
|
||||||
|
for _, ns := range matchingNamespaces {
|
||||||
|
namespaceSet[ns.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalMatchingObjects := make([]metav1.Object, 0, len(labelFilteredObjects))
|
||||||
|
|
||||||
|
for _, obj := range labelFilteredObjects {
|
||||||
|
if len(namespaceSet) > 0 {
|
||||||
|
if _, exists := namespaceSet[obj.GetNamespace()]; !exists {
|
||||||
|
continue // Skip objects in disallowed namespaces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalMatchingObjects = append(finalMatchingObjects, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalMatchingObjects, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListBySelectors lists objects of type T (using list L), then returns all items that
|
// ListBySelectors lists objects of type T (using list L), then returns all items that
|
||||||
// match ANY of the provided LabelSelectors. The result is unique by namespace/name.
|
// match ANY of the provided LabelSelectors. The result is unique by namespace/name.
|
||||||
func ListBySelectors[T client.Object](
|
func ListBySelectors[T client.Object](
|
||||||
|
|||||||
@@ -30,3 +30,28 @@ func (in *NamespaceSelector) DeepCopy() *NamespaceSelector {
|
|||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *SelectorWithNamespaceSelector) DeepCopyInto(out *SelectorWithNamespaceSelector) {
|
||||||
|
*out = *in
|
||||||
|
if in.LabelSelector != nil {
|
||||||
|
in, out := &in.LabelSelector, &out.LabelSelector
|
||||||
|
*out = new(v1.LabelSelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.NamespaceSelector != nil {
|
||||||
|
in, out := &in.NamespaceSelector, &out.NamespaceSelector
|
||||||
|
*out = new(NamespaceSelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelectorWithNamespaceSelector.
|
||||||
|
func (in *SelectorWithNamespaceSelector) DeepCopy() *SelectorWithNamespaceSelector {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(SelectorWithNamespaceSelector)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
// +kubebuilder:object:generate=true
|
// +kubebuilder:object:generate=true
|
||||||
|
|
||||||
type OwnerSpec struct {
|
type OwnerSpec struct {
|
||||||
@@ -26,6 +30,21 @@ type CoreOwnerSpec struct {
|
|||||||
ClusterRoles []string `json:"clusterRoles,omitempty"`
|
ClusterRoles []string `json:"clusterRoles,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o CoreOwnerSpec) ToAdditionalRolebindings() []AdditionalRoleBindingsSpec {
|
||||||
|
bindings := make([]AdditionalRoleBindingsSpec, 0, len(o.ClusterRoles))
|
||||||
|
|
||||||
|
for _, clusterRoleName := range o.ClusterRoles {
|
||||||
|
bindings = append(bindings, AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: clusterRoleName,
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
o.Subject(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
|
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
|
||||||
type OwnerKind string
|
type OwnerKind string
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ func (o *OwnerStatusListSpec) Upsert(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Strings(existing.ClusterRoles)
|
||||||
|
|
||||||
*o = owners
|
*o = owners
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func TestUpsert_DeduplicatesClusterRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
got := list[0]
|
got := list[0]
|
||||||
|
|
||||||
expected := []string{"admin", "viewer", "editor"}
|
expected := []string{"admin", "editor", "viewer"}
|
||||||
if !reflect.DeepEqual(got.ClusterRoles, expected) {
|
if !reflect.DeepEqual(got.ClusterRoles, expected) {
|
||||||
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
|
t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles)
|
||||||
}
|
}
|
||||||
|
|||||||
121
pkg/api/owner_test.go
Normal file
121
pkg/api/owner_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCoreOwnerSpec_ToAdditionalRolebindings(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in api.CoreOwnerSpec
|
||||||
|
want []api.AdditionalRoleBindingsSpec
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no cluster roles yields empty slice",
|
||||||
|
in: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "alice",
|
||||||
|
},
|
||||||
|
ClusterRoles: nil,
|
||||||
|
},
|
||||||
|
want: []api.AdditionalRoleBindingsSpec{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one role creates one binding with subject",
|
||||||
|
in: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "alice",
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"admin"},
|
||||||
|
},
|
||||||
|
want: []api.AdditionalRoleBindingsSpec{
|
||||||
|
{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{APIGroup: rbacv1.GroupName, Kind: "User", Name: "alice"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple roles create one binding per role (preserves order)",
|
||||||
|
in: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "devops",
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"view", "edit"},
|
||||||
|
},
|
||||||
|
want: []api.AdditionalRoleBindingsSpec{
|
||||||
|
{
|
||||||
|
ClusterRoleName: "view",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{APIGroup: rbacv1.GroupName, Kind: "Group", Name: "devops"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ClusterRoleName: "edit",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{APIGroup: rbacv1.GroupName, Kind: "Group", Name: "devops"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serviceaccount subject is split correctly in bindings",
|
||||||
|
in: api.CoreOwnerSpec{
|
||||||
|
UserSpec: api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "system:serviceaccount:capsule-system:capsule",
|
||||||
|
},
|
||||||
|
ClusterRoles: []string{"admin", "service-admin"},
|
||||||
|
},
|
||||||
|
want: []api.AdditionalRoleBindingsSpec{
|
||||||
|
{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{Kind: "ServiceAccount", Namespace: "capsule-system", Name: "capsule"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ClusterRoleName: "service-admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{Kind: "ServiceAccount", Namespace: "capsule-system", Name: "capsule"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.in.ToAdditionalRolebindings()
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("expected %d bindings, got %d: %#v", len(tt.want), len(got), got)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tt.want {
|
||||||
|
if got[i].ClusterRoleName != tt.want[i].ClusterRoleName {
|
||||||
|
t.Fatalf("binding[%d].ClusterRoleName: expected %q, got %q", i, tt.want[i].ClusterRoleName, got[i].ClusterRoleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got[i].Subjects) != len(tt.want[i].Subjects) {
|
||||||
|
t.Fatalf("binding[%d].Subjects length: expected %d, got %d", i, len(tt.want[i].Subjects), len(got[i].Subjects))
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := range tt.want[i].Subjects {
|
||||||
|
if got[i].Subjects[j] != tt.want[i].Subjects[j] {
|
||||||
|
t.Fatalf("binding[%d].Subjects[%d]: expected %#v, got %#v", i, j, tt.want[i].Subjects[j], got[i].Subjects[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
)
|
||||||
|
|
||||||
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
|
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
|
||||||
type UserKind string
|
type UserKind string
|
||||||
|
|
||||||
@@ -17,3 +23,23 @@ type UserSpec struct {
|
|||||||
// Name of the entity.
|
// Name of the entity.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u UserSpec) Subject() (subject rbacv1.Subject) {
|
||||||
|
if u.Kind == ServiceAccountOwner {
|
||||||
|
splitName := strings.Split(u.Name, ":")
|
||||||
|
|
||||||
|
subject = rbacv1.Subject{
|
||||||
|
Kind: u.Kind.String(),
|
||||||
|
Name: splitName[len(splitName)-1],
|
||||||
|
Namespace: splitName[len(splitName)-2],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subject = rbacv1.Subject{
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Kind: u.Kind.String(),
|
||||||
|
Name: u.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ import (
|
|||||||
// +kubebuilder:object:generate=true
|
// +kubebuilder:object:generate=true
|
||||||
type UserListSpec []UserSpec
|
type UserListSpec []UserSpec
|
||||||
|
|
||||||
|
func (o *UserListSpec) Upsert(newUser UserSpec) {
|
||||||
|
users := *o
|
||||||
|
|
||||||
|
// Comparator consistent with ByKindName
|
||||||
|
less := func(a, b UserSpec) bool {
|
||||||
|
ak, bk := a.Kind.String(), b.Kind.String()
|
||||||
|
if ak != bk {
|
||||||
|
return ak < bk
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Name < b.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure sorted before binary search
|
||||||
|
sort.Sort(ByKindName(users))
|
||||||
|
|
||||||
|
// Find first index where users[i] >= newUser
|
||||||
|
idx := sort.Search(len(users), func(i int) bool {
|
||||||
|
return !less(users[i], newUser)
|
||||||
|
})
|
||||||
|
|
||||||
|
// In this case merging for duplicates makes little sense as the values are identical
|
||||||
|
if idx < len(users) && !less(newUser, users[idx]) && !less(users[idx], newUser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users = append(users, newUser)
|
||||||
|
sort.Sort(ByKindName(users))
|
||||||
|
*o = users
|
||||||
|
}
|
||||||
|
|
||||||
func (u UserListSpec) IsPresent(name string, groups []string) bool {
|
func (u UserListSpec) IsPresent(name string, groups []string) bool {
|
||||||
groupSet := make(map[string]struct{}, len(groups))
|
groupSet := make(map[string]struct{}, len(groups))
|
||||||
for _, g := range groups {
|
for _, g := range groups {
|
||||||
|
|||||||
105
pkg/api/users_test.go
Normal file
105
pkg/api/users_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserSpec_Subject_ServiceAccount(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in api.UserSpec
|
||||||
|
want rbacv1.Subject
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "system serviceaccount format",
|
||||||
|
in: api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "system:serviceaccount:capsule-system:capsule",
|
||||||
|
},
|
||||||
|
want: rbacv1.Subject{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Namespace: "capsule-system",
|
||||||
|
Name: "capsule",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minimal ns:name style (still splits from end)",
|
||||||
|
in: api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "ns:sa",
|
||||||
|
},
|
||||||
|
want: rbacv1.Subject{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Namespace: "ns",
|
||||||
|
Name: "sa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extra segments (uses last two)",
|
||||||
|
in: api.UserSpec{
|
||||||
|
Kind: api.ServiceAccountOwner,
|
||||||
|
Name: "a:b:c:d",
|
||||||
|
},
|
||||||
|
want: rbacv1.Subject{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Namespace: "c",
|
||||||
|
Name: "d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.in.Subject()
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("expected %#v, got %#v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserSpec_Subject_UserAndGroup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in api.UserSpec
|
||||||
|
want rbacv1.Subject
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user subject",
|
||||||
|
in: api.UserSpec{
|
||||||
|
Kind: api.UserOwner,
|
||||||
|
Name: "alice",
|
||||||
|
},
|
||||||
|
want: rbacv1.Subject{
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Kind: "User",
|
||||||
|
Name: "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "group subject",
|
||||||
|
in: api.UserSpec{
|
||||||
|
Kind: api.GroupOwner,
|
||||||
|
Name: "devops",
|
||||||
|
},
|
||||||
|
want: rbacv1.Subject{
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Kind: "Group",
|
||||||
|
Name: "devops",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.in.Subject()
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("expected %#v, got %#v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
@@ -22,25 +23,50 @@ type capsuleConfiguration struct {
|
|||||||
retrievalFn func() *capsulev1beta2.CapsuleConfiguration
|
retrievalFn func() *capsulev1beta2.CapsuleConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCapsuleConfiguration(ctx context.Context, client client.Client, name string) Configuration {
|
func DefaultCapsuleConfiguration() capsulev1beta2.CapsuleConfigurationSpec {
|
||||||
|
return capsulev1beta2.CapsuleConfigurationSpec{
|
||||||
|
Users: []capsuleapi.UserSpec{
|
||||||
|
{
|
||||||
|
Name: "projectcapsule.dev",
|
||||||
|
Kind: capsuleapi.GroupOwner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ForceTenantPrefix: false,
|
||||||
|
ProtectedNamespaceRegexpString: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCapsuleConfiguration(ctx context.Context, c client.Client, name string) Configuration {
|
||||||
return &capsuleConfiguration{retrievalFn: func() *capsulev1beta2.CapsuleConfiguration {
|
return &capsuleConfiguration{retrievalFn: func() *capsulev1beta2.CapsuleConfiguration {
|
||||||
config := &capsulev1beta2.CapsuleConfiguration{}
|
cfg := &capsulev1beta2.CapsuleConfiguration{}
|
||||||
|
key := types.NamespacedName{Name: name}
|
||||||
|
|
||||||
if err := client.Get(ctx, types.NamespacedName{Name: name}, config); err != nil {
|
if err := c.Get(ctx, key, cfg); err == nil {
|
||||||
if apierrors.IsNotFound(err) {
|
return cfg
|
||||||
return &capsulev1beta2.CapsuleConfiguration{
|
} else if !apierrors.IsNotFound(err) {
|
||||||
Spec: capsulev1beta2.CapsuleConfigurationSpec{
|
panic(errors.Wrap(err, "cannot retrieve Capsule configuration with name "+name))
|
||||||
Users: []capsuleapi.UserSpec{{Name: "projectcapsule.dev", Kind: capsuleapi.GroupOwner}},
|
|
||||||
ForceTenantPrefix: false,
|
|
||||||
ProtectedNamespaceRegexpString: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(errors.Wrap(err, "Cannot retrieve Capsule configuration with name "+name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
cfg = &capsulev1beta2.CapsuleConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: DefaultCapsuleConfiguration(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Create(ctx, cfg); err != nil {
|
||||||
|
if apierrors.IsAlreadyExists(err) {
|
||||||
|
if err := c.Get(ctx, key, cfg); err != nil {
|
||||||
|
panic(errors.Wrap(err, "configuration created concurrently but cannot be retrieved"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(errors.Wrap(err, "cannot create Capsule configuration with name "+name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +122,30 @@ func (c *capsuleConfiguration) UserNames() []string {
|
|||||||
return append(c.retrievalFn().Spec.UserNames, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.UserOwner, capsuleapi.ServiceAccountOwner})...)
|
return append(c.retrievalFn().Spec.UserNames, c.retrievalFn().Spec.Users.GetByKinds([]capsuleapi.OwnerKind{capsuleapi.UserOwner, capsuleapi.ServiceAccountOwner})...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *capsuleConfiguration) Users() capsuleapi.UserListSpec {
|
||||||
|
out := capsuleapi.UserListSpec{}
|
||||||
|
|
||||||
|
for _, user := range c.UserNames() {
|
||||||
|
out.Upsert(capsuleapi.UserSpec{
|
||||||
|
Kind: capsuleapi.UserOwner,
|
||||||
|
Name: user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range c.UserGroups() {
|
||||||
|
out.Upsert(capsuleapi.UserSpec{
|
||||||
|
Kind: capsuleapi.GroupOwner,
|
||||||
|
Name: group,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *capsuleConfiguration) GetUsersByStatus() capsuleapi.UserListSpec {
|
||||||
|
return c.retrievalFn().Status.Users
|
||||||
|
}
|
||||||
|
|
||||||
func (c *capsuleConfiguration) IgnoreUserWithGroups() []string {
|
func (c *capsuleConfiguration) IgnoreUserWithGroups() []string {
|
||||||
return c.retrievalFn().Spec.IgnoreUserWithGroups
|
return c.retrievalFn().Spec.IgnoreUserWithGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type Configuration interface {
|
|||||||
TenantCRDName() string
|
TenantCRDName() string
|
||||||
UserNames() []string
|
UserNames() []string
|
||||||
UserGroups() []string
|
UserGroups() []string
|
||||||
|
Users() capsuleapi.UserListSpec
|
||||||
|
GetUsersByStatus() capsuleapi.UserListSpec
|
||||||
IgnoreUserWithGroups() []string
|
IgnoreUserWithGroups() []string
|
||||||
ForbiddenUserNodeLabels() *capsuleapi.ForbiddenListSpec
|
ForbiddenUserNodeLabels() *capsuleapi.ForbiddenListSpec
|
||||||
ForbiddenUserNodeAnnotations() *capsuleapi.ForbiddenListSpec
|
ForbiddenUserNodeAnnotations() *capsuleapi.ForbiddenListSpec
|
||||||
|
|||||||
23
pkg/utils/hashes.go
Normal file
23
pkg/utils/hashes.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2020-2025 Project Capsule Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RoleBindingHashFunc(binding api.AdditionalRoleBindingsSpec) string {
|
||||||
|
h := fnv.New64a()
|
||||||
|
|
||||||
|
_, _ = h.Write([]byte(binding.ClusterRoleName))
|
||||||
|
|
||||||
|
for _, sub := range binding.Subjects {
|
||||||
|
_, _ = h.Write([]byte(sub.Kind + sub.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x", h.Sum64())
|
||||||
|
}
|
||||||
125
pkg/utils/hashes_test.go
Normal file
125
pkg/utils/hashes_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Copyright 2020-2025 Project Capsule Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoleBindingHashFunc_Deterministic(t *testing.T) {
|
||||||
|
b := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{Kind: "User", Name: "alice"},
|
||||||
|
{Kind: "Group", Name: "devops"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 := utils.RoleBindingHashFunc(b)
|
||||||
|
h2 := utils.RoleBindingHashFunc(b)
|
||||||
|
|
||||||
|
if h1 != h2 {
|
||||||
|
t.Fatalf("expected deterministic hash, got %q and %q", h1, h2)
|
||||||
|
}
|
||||||
|
if h1 == "" {
|
||||||
|
t.Fatalf("expected non-empty hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleBindingHashFunc_ChangesWhenClusterRoleChanges(t *testing.T) {
|
||||||
|
b1 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
|
||||||
|
}
|
||||||
|
b2 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "view",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 := utils.RoleBindingHashFunc(b1)
|
||||||
|
h2 := utils.RoleBindingHashFunc(b2)
|
||||||
|
|
||||||
|
if h1 == h2 {
|
||||||
|
t.Fatalf("expected different hashes when ClusterRoleName changes, got %q", h1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleBindingHashFunc_ChangesWhenSubjectKindChanges(t *testing.T) {
|
||||||
|
b1 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
|
||||||
|
}
|
||||||
|
b2 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "Group", Name: "alice"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 := utils.RoleBindingHashFunc(b1)
|
||||||
|
h2 := utils.RoleBindingHashFunc(b2)
|
||||||
|
|
||||||
|
if h1 == h2 {
|
||||||
|
t.Fatalf("expected different hashes when subject Kind changes, got %q", h1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleBindingHashFunc_ChangesWhenSubjectNameChanges(t *testing.T) {
|
||||||
|
b1 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}},
|
||||||
|
}
|
||||||
|
b2 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{{Kind: "User", Name: "bob"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 := utils.RoleBindingHashFunc(b1)
|
||||||
|
h2 := utils.RoleBindingHashFunc(b2)
|
||||||
|
|
||||||
|
if h1 == h2 {
|
||||||
|
t.Fatalf("expected different hashes when subject Name changes, got %q", h1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoleBindingHashFunc_EmptyInputsStillProduceHash(t *testing.T) {
|
||||||
|
b := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "",
|
||||||
|
Subjects: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
h := utils.RoleBindingHashFunc(b)
|
||||||
|
if h == "" {
|
||||||
|
t.Fatalf("expected non-empty hash even for empty input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{Kind: "User", Name: "alice"},
|
||||||
|
{Kind: "Group", Name: "devops"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b2 := api.AdditionalRoleBindingsSpec{
|
||||||
|
ClusterRoleName: "admin",
|
||||||
|
Subjects: []rbacv1.Subject{
|
||||||
|
{Kind: "Group", Name: "devops"},
|
||||||
|
{Kind: "User", Name: "alice"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 := utils.RoleBindingHashFunc(b1)
|
||||||
|
h2 := utils.RoleBindingHashFunc(b2)
|
||||||
|
|
||||||
|
if h1 == h2 {
|
||||||
|
t.Fatalf("expected different hashes when subject order changes (current behavior), got %q", h1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,6 @@ func GetTenantByOwnerreferences(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:nestif
|
|
||||||
func GetTenantByUserInfo(
|
func GetTenantByUserInfo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
c client.Client,
|
c client.Client,
|
||||||
@@ -104,14 +103,6 @@ func GetTenantByUserInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
tenants = append(tenants, saTntList.Items...)
|
tenants = append(tenants, saTntList.Items...)
|
||||||
|
|
||||||
if cfg.AllowServiceAccountPromotion() {
|
|
||||||
if tnt, err := users.ResolveServiceAccountActor(ctx, c, ns, username, cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if tnt != nil {
|
|
||||||
tenants = append(tenants, *tnt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group tenants.
|
// Group tenants.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetOwnersWithKinds(tenant *capsulev1beta2.Tenant) (owners []string) {
|
func GetOwnersWithKinds(tenant *capsulev1beta2.Tenant) (owners []string) {
|
||||||
for _, owner := range tenant.Spec.Owners {
|
for _, owner := range tenant.Status.Owners {
|
||||||
owners = append(owners, fmt.Sprintf("%s:%s", owner.Kind.String(), owner.Name))
|
owners = append(owners, fmt.Sprintf("%s:%s", owner.Kind.String(), owner.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||||
|
"github.com/projectcapsule/capsule/pkg/api"
|
||||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,8 +51,10 @@ func IsCapsuleUser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
capsuleUsers := cfg.GetUsersByStatus()
|
||||||
|
|
||||||
//nolint:modernize
|
//nolint:modernize
|
||||||
for _, group := range cfg.UserGroups() {
|
for _, group := range capsuleUsers.GetByKinds([]api.OwnerKind{api.GroupOwner}) {
|
||||||
if groupList.Find(group) {
|
if groupList.Find(group) {
|
||||||
if len(cfg.IgnoreUserWithGroups()) > 0 {
|
if len(cfg.IgnoreUserWithGroups()) > 0 {
|
||||||
for _, ignoreGroup := range cfg.IgnoreUserWithGroups() {
|
for _, ignoreGroup := range cfg.IgnoreUserWithGroups() {
|
||||||
@@ -65,7 +68,8 @@ func IsCapsuleUser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.UserNames()) > 0 && sets.New[string](cfg.UserNames()...).Has(user) {
|
users := capsuleUsers.GetByKinds([]api.OwnerKind{api.UserOwner})
|
||||||
|
if len(users) > 0 && sets.New[string](users...).Has(user) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user