diff --git a/.github/workflows/check-commit.yml b/.github/workflows/check-commit.yml index 7ae1ff9d5..6ddfe62a9 100644 --- a/.github/workflows/check-commit.yml +++ b/.github/workflows/check-commit.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@baa1b236f990293a1b2d94c19e41c2313a85e749 #v6.0.2 + - uses: wagoid/commitlint-github-action@a2bc521d745b1ba127ee2f8b02d6afaa4eed035c #v6.1.1 with: firstParent: true diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 518e3369f..81616f0d9 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -30,7 +30,7 @@ jobs: timeout-minutes: 5 continue-on-error: true - uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0 - - uses: anchore/sbom-action/download-syft@ab9d16d4b419c9d1a02df5213fa0ebe965ca5a57 + - uses: anchore/sbom-action/download-syft@61119d458adab75f756bc0b9e4bde25725f86a7a - name: Install Cosign uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0 - name: Run GoReleaser diff --git a/Makefile b/Makefile index 45346b28d..86cd1dd0d 100644 --- a/Makefile +++ b/Makefile @@ -194,14 +194,13 @@ ko-publish-all: ko-publish-capsule #################### CONTROLLER_GEN := $(shell pwd)/bin/controller-gen -CONTROLLER_GEN_VERSION := v0.15.0 +CONTROLLER_GEN_VERSION := v0.16.1 controller-gen: ## Download controller-gen locally if necessary. $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION)) GINKGO := $(shell pwd)/bin/ginkgo -GINGKO_VERSION := v2.17.2 ginkgo: ## Download ginkgo locally if necessary. - $(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo@$(GINGKO_VERSION)) + $(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo) CT := $(shell pwd)/bin/ct CT_VERSION := v3.10.1 @@ -277,7 +276,7 @@ e2e/%: ginkgo e2e-build/%: kind create cluster --wait=60s --name capsule --image=kindest/node:$* - make e2e-install + $(MAKE) e2e-install .PHONY: e2e-install e2e-install: e2e-load-image diff --git a/api/v1beta2/tenant_types.go b/api/v1beta2/tenant_types.go index 52e155136..bb67c3bde 100644 --- a/api/v1beta2/tenant_types.go +++ b/api/v1beta2/tenant_types.go @@ -43,7 +43,7 @@ type TenantSpec struct { // Specifies the allowed RuntimeClasses assigned to the Tenant. // Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses. // Optional. - RuntimeClasses *api.SelectorAllowedListSpec `json:"runtimeClasses,omitempty"` + RuntimeClasses *api.DefaultAllowedListSpec `json:"runtimeClasses,omitempty"` // Specifies the allowed priorityClasses assigned to the Tenant. // Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. // A default value can be specified, and all the Pod resources created will inherit the declared class. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index b64cd8728..f9feb05e3 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -755,7 +755,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { } if in.RuntimeClasses != nil { in, out := &in.RuntimeClasses, &out.RuntimeClasses - *out = new(api.SelectorAllowedListSpec) + *out = new(api.DefaultAllowedListSpec) (*in).DeepCopyInto(*out) } if in.PriorityClasses != nil { diff --git a/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml b/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml index c95f5c4dd..73ff15e4f 100644 --- a/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml +++ b/charts/capsule/crds/capsule.clastix.io_capsuleconfigurations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.1 name: capsuleconfigurations.capsule.clastix.io spec: group: capsule.clastix.io diff --git a/charts/capsule/crds/capsule.clastix.io_globaltenantresources.yaml b/charts/capsule/crds/capsule.clastix.io_globaltenantresources.yaml index 8ae724fa3..0c6a7981f 100644 --- a/charts/capsule/crds/capsule.clastix.io_globaltenantresources.yaml +++ b/charts/capsule/crds/capsule.clastix.io_globaltenantresources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.1 name: globaltenantresources.capsule.clastix.io spec: group: capsule.clastix.io diff --git a/charts/capsule/crds/capsule.clastix.io_tenantresources.yaml b/charts/capsule/crds/capsule.clastix.io_tenantresources.yaml index 4e48c0ef5..e8804f8fc 100644 --- a/charts/capsule/crds/capsule.clastix.io_tenantresources.yaml +++ b/charts/capsule/crds/capsule.clastix.io_tenantresources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.1 name: tenantresources.capsule.clastix.io spec: group: capsule.clastix.io diff --git a/charts/capsule/crds/capsule.clastix.io_tenants.yaml b/charts/capsule/crds/capsule.clastix.io_tenants.yaml index 64c6103d7..c2cade711 100644 --- a/charts/capsule/crds/capsule.clastix.io_tenants.yaml +++ b/charts/capsule/crds/capsule.clastix.io_tenants.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.1 name: tenants.capsule.clastix.io spec: group: capsule.clastix.io @@ -165,16 +165,12 @@ spec: description: |- Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames. - - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule. - - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant. - - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace. - Optional. enum: - Cluster @@ -190,7 +186,82 @@ spec: properties: items: items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LimitRangeSpec' + description: LimitRangeSpec defines a min/max usage limit for + resources that match on kind. + properties: + limits: + description: Limits is the list of LimitRangeItem objects + that are enforced. + items: + description: LimitRangeItem defines a min/max usage limit + for any resource that matches on kind. + properties: + default: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Default resource requirement limit value + by resource name if resource limit is omitted. + type: object + defaultRequest: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: DefaultRequest is the default resource + requirement request value by resource name if resource + request is omitted. + type: object + max: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Max usage constraints on this kind by + resource name. + type: object + maxLimitRequestRatio: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: MaxLimitRequestRatio if specified, the + named resource must have a request and limit that + are both non-zero where limit divided by request + is less than or equal to the enumerated value; this + represents the max burst for the named resource. + type: object + min: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Min usage constraints on this kind by + resource name. + type: object + type: + description: Type of resource that this limit applies + to. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - limits + type: object type: array type: object namespaceOptions: @@ -276,7 +347,6 @@ spec: If present, only traffic on the specified protocol AND port will be matched. x-kubernetes-int-or-string: true protocol: - default: TCP description: |- protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP. @@ -323,7 +393,6 @@ spec: namespaceSelector selects namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. - If podSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the namespaces selected by namespaceSelector. Otherwise it selects all pods in the namespaces selected by namespaceSelector. @@ -377,7 +446,6 @@ spec: podSelector is a label selector which selects pods. This field follows standard label selector semantics; if present but empty, it selects all pods. - If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the pods matching podSelector in the policy's own namespace. @@ -485,7 +553,6 @@ spec: namespaceSelector selects namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. - If podSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the namespaces selected by namespaceSelector. Otherwise it selects all pods in the namespaces selected by namespaceSelector. @@ -539,7 +606,6 @@ spec: podSelector is a label selector which selects pods. This field follows standard label selector semantics; if present but empty, it selects all pods. - If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the pods matching podSelector in the policy's own namespace. @@ -621,7 +687,6 @@ spec: If present, only traffic on the specified protocol AND port will be matched. x-kubernetes-int-or-string: true protocol: - default: TCP description: |- protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP. @@ -783,7 +848,72 @@ spec: properties: items: items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ResourceQuotaSpec' + description: ResourceQuotaSpec defines the desired hard limits + to enforce for Quota. + properties: + hard: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + hard is the set of desired hard limits for each named resource. + More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/ + type: object + scopeSelector: + description: |- + scopeSelector is also a collection of filters like scopes that must match each object tracked by a quota + but expressed using ScopeSelectorOperator in combination with possible values. + For a resource to match, both scopes AND scopeSelector (if specified in spec), must be matched. + properties: + matchExpressions: + description: A list of scope selector requirements by + scope of the resources. + items: + description: |- + A scoped-resource selector requirement is a selector that contains values, a scope name, and an operator + that relates the scope name and values. + properties: + operator: + description: |- + Represents a scope's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + type: string + scopeName: + description: The name of the scope that the selector + applies to. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - operator + - scopeName + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + scopes: + description: |- + A collection of filters that must match each object tracked by a quota. + If not specified, the quota matches all objects. + items: + description: A ResourceQuotaScope defines a filter that + must match each object tracked by a quota + type: string + type: array + x-kubernetes-list-type: atomic + type: object type: array scope: default: Tenant @@ -1114,16 +1244,12 @@ spec: description: |- Defines the scope of hostname collision check performed when Tenant Owners create Ingress with allowed hostnames. - - Cluster: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces managed by Capsule. - - Tenant: disallow the creation of an Ingress if the pair hostname and path is already used across the Namespaces of the Tenant. - - Namespace: disallow the creation of an Ingress if the pair hostname and path is already used in the Ingress Namespace. - Optional. enum: - Cluster @@ -1139,7 +1265,82 @@ spec: properties: items: items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0LimitRangeSpec' + description: LimitRangeSpec defines a min/max usage limit for + resources that match on kind. + properties: + limits: + description: Limits is the list of LimitRangeItem objects + that are enforced. + items: + description: LimitRangeItem defines a min/max usage limit + for any resource that matches on kind. + properties: + default: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Default resource requirement limit value + by resource name if resource limit is omitted. + type: object + defaultRequest: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: DefaultRequest is the default resource + requirement request value by resource name if resource + request is omitted. + type: object + max: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Max usage constraints on this kind by + resource name. + type: object + maxLimitRequestRatio: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: MaxLimitRequestRatio if specified, the + named resource must have a request and limit that + are both non-zero where limit divided by request + is less than or equal to the enumerated value; this + represents the max burst for the named resource. + type: object + min: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Min usage constraints on this kind by + resource name. + type: object + type: + description: Type of resource that this limit applies + to. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - limits + type: object type: array type: object namespaceOptions: @@ -1247,7 +1448,6 @@ spec: If present, only traffic on the specified protocol AND port will be matched. x-kubernetes-int-or-string: true protocol: - default: TCP description: |- protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP. @@ -1294,7 +1494,6 @@ spec: namespaceSelector selects namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. - If podSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the namespaces selected by namespaceSelector. Otherwise it selects all pods in the namespaces selected by namespaceSelector. @@ -1348,7 +1547,6 @@ spec: podSelector is a label selector which selects pods. This field follows standard label selector semantics; if present but empty, it selects all pods. - If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the pods matching podSelector in the policy's own namespace. @@ -1456,7 +1654,6 @@ spec: namespaceSelector selects namespaces using cluster-scoped labels. This field follows standard label selector semantics; if present but empty, it selects all namespaces. - If podSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the namespaces selected by namespaceSelector. Otherwise it selects all pods in the namespaces selected by namespaceSelector. @@ -1510,7 +1707,6 @@ spec: podSelector is a label selector which selects pods. This field follows standard label selector semantics; if present but empty, it selects all pods. - If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects the pods matching podSelector in the Namespaces selected by NamespaceSelector. Otherwise it selects the pods matching podSelector in the policy's own namespace. @@ -1592,7 +1788,6 @@ spec: If present, only traffic on the specified protocol AND port will be matched. x-kubernetes-int-or-string: true protocol: - default: TCP description: |- protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. If not specified, this field defaults to TCP. @@ -1835,7 +2030,72 @@ spec: properties: items: items: - $ref: '#/definitions/k8s.io~1api~1core~1v1~0ResourceQuotaSpec' + description: ResourceQuotaSpec defines the desired hard limits + to enforce for Quota. + properties: + hard: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + hard is the set of desired hard limits for each named resource. + More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/ + type: object + scopeSelector: + description: |- + scopeSelector is also a collection of filters like scopes that must match each object tracked by a quota + but expressed using ScopeSelectorOperator in combination with possible values. + For a resource to match, both scopes AND scopeSelector (if specified in spec), must be matched. + properties: + matchExpressions: + description: A list of scope selector requirements by + scope of the resources. + items: + description: |- + A scoped-resource selector requirement is a selector that contains values, a scope name, and an operator + that relates the scope name and values. + properties: + operator: + description: |- + Represents a scope's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + type: string + scopeName: + description: The name of the scope that the selector + applies to. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - operator + - scopeName + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + scopes: + description: |- + A collection of filters that must match each object tracked by a quota. + If not specified, the quota matches all objects. + items: + description: A ResourceQuotaScope defines a filter that + must match each object tracked by a quota + type: string + type: array + x-kubernetes-list-type: atomic + type: object type: array scope: default: Tenant @@ -1859,6 +2119,8 @@ spec: type: array allowedRegex: type: string + default: + type: string matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/e2e/pod_runtime_class_test.go b/e2e/pod_runtime_class_test.go index 259b35bc6..d3c3096f2 100644 --- a/e2e/pod_runtime_class_test.go +++ b/e2e/pod_runtime_class_test.go @@ -33,14 +33,17 @@ var _ = Describe("enforcing a Runtime Class", func() { Kind: "User", }, }, - RuntimeClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{"legacy"}, - Regex: "^hardened-.*$", - }, - LabelSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "env": "customers", + RuntimeClasses: &api.DefaultAllowedListSpec{ + Default: "default-runtime", + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{"legacy"}, + Regex: "^hardened-.*$", + }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customers", + }, }, }, }, @@ -221,4 +224,49 @@ var _ = Describe("enforcing a Runtime Class", func() { } }) + It("should auto assign the default", func() { + ns := NewNamespace("rc-default") + + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + + runtime := &nodev1.RuntimeClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-runtime", + }, + Handler: "custom-handler", + } + Expect(k8sClient.Create(context.TODO(), runtime)).Should(Succeed()) + defer func() { + Expect(k8sClient.Delete(context.TODO(), runtime)).Should(Succeed()) + }() + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rc-default", + Namespace: ns.Name, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + }, + } + + cs := ownerClient(tnt.Spec.Owners[0]) + + var createdPod *corev1.Pod + + EventuallyCreation(func() (err error) { + createdPod, err = cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), &pod, metav1.CreateOptions{}) + + return err + }).Should(Succeed()) + + Expect(createdPod.Spec.RuntimeClassName).NotTo(BeNil()) + _, err := Equal(createdPod.Spec.RuntimeClassName).Match(tnt.Spec.RuntimeClasses.Default) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/go.mod b/go.mod index 225e6143f..b02a18793 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/onsi/ginkgo/v2 v2.20.0 github.com/onsi/gomega v1.34.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.20.1 + github.com/prometheus/client_golang v1.20.2 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/valyala/fasttemplate v1.2.2 @@ -61,7 +61,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sys v0.24.0 // indirect @@ -75,7 +75,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34 // indirect + k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 24195ecba..bc21d48bf 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -125,10 +126,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= -github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8= github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -195,6 +196,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -275,6 +278,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34 h1:/amS69DLm09mtbFtN3+LyygSFohnYGMseF8iv+2zulg= k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34/go.mod h1:G0W3eI9gG219NHRq3h5uQaRBl4pj4ZpwzRP5ti8y770= +k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9 h1:y+4z/s0h3R97P/o/098DSjlpyNpHzGirNPlTL+GHdqY= +k8s.io/kube-openapi v0.0.0-20240822171749-76de80e0abd9/go.mod h1:s4yb9FXajAVNRnxSB5Ckpr/oq2LP4mKSMWeZDVppd30= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI= diff --git a/pkg/webhook/defaults/pods.go b/pkg/webhook/defaults/pods.go index bd21f51af..2ca8dcdea 100644 --- a/pkg/webhook/defaults/pods.go +++ b/pkg/webhook/defaults/pods.go @@ -11,43 +11,91 @@ import ( corev1 "k8s.io/api/core/v1" schedulev1 "k8s.io/api/scheduling/v1" "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/webhook/utils" ) func mutatePodDefaults(ctx context.Context, req admission.Request, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, namespace string) *admission.Response { - var err error - - pod := &corev1.Pod{} - if err = decoder.Decode(req, pod); err != nil { + var pod corev1.Pod + if err := decoder.Decode(req, &pod); err != nil { return utils.ErroredResponse(err) } pod.SetNamespace(namespace) - var tnt *capsulev1beta2.Tenant + tnt, tErr := utils.TenantByStatusNamespace(ctx, c, pod.Namespace) + if tErr != nil { + return utils.ErroredResponse(tErr) + } else if tnt == nil { + return nil + } + + var err error - tnt, err = utils.TenantByStatusNamespace(ctx, c, pod.Namespace) - if err != nil { - return utils.ErroredResponse(err) + pcMutated, pcErr := handlePriorityClassDefault(ctx, c, tnt.Spec.PriorityClasses, &pod) + if pcErr != nil { + return utils.ErroredResponse(pcErr) + } else if pcMutated { + defer func() { + if err == nil { + recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Priority Class %s to %s/%s", tnt.Spec.PriorityClasses.Default, pod.Namespace, pod.Name) + } + }() } - if tnt == nil { + rcMutated := handleRuntimeClassDefault(tnt.Spec.RuntimeClasses, &pod) + if rcMutated { + defer func() { + if err == nil { + recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Runtime Class %s to %s/%s", tnt.Spec.RuntimeClasses.Default, pod.Namespace, pod.Name) + } + }() + } + + if !rcMutated && !pcMutated { return nil } - allowed := tnt.Spec.PriorityClasses + var marshaled []byte + + if marshaled, err = json.Marshal(pod); err != nil { + return utils.ErroredResponse(err) + } + + return ptr.To(admission.PatchResponseFromRaw(req.Object.Raw, marshaled)) +} +func handleRuntimeClassDefault(allowed *api.DefaultAllowedListSpec, pod *corev1.Pod) (mutated bool) { if allowed == nil || allowed.Default == "" { - return nil + return false } - priorityClassPod := pod.Spec.PriorityClassName + runtimeClass := pod.Spec.RuntimeClassName + + switch { + case allowed.Default == "": + return false + case runtimeClass != nil && *runtimeClass != "": + return false + case runtimeClass != nil && *runtimeClass != allowed.Default: + return false + default: + pod.Spec.RuntimeClassName = &allowed.Default + + return true + } +} + +func handlePriorityClassDefault(ctx context.Context, c client.Client, allowed *api.DefaultAllowedListSpec, pod *corev1.Pod) (mutated bool, err error) { + if allowed == nil || allowed.Default == "" { + return false, nil + } - var mutate bool + priorityClassPod := pod.Spec.PriorityClassName var cpc *schedulev1.PriorityClass // PriorityClass name is empty, if no GlobalDefault is set and no PriorityClass was given on pod @@ -55,35 +103,24 @@ func mutatePodDefaults(ctx context.Context, req admission.Request, c client.Clie cpc, err = utils.GetPriorityClassByName(ctx, c, priorityClassPod) // Should not happen, since API already checks if PC present if err != nil { - response := admission.Denied(NewPriorityClassError(priorityClassPod, err).Error()) - - return &response + return false, NewPriorityClassError(priorityClassPod, err) } } else { - mutate = true + mutated = true } - if mutate = mutate || (utils.IsDefaultPriorityClass(cpc) && cpc.GetName() != allowed.Default); !mutate { - return nil + if mutated = mutated || (utils.IsDefaultPriorityClass(cpc) && cpc.GetName() != allowed.Default); !mutated { + return false, nil } pc, err := utils.GetPriorityClassByName(ctx, c, allowed.Default) if err != nil { - return utils.ErroredResponse(fmt.Errorf("failed to assign tenant default Priority Class: %w", err)) + return false, fmt.Errorf("failed to assign tenant default Priority Class: %w", err) } pod.Spec.PreemptionPolicy = pc.PreemptionPolicy pod.Spec.Priority = &pc.Value pod.Spec.PriorityClassName = pc.Name - // Marshal Pod - marshaled, err := json.Marshal(pod) - if err != nil { - return utils.ErroredResponse(err) - } - - recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Priority Class %s to %s/%s", allowed.Default, pod.Namespace, pod.Name) - - response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled) - return &response + return true, nil } diff --git a/pkg/webhook/pod/runtimeclass.go b/pkg/webhook/pod/runtimeclass.go index 37b9cab16..5bbd976a6 100644 --- a/pkg/webhook/pod/runtimeclass.go +++ b/pkg/webhook/pod/runtimeclass.go @@ -88,8 +88,8 @@ func (h *runtimeClass) validate(ctx context.Context, c client.Client, decoder ad case allowed == nil: // Enforcement is not in place, skipping it at all return nil - case len(runtimeClassName) == 0: - // We don't have to force Pod to specify a RuntimeClass + case len(runtimeClassName) == 0 || runtimeClassName == allowed.Default: + // Delegating mutating webhook to specify a default RuntimeClass return nil case !allowed.MatchSelectByName(class): recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenRuntimeClass", "Pod %s/%s is using Runtime Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, runtimeClassName) diff --git a/pkg/webhook/pod/runtimeclass_errors.go b/pkg/webhook/pod/runtimeclass_errors.go index 1dce1999a..cae23663d 100644 --- a/pkg/webhook/pod/runtimeclass_errors.go +++ b/pkg/webhook/pod/runtimeclass_errors.go @@ -12,10 +12,10 @@ import ( type podRuntimeClassForbiddenError struct { runtimeClassName string - spec api.SelectorAllowedListSpec + spec api.DefaultAllowedListSpec } -func NewPodRuntimeClassForbidden(runtimeClassName string, spec api.SelectorAllowedListSpec) error { +func NewPodRuntimeClassForbidden(runtimeClassName string, spec api.DefaultAllowedListSpec) error { return &podRuntimeClassForbiddenError{ runtimeClassName: runtimeClassName, spec: spec, @@ -25,5 +25,5 @@ func NewPodRuntimeClassForbidden(runtimeClassName string, spec api.SelectorAllow func (f podRuntimeClassForbiddenError) Error() (err string) { err = fmt.Sprintf("Pod Runtime Class %s is forbidden for the current Tenant: ", f.runtimeClassName) - return utils.AllowedValuesErrorMessage(f.spec, err) + return utils.DefaultAllowedValuesErrorMessage(f.spec, err) } diff --git a/pkg/webhook/utils/error.go b/pkg/webhook/utils/error.go index a24036ff8..509983986 100644 --- a/pkg/webhook/utils/error.go +++ b/pkg/webhook/utils/error.go @@ -20,10 +20,6 @@ func ErroredResponse(err error) *admission.Response { } func DefaultAllowedValuesErrorMessage(allowed api.DefaultAllowedListSpec, err string) string { - return AllowedValuesErrorMessage(allowed.SelectorAllowedListSpec, err) -} - -func AllowedValuesErrorMessage(allowed api.SelectorAllowedListSpec, err string) string { var extra []string if len(allowed.Exact) > 0 { extra = append(extra, fmt.Sprintf("use one from the following list (%s)", strings.Join(allowed.Exact, ", ")))