feat: add action type for rules and regexp cache (#1957)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update codecov/codecov-action action to v5.5.2 (#1783)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 43a17d6 (#1781)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module k8s.io/dynamic-resource-allocation to v0.34.3 (#1786)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module k8s.io/apiextensions-apiserver to v0.34.3 (#1785)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(controller): allow no spaces in template references (#1789)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(controller): allow no spaces in template references

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(controller): allow no spaces in template references

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update securego/gosec action to v2.22.11 (#1788)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1791)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update k8s.io/utils digest to 61b37f7 (#1801)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(controller): template concurrency (#1802)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1795)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency kubernetes-sigs/kind to v0.31.0 (#1796)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update kubernetes packages to v0.35.0 (#1797)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module k8s.io/dynamic-resource-allocation to v0.35.0 (#1798)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.0 (#1799)

* chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.0

* chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update k8s.io/utils digest to 98d557b (#1803)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1793)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module github.com/onsi/ginkgo/v2 to v2.27.3 (#1776)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to f67ec12 (#1790)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency google/ko to v0.18.1 (#1792)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module github.com/onsi/gomega to v1.38.3 (#1777)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module sigs.k8s.io/cluster-api to v1.12.1 (#1784)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update k8s.io/utils digest to 383b50a (#1804)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/stale digest to a21a081 (#1808)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: adjust makefile and releaser for kubernetes 1.35 (#1809)

* chore: adjust makefile and releaser for kubernetes 1.35

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore: adjust makefile and releaser for kubernetes 1.35

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1807)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update k8s.io/utils digest to 718f0e5 (#1806)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to a930d0a (#1805)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update helm release kube-prometheus-stack to v80.8.2 (#1810)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* 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>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update helm release kube-prometheus-stack to v80.9.2 (#1812)

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1814)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update amannn/action-semantic-pull-request digest to 71b07ef (#1815)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to fd448f7 (#1816)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: use cert-manager certificates by default (#1818)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(helm): use cert-manager certificates by default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: move dependencies to trackable resources

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update helm release kube-prometheus-stack to v80.13.2 (#1817)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency prometheus-operator/prometheus-operator to v0.87.1 (#1820)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency golangci/golangci-lint to v2.8.0 (#1823)

* chore(deps): update dependency golangci/golangci-lint to v2.8.0

* chore(deps): update dependency golangci/golangci-lint to v2.8.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore(deps): update dependency golangci/golangci-lint to v2.8.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore(deps): update dependency golangci/golangci-lint to v2.8.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update helm release kube-prometheus-stack to v80.13.3 (#1827)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module github.com/onsi/gomega to v1.39.0 (#1826)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency prometheus-operator/prometheus-operator to v0.88.0 (#1828)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module github.com/onsi/ginkgo/v2 to v2.27.4 (#1825)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 0b82b0b (#1824)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update k8s.io/utils digest to 914a6e7 (#1822)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1830)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/stale digest to d6f8a33 (#1843)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update amannn/action-semantic-pull-request digest to b439535 (#1835)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/checkout action to v6.0.2 (#1845)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1847)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1848)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ruleset api(#1844)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency alessandrojcm/commitlint-pre-commit-hook to v9.24.0 (#1833)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to deef08a (#1836)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency adrienverge/yamllint to v1.38.0 (#1832)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to b2ff80d (#1821)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to f985be5 (#1850)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 5620efe (#1852)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1851)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1837)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1856)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to 8aac4e4 (#1855)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: diverse performance improvements (#1861)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/stale digest to dcd2b94 (#1857)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(docs): update home in chart.yaml (#1864)

* fix(docs): update home in chart.yaml

Signed-off-by: sandert-k8s <sandert98@gmail.com>

* fix: linter

Signed-off-by: sandert-k8s <sandert98@gmail.com>

---------

Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/stale digest to b5d41d4 (#1866)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update zgosalvez/github-actions-ensure-sha-pinned-actions action to v5 (#1865)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update amannn/action-semantic-pull-request digest to ac7e3fc (#1871)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 6d473d3 (#1860)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 17ae174 (#1876)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update github/codeql-action digest to 0ec47d0 (#1858)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to 57aae52 (#1882)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update actions/stale digest to db5d06a (#1886)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to a0a6512 (#1887)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to e22c389 (#1888)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update anchore/sbom-action digest to f0d33c1 (#1893)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(workflows): bump trivy action to 0.35.0 (#1896)

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(webhook): adapt to controller-runtime breaking change in newwebhookmanagedby (#1898)

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add e2e openshift support (#1894)

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

* feat: add e2e openshift support

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1873)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1859)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency alessandrojcm/commitlint-pre-commit-hook to v9.25.0 (#1907)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): set renovate minimum release age to 14 days (#1908)

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: add lfx status badges (#1909)

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* ci: pin slsa provenance workflow (#1903)

Signed-off-by: Akash Kumar <meakash7902@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency b1nary-gr0up/nwa to v0.7.8 (#1906)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1900)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: upstream enterprise preview (#1841)

feat: upstream enterprise preview

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: bump supported version (#1918)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: bump makefile 1.35

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: release workflows  (#1919)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: bump makefile 1.35

* fix: release workflows

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: reuse webhookport from values (#1927)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: reuse webhookport from values

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: consider webhooks.service.port

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: consider webhooks.service.port

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: correct helm values schema for webservcie ports (#1928)

Signed-off-by: bakito <github@bakito.ch>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: correct helm docs for webhook service port schema (#1929)

Signed-off-by: bakito <github@bakito.ch>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update capsule-proxy docker tag to v0.12.0 (#1846)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency prometheus-operator/prometheus-operator to v0.91.0 (#1849)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update dependency grafana/grafana-operator to v5.22.2 (#1819)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: use release age for all managers except helm  (#1931)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: use release age for all managers except helm

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update capsule-proxy docker tag to v0.13.1 (#1932)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add observedgeneration to status object of all crds (#1930)

* feat: add observedgeneration to status object of all crds

Signed-off-by: sandert-k8s <sandert98@gmail.com>

* chore(api): rename rulestatusspec to rulestatusstatus

Signed-off-by: sandert-k8s <sandert98@gmail.com>

---------

Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: promote sander tervoert as maintainer (#1933)

* chore: promote sander tervoert as maintainer

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore: promote sander tervoert as maintainer

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore: promote sander tervoert as maintainer

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore: promote sander tervoert as maintainer

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: promote sander tervoert as maintainer (#1939)

Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* ci: add goreleaser dry run (#1936)

Signed-off-by: Alan <alan747271363-art@users.noreply.github.com>
Co-authored-by: Alan <alan747271363-art@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module gomodules.xyz/jsonpatch/v2 to v3 (#1917)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update helm release kube-prometheus-stack to v85 (#1914)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module go.uber.org/zap to v1.28.0 (#1904)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: avoid rejection when users are classified as administrators (#1941)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: avoid rejection when users are classified as administrators

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update capsule-proxy docker tag to v0.13.2 (#1942)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module sigs.k8s.io/gateway-api to v1.5.1 (#1878)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: typo in ruleset description crd (#1944)

Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add tenant list to status of capsuleconfiguration (#1935)

Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: correct tls reconciler and add tenantowners (#1946)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: tls controller

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add tenantowner tenant status reference

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: tlsreconciler only patches cabundles

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: refactor logger usage

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: tlsreconciler only patches cabundles

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: tlsreconciler only patches cabundles

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: fix typo (#1945)

* chore: typo in ruleset description crd

Signed-off-by: sandert-k8s <sandert98@gmail.com>

* chore: fix typo

Signed-off-by: sandert-k8s <sandert98@gmail.com>

---------

Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: allow managed metadata defined per tenant (#1947)

* fix: allow managed metadata defined per tenant

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: allow managed metadata defined per tenant

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: action type

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: preserve ca-bundles injected from external providers  (#1948)

* fix: preserve ca-bundles injected from external providers  (#1948)

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix(deps): update module sigs.k8s.io/cluster-api to v1.13.2 (#1874)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(deps): bump golang 1.26.4  (#1949)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(deps): bump golang 1.26.4

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* feat(deps): bump golang 1.26.4

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update capsule-proxy docker tag to v0.13.3 (#1950)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore(deps): update all-ci-updates (#1902)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: best effort patch reconciling status  (#1952)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: best effort patch reconciling status

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: use different match strategy for truthy and match (#1953)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: best effort patch reconciling status

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: use different match strategy for truthy and match

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* progress

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add registry

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add registry

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* chore: update all gihub actions, use digest versioning and remove obsolete docs-lint workflow (#1955)

Signed-off-by: bakito <github@bakito.ch>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: translate serviceaccounts to type serviceaccount not user (#1956)

* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: translate serviceaccounts to type serviceaccount not user

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add improved registry enforcement

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Signed-off-by: Hristo Hristov <me@hhristov.info>
Signed-off-by: sandert-k8s <sandert98@gmail.com>
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
Signed-off-by: bakito <github@bakito.ch>
Signed-off-by: Alan <alan747271363-art@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hristo Hristov <me@hhristov.info>
Co-authored-by: Sander Tervoert <32864332+sandert-k8s@users.noreply.github.com>
Co-authored-by: Akash Kumar <91385321+AkashKumar7902@users.noreply.github.com>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
Co-authored-by: Marc Brugger <github@bakito.ch>
Co-authored-by: alan747271363-art <alan747271363@gmail.com>
Co-authored-by: Alan <alan747271363-art@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
40 changed files with 2750 additions and 1281 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
// TenantSpec defines the desired state of Tenant.
@@ -39,7 +40,7 @@ type TenantSpec struct {
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []rbac.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
ImagePullPolicies []rules.ImagePullPolicySpec `json:"imagePullPolicies,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. Optional.
PriorityClasses *api.AllowedListSpec `json:"priorityClasses,omitempty"`
}
+2 -1
View File
@@ -10,6 +10,7 @@ package v1beta1
import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@@ -331,7 +332,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]api.ImagePullPolicySpec, len(*in))
*out = make([]rules.ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
if in.PriorityClasses != nil {
+13 -9
View File
@@ -6,8 +6,8 @@ package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
@@ -16,9 +16,14 @@ type RuleStatusStatus struct {
// ObservedGeneration is the most recent generation the controller has observed.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Managed Enforcement properties per Namespace (aggregated from rules)
//+optional
Rule api.NamespaceRuleBodyNamespace `json:"rule,omitzero"`
// Deprecated: use Rules.
// Rule contains a legacy flattened view and cannot fully represent action-aware rules.
// +optional
Rule rules.NamespaceRuleBodyNamespace `json:"rule,omitzero"`
// Rules contains the effective namespace rules after tenant rule selection.
// Order is preserved from the originating Tenant rules.
// +optional
Rules []*rules.NamespaceRuleBodyNamespace `json:"rules,omitempty"`
// Conditions
Conditions meta.ConditionList `json:"conditions"`
}
@@ -26,15 +31,14 @@ type RuleStatusStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Ready Status"
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Ready Message"
type RuleStatus struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
// +optional
Spec []*api.NamespaceRuleBodyNamespace `json:"spec,omitzero"`
Spec []*rules.NamespaceRuleBodyNamespace `json:"spec,omitzero"`
// +optional
Status RuleStatusStatus `json:"status,omitzero"`
+2 -2
View File
@@ -6,9 +6,9 @@ package v1beta2
import (
k8stypes "k8s.io/apimachinery/pkg/types"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
// +kubebuilder:validation:Enum=Cordoned;Active;Terminating
@@ -72,7 +72,7 @@ type TenantStatusRuleStatusItem struct {
type TenantStatusNamespaceEnforcement struct {
// Registries which are allowed within this namespace
Registries []api.OCIRegistry `json:"registry,omitempty"`
Registries []rules.OCIRegistry `json:"registry,omitempty"`
}
type TenantStatusNamespaceMetadata struct {
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
@@ -32,7 +33,7 @@ type TenantSpec struct {
//
// Read More: https://projectcapsule.dev/docs/tenants/rules/
//+optional
Rules []*api.NamespaceRuleBodyTenant `json:"rules,omitzero"`
Rules []*rules.NamespaceRuleBodyTenant `json:"rules,omitzero"`
// Specifies the owners of the Tenant.
// Optional
@@ -96,7 +97,7 @@ type TenantSpec struct {
// Deprecated: Use Enforcement.Registries instead
//
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
ImagePullPolicies []rules.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
+18 -6
View File
@@ -11,6 +11,7 @@ import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/admission"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
"github.com/projectcapsule/capsule/pkg/template"
@@ -1575,11 +1576,11 @@ func (in *RuleStatus) DeepCopyInto(out *RuleStatus) {
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Spec != nil {
in, out := &in.Spec, &out.Spec
*out = make([]*api.NamespaceRuleBodyNamespace, len(*in))
*out = make([]*rules.NamespaceRuleBodyNamespace, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(api.NamespaceRuleBodyNamespace)
*out = new(rules.NamespaceRuleBodyNamespace)
(*in).DeepCopyInto(*out)
}
}
@@ -1641,6 +1642,17 @@ func (in *RuleStatusList) DeepCopyObject() runtime.Object {
func (in *RuleStatusStatus) DeepCopyInto(out *RuleStatusStatus) {
*out = *in
in.Rule.DeepCopyInto(&out.Rule)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]*rules.NamespaceRuleBodyNamespace, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(rules.NamespaceRuleBodyNamespace)
(*in).DeepCopyInto(*out)
}
}
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make(meta.ConditionList, len(*in))
@@ -2108,11 +2120,11 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
in.Permissions.DeepCopyInto(&out.Permissions)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]*api.NamespaceRuleBodyTenant, len(*in))
*out = make([]*rules.NamespaceRuleBodyTenant, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(api.NamespaceRuleBodyTenant)
*out = new(rules.NamespaceRuleBodyTenant)
(*in).DeepCopyInto(*out)
}
}
@@ -2188,7 +2200,7 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]api.ImagePullPolicySpec, len(*in))
*out = make([]rules.ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
@@ -2263,7 +2275,7 @@ func (in *TenantStatusNamespaceEnforcement) DeepCopyInto(out *TenantStatusNamesp
*out = *in
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]api.OCIRegistry, len(*in))
*out = make([]rules.OCIRegistry, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -15,10 +15,14 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
- description: Ready Status
jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- description: Ready Message
jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Message
type: string
name: v1beta2
schema:
openAPIV3Schema:
@@ -48,12 +52,31 @@ spec:
enforce:
description: Enforcement for given rule
properties:
action:
default: deny
description: |-
Declare the action being performed on the enforcement rule:
deny: On match, deny admission request
allow: On match, allowed admission request
audit: On match, audit (post event) of admission request
enum:
- allow
- deny
- audit
type: string
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
exp:
description: Expression used to evaluate regex
type: string
negate:
default: false
description: Negate regular Expression
type: boolean
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
@@ -63,8 +86,10 @@ spec:
type: string
type: array
url:
description: OCI Registry endpoint, is treated as regular
expression.
description: |-
Deprecated: Use exp field
OCI Registry endpoint, is treated as regular expression.
type: string
validation:
default:
@@ -77,8 +102,6 @@ spec:
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
@@ -151,18 +174,38 @@ spec:
format: int64
type: integer
rule:
description: Managed Enforcement properties per Namespace (aggregated
from rules)
description: |-
Deprecated: use Rules.
Rule contains a legacy flattened view and cannot fully represent action-aware rules.
properties:
enforce:
description: Enforcement for given rule
properties:
action:
default: deny
description: |-
Declare the action being performed on the enforcement rule:
deny: On match, deny admission request
allow: On match, allowed admission request
audit: On match, audit (post event) of admission request
enum:
- allow
- deny
- audit
type: string
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
exp:
description: Expression used to evaluate regex
type: string
negate:
default: false
description: Negate regular Expression
type: boolean
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
@@ -172,8 +215,10 @@ spec:
type: string
type: array
url:
description: OCI Registry endpoint, is treated as regular
expression.
description: |-
Deprecated: Use exp field
OCI Registry endpoint, is treated as regular expression.
type: string
validation:
default:
@@ -186,15 +231,81 @@ spec:
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
type: object
rules:
description: |-
Rules contains the effective namespace rules after tenant rule selection.
Order is preserved from the originating Tenant rules.
items:
description: For future implementation where users might manage
RuleStatus CRs themselves
properties:
enforce:
description: Enforcement for given rule
properties:
action:
default: deny
description: |-
Declare the action being performed on the enforcement rule:
deny: On match, deny admission request
allow: On match, allowed admission request
audit: On match, audit (post event) of admission request
enum:
- allow
- deny
- audit
type: string
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
exp:
description: Expression used to evaluate regex
type: string
negate:
default: false
description: Negate regular Expression
type: boolean
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
items:
description: PullPolicy describes a policy for if/when
to pull a container image
type: string
type: array
url:
description: |-
Deprecated: Use exp field
OCI Registry endpoint, is treated as regular expression.
type: string
validation:
default:
- pod/images
- pod/volumes
description: Requesting Resources
items:
enum:
- pod/images
- pod/volumes
type: string
type: array
type: object
type: array
type: object
type: object
type: array
required:
- conditions
type: object
required:
- metadata
type: object
served: true
storage: true
@@ -2503,12 +2503,31 @@ spec:
enforce:
description: Enforcement for given rule
properties:
action:
default: deny
description: |-
Declare the action being performed on the enforcement rule:
deny: On match, deny admission request
allow: On match, allowed admission request
audit: On match, audit (post event) of admission request
enum:
- allow
- deny
- audit
type: string
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
exp:
description: Expression used to evaluate regex
type: string
negate:
default: false
description: Negate regular Expression
type: boolean
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
@@ -2518,8 +2537,10 @@ spec:
type: string
type: array
url:
description: OCI Registry endpoint, is treated as
regular expression.
description: |-
Deprecated: Use exp field
OCI Registry endpoint, is treated as regular expression.
type: string
validation:
default:
@@ -2532,8 +2553,6 @@ spec:
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
@@ -3091,6 +3110,13 @@ spec:
description: Registries which are allowed within this namespace
items:
properties:
exp:
description: Expression used to evaluate regex
type: string
negate:
default: false
description: Negate regular Expression
type: boolean
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
@@ -3100,8 +3126,10 @@ spec:
type: string
type: array
url:
description: OCI Registry endpoint, is treated as
regular expression.
description: |-
Deprecated: Use exp field
OCI Registry endpoint, is treated as regular expression.
type: string
validation:
default:
@@ -3114,8 +3142,6 @@ spec:
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
+3 -1
View File
@@ -503,7 +503,8 @@ func main() {
// Initialize Caches
impersonationCache := cache.NewImpersonationCache()
registryCache := cache.NewRegistryRuleSetCache()
regexCache := cache.NewRegexCache()
registryCache := cache.NewRegistryRuleSetCache(regexCache)
customQuotaQuantityCache := cache.NewQuantityCache[string]()
jsonPathCache := cache.NewJSONPathCache()
targetsCache := cache.NewCompiledTargetsCache[string]()
@@ -749,6 +750,7 @@ func main() {
RegistryCache: registryCache,
JSONPathCache: jsonPathCache,
TargetsCache: targetsCache,
RegexCache: regexCache,
}
if err := localInvalidator.SetupWithManager(manager, controllerConfig); err != nil {
+467 -378
View File
@@ -1,4 +1,4 @@
// Copyright 2020-2023 Project Capsule Authors.
// Copyright 2020-2026 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
@@ -21,82 +21,141 @@ import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rules", "images", "registry"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-rule-registry",
Labels: map[string]string{
"env": "e2e",
var _ = Describe("enforcing container registry namespace rules", Ordered, Label("tenant", "rules", "images", "registry"), func() {
const ownerName = "e2e-rules-registry"
var tnt *capsulev1beta2.Tenant
newTenant := func() *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-rule-registry",
Labels: map[string]string{
"env": "e2e",
},
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: "e2e-rules-registry",
Kind: "User",
Spec: capsulev1beta2.TenantSpec{
Owners: rbac.OwnerListSpec{
{
CoreOwnerSpec: rbac.CoreOwnerSpec{
UserSpec: rbac.UserSpec{
Name: ownerName,
Kind: "User",
},
},
},
},
},
Rules: []*api.NamespaceRuleBodyTenant{
{
NamespaceRuleBodyNamespace: api.NamespaceRuleBodyNamespace{
Enforce: api.NamespaceRuleEnforceBody{
Registries: []api.OCIRegistry{
// Global: allow any registry, but require PullPolicy Always (images+volumes)
{
Registry: ".*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
Rules: []*rules.NamespaceRuleBodyTenant{
{
NamespaceRuleBodyNamespace: rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Registries: []rules.OCIRegistry{
{
Registry: "harbor/.*",
Validation: []rules.RegistryValidationTarget{
rules.ValidateImages,
rules.ValidateVolumes,
},
},
Policy: []corev1.PullPolicy{corev1.PullAlways},
},
// More specific harbor rule (no policy override => should NOT remove Always restriction)
{
Registry: "harbor/.*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
},
},
},
{
NamespaceRuleBodyNamespace: rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeDeny,
Registries: []rules.OCIRegistry{
{
Registry: "harbor/customer/.*",
Policy: []corev1.PullPolicy{
corev1.PullNever,
},
Validation: []rules.RegistryValidationTarget{
rules.ValidateImages,
rules.ValidateVolumes,
},
},
},
},
},
},
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"environment": "prod",
},
},
NamespaceRuleBodyNamespace: rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Registries: []rules.OCIRegistry{
{
Registry: "harbor/customer/prod-image/.*",
Validation: []rules.RegistryValidationTarget{
rules.ValidateImages,
rules.ValidateVolumes,
},
},
},
},
},
},
{
NamespaceRuleBodyNamespace: rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAudit,
Registries: []rules.OCIRegistry{
{
Registry: "audit/.*",
Validation: []rules.RegistryValidationTarget{
rules.ValidateImages,
rules.ValidateVolumes,
},
},
},
},
},
},
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"negate": "true",
},
},
NamespaceRuleBodyNamespace: rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeDeny,
Registries: []rules.OCIRegistry{
{
RegExpression: api.RegExpression{
Expression: "trusted/.*",
Negate: true,
},
Validation: []rules.RegistryValidationTarget{
rules.ValidateImages,
},
},
},
},
},
},
},
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"environment": "prod",
},
},
NamespaceRuleBodyNamespace: api.NamespaceRuleBodyNamespace{
Enforce: api.NamespaceRuleEnforceBody{
Registries: []api.OCIRegistry{
// Prod-only special-case
{
Registry: "harbor/production-image/.*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
},
Policy: []corev1.PullPolicy{corev1.PullAlways},
},
},
},
},
},
},
},
}
}
// ---- Small local helpers (keep e2e readable) ----
type expectedStatusRule struct {
action rules.ActionType
expressions []string
negated []bool
}
expectNamespaceStatusRegistries := func(nsName string, want []string) {
expectNamespaceStatusRules := func(nsName string, want []expectedStatusRule) {
Eventually(func(g Gomega) {
nsStatus := &capsulev1beta2.RuleStatus{}
g.Expect(k8sClient.Get(
@@ -105,12 +164,23 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
nsStatus,
)).To(Succeed())
got := make([]string, 0, len(nsStatus.Status.Rule.Enforce.Registries))
for _, r := range nsStatus.Status.Rule.Enforce.Registries {
got = append(got, r.Registry)
}
g.Expect(nsStatus.Status.Rules).To(HaveLen(len(want)))
g.Expect(got).To(Equal(want))
for i, expected := range want {
gotRule := nsStatus.Status.Rules[i]
g.Expect(gotRule).NotTo(BeNil())
g.Expect(gotRule.Enforce.Action).To(Equal(expected.action))
g.Expect(gotRule.Enforce.Registries).To(HaveLen(len(expected.expressions)))
for j, expectedExpression := range expected.expressions {
expr := gotRule.Enforce.Registries[j].Expression()
g.Expect(expr.Expression).To(Equal(expectedExpression))
if len(expected.negated) > j {
g.Expect(expr.Negate).To(Equal(expected.negated[j]))
}
}
}
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
@@ -122,13 +192,13 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
}
Eventually(func() error {
// unique name per attempt to avoid AlreadyExists
p := base.DeepCopy()
p.Name = fmt.Sprintf("%s-%d", baseName, int(time.Now().UnixNano()%1e6))
p.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), p, metav1.CreateOptions{})
if err == nil {
_ = cs.CoreV1().Pods(nsName).Delete(context.Background(), p.Name, metav1.DeleteOptions{})
return fmt.Errorf("expected create to be denied, but it succeeded")
}
@@ -137,11 +207,12 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
}
msg := err.Error()
for _, s := range substrings {
if !strings.Contains(msg, s) {
return fmt.Errorf("expected error to contain %q, got: %s", s, msg)
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
@@ -149,13 +220,93 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
createPodAndExpectAllowed := func(cs kubernetes.Interface, nsName string, pod *corev1.Pod) {
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).Should(Succeed())
}
updatePodAndExpectDenied := func(cs kubernetes.Interface, nsName string, podName string, mutate func(*corev1.Pod), substrings ...string) {
Eventually(func() error {
pod, err := cs.CoreV1().Pods(nsName).Get(context.Background(), podName, metav1.GetOptions{})
if err != nil {
return err
}
mutate(pod)
_, err = cs.CoreV1().Pods(nsName).Update(context.Background(), pod, metav1.UpdateOptions{})
if err == nil {
return fmt.Errorf("expected update to be denied, but it succeeded")
}
msg := err.Error()
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
restrictedPod := func(name string, image string, pullPolicy corev1.PullPolicy) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{
Name: "c",
Image: image,
ImagePullPolicy: pullPolicy,
SecurityContext: restrictedContainerSecurityContext(),
},
},
},
}
}
expectAuditEvent := func(cs kubernetes.Interface, nsName string, podName string, substrings ...string) {
Eventually(func() error {
events, err := cs.CoreV1().Events(nsName).List(context.Background(), metav1.ListOptions{})
if err != nil {
return err
}
for _, event := range events.Items {
if event.InvolvedObject.Name != podName {
continue
}
msg := event.Message
matched := true
for _, substring := range substrings {
if !strings.Contains(msg, substring) {
matched = false
break
}
}
if matched {
return nil
}
}
return fmt.Errorf("expected audit event for pod %q containing %q", podName, substrings)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
JustBeforeEach(func() {
tnt = newTenant()
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
@@ -166,34 +317,149 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
EventuallyDeletion(tnt)
})
It("aggregates enforcement rules into NamespaceStatus for a non-prod namespace", func() {
It("stores matching tenant rules as independent status rule blocks", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
expectNamespaceStatusRules(ns.GetName(), []expectedStatusRule{
{
action: rules.ActionTypeAllow,
expressions: []string{"harbor/.*"},
},
{
action: rules.ActionTypeDeny,
expressions: []string{"harbor/customer/.*"},
},
{
action: rules.ActionTypeAudit,
expressions: []string{"audit/.*"},
},
})
})
It("stores namespace-selector matched rules as additional independent status rule blocks", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
expectNamespaceStatusRules(ns.GetName(), []expectedStatusRule{
{
action: rules.ActionTypeAllow,
expressions: []string{"harbor/.*"},
},
{
action: rules.ActionTypeDeny,
expressions: []string{"harbor/customer/.*"},
},
{
action: rules.ActionTypeAllow,
expressions: []string{"harbor/customer/prod-image/.*"},
},
{
action: rules.ActionTypeAudit,
expressions: []string{"audit/.*"},
},
})
})
It("stores namespace-selector matched negated regex rules as independent status rule blocks", func() {
ns := NewNamespace("", map[string]string{
"negate": "true",
meta.TenantLabel: tnt.GetName(),
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
expectNamespaceStatusRules(ns.GetName(), []expectedStatusRule{
{
action: rules.ActionTypeAllow,
expressions: []string{"harbor/.*"},
},
{
action: rules.ActionTypeDeny,
expressions: []string{"harbor/customer/.*"},
},
{
action: rules.ActionTypeAudit,
expressions: []string{"audit/.*"},
},
{
action: rules.ActionTypeDeny,
expressions: []string{"trusted/.*"},
negated: []bool{true},
},
})
})
It("allows a broad matching allow rule", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
// Non-prod: should include only the global rule body (two registries in order)
expectNamespaceStatusRegistries(ns.GetName(), []string{
".*",
"harbor/.*",
})
pod := restrictedPod("harbor-allowed", "harbor/platform/app:1", corev1.PullIfNotPresent)
// Sanity: we can still create a trivial pod with explicit Always (since global allows all registries)
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "sanity"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "gcr.io/google_containers/pause-amd64:3.0", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
})
It("aggregates enforcement rules into NamespaceStatus for a prod namespace", func() {
It("denies a later more specific deny rule even when an earlier broad allow rule matched", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := restrictedPod("customer-denied", "harbor/customer/app:1", corev1.PullIfNotPresent)
createPodAndExpectDenied(cs, ns.Name, pod,
"containers[0]",
"harbor/customer/app:1",
"denied",
"harbor/customer/.*",
)
})
It("denies an update when the new image matches a later specific deny rule", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := restrictedPod("update-to-denied", "harbor/platform/app:1", corev1.PullIfNotPresent)
createPodAndExpectAllowed(cs, ns.Name, pod)
updatePodAndExpectDenied(cs, ns.Name, pod.Name, func(pod *corev1.Pod) {
pod.Spec.Containers[0].Image = "harbor/customer/adad:1"
},
"containers[0]",
"harbor/customer/adad:1",
"denied",
"harbor/customer/.*",
)
})
It("allows a later more specific allow rule to override an earlier deny rule in a selected namespace", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
meta.TenantLabel: tnt.GetName(),
@@ -204,70 +470,66 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
// Prod: should include global + prod rule (3 registries in order)
expectNamespaceStatusRegistries(ns.GetName(), []string{
".*",
"harbor/.*",
"harbor/production-image/.*",
})
denied := restrictedPod("prod-customer-denied", "harbor/customer/other-image/app:1", corev1.PullIfNotPresent)
createPodAndExpectDenied(cs, ns.Name, denied,
"containers[0]",
"harbor/customer/other-image/app:1",
"denied",
"harbor/customer/.*",
)
// Sanity allow with Always
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-sanity"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
allowed := restrictedPod("prod-customer-allowed", "harbor/customer/prod-image/app:1", corev1.PullIfNotPresent)
createPodAndExpectAllowed(cs, ns.Name, allowed)
})
It("denies a container image when pullPolicy is not explicitly set under restriction (dev)", func() {
ns := NewNamespace("",
map[string]string{
meta.TenantLabel: tnt.GetName(),
},
)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
// No ImagePullPolicy set => "" => should be denied because global rule restricts policy to Always
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "no-pullpolicy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "gcr.io/google_containers/pause-amd64:3.0"},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"uses pullPolicy=IfNotPresent",
"not allowed",
"allowed: Always",
)
})
It("denies a harbor image with pullPolicy IfNotPresent because global Always must still apply (dev)", func() {
It("audits a matching image by allowing admission and emitting an event", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := restrictedPod("audit-allowed", "audit/team/app:1", corev1.PullIfNotPresent)
createPodAndExpectAllowed(cs, ns.Name, pod)
expectAuditEvent(cs, ns.Name, pod.Name,
"matched audit registry rule",
"audit/.*",
)
})
It("evaluates init containers with the same multi-rule action semantics", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "harbor-wrong-policy"},
ObjectMeta: metav1.ObjectMeta{
Name: "init-denied",
},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
InitContainers: []corev1.Container{
{
Name: "init",
Image: "harbor/customer/init:1",
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
},
},
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
Image: "harbor/platform/app:1",
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
},
@@ -275,98 +537,35 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"pullPolicy=IfNotPresent",
"not allowed",
"allowed:",
)
})
It("allows a harbor image with pullPolicy Always (dev)", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "harbor-always"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
SecurityContext: restrictedContainerSecurityContext(),
},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
})
It("denies initContainers when they violate policy (dev) and includes the correct location in the message", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "init-deny"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
InitContainers: []corev1.Container{
{
Name: "init",
Image: "harbor/some-team/init:1",
ImagePullPolicy: corev1.PullIfNotPresent, // should be denied
SecurityContext: restrictedContainerSecurityContext(),
},
},
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
SecurityContext: restrictedContainerSecurityContext(),
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"initContainers[0]",
"pullPolicy=IfNotPresent",
"allowed:",
"harbor/customer/init:1",
"denied",
"harbor/customer/.*",
)
})
It("denies volume image pullPolicy if not allowed (dev)", Label("skip-on-openshift"), func() {
It("evaluates image volumes with the same multi-rule action semantics", Label("skip-on-openshift"), func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "volume-deny"},
ObjectMeta: metav1.ObjectMeta{
Name: "volume-denied",
},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
Image: "harbor/platform/app:1",
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
},
},
@@ -375,176 +574,7 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:1",
PullPolicy: corev1.PullIfNotPresent, // should be denied
},
},
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"volumes[0](imgvol)",
"pullPolicy=IfNotPresent",
"allowed:",
)
})
It("allows prod-specific image only with Always, still enforcing global policy", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
// Wrong policy => denied
bad := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-bad"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullNever, SecurityContext: restrictedContainerSecurityContext()},
},
},
}
createPodAndExpectDenied(cs, ns.Name, bad,
"pullPolicy=Never",
"allowed:",
)
// Correct policy => allowed
good := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-good"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullAlways, SecurityContext: restrictedContainerSecurityContext()},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, good)
})
It("denies adding an ephemeral container with wrong pullPolicy on UPDATE", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
expectNamespaceStatusRegistries(ns.GetName(), []string{".*", "harbor/.*"})
cleanupRBAC := GrantEphemeralContainersUpdate(ns.Name, tnt.Spec.Owners[0].UserSpec.Name)
defer cleanupRBAC()
// Create an allowed pod
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "base"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
SecurityContext: restrictedContainerSecurityContext(),
},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
// Now attempt to add an ephemeral container with IfNotPresent (should be denied)
ephem := corev1.EphemeralContainer{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "debug",
Image: "harbor/some-team/debug:1",
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
},
}
Eventually(func() error {
// Must use the ephemeralcontainers subresource
cur, err := cs.CoreV1().Pods(ns.Name).Get(context.Background(), pod.Name, metav1.GetOptions{})
if err != nil {
return err
}
cur.Spec.EphemeralContainers = append(cur.Spec.EphemeralContainers, ephem)
_, err = cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(
context.Background(),
cur.Name,
cur,
metav1.UpdateOptions{},
)
if err == nil {
return fmt.Errorf("expected UpdateEphemeralContainers to be denied, but it succeeded")
}
msg := err.Error()
// Your webhook reports "ephemeralContainers[0]" location
if !strings.Contains(msg, "ephemeralContainers") || !strings.Contains(msg, "pullPolicy=IfNotPresent") {
return fmt.Errorf("unexpected error: %v", err)
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("denies a pod when volume image reference changes to a disallowed pullPolicy (recreate)", Label("skip-on-openshift"), func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
expectNamespaceStatusRegistries(ns.GetName(), []string{".*", "harbor/.*"})
pod1 := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "vol-ok"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways, SecurityContext: restrictedContainerSecurityContext()},
},
Volumes: []corev1.Volume{
{
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:1",
PullPolicy: corev1.PullAlways,
},
},
},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod1)
pod2 := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "vol-bad"},
Spec: corev1.PodSpec{
SecurityContext: nobodyPodSecurityContext(),
Containers: []corev1.Container{
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways, SecurityContext: restrictedContainerSecurityContext()},
},
Volumes: []corev1.Volume{
{
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:2",
Reference: "harbor/customer/volume:1",
PullPolicy: corev1.PullIfNotPresent,
},
},
@@ -553,11 +583,70 @@ var _ = Describe("enforcing a Container Registry", Ordered, Label("tenant", "rul
},
}
createPodAndExpectDenied(cs, ns.Name, pod2,
createPodAndExpectDenied(cs, ns.Name, pod,
"volumes[0](imgvol)",
"pullPolicy=IfNotPresent",
"allowed:",
"harbor/customer/volume:1",
"denied",
"harbor/customer/.*",
)
})
It("denies adding an ephemeral container when it matches the later specific deny rule", func() {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt.GetName(),
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())
cleanupRBAC := GrantEphemeralContainersUpdate(ns.Name, tnt.Spec.Owners[0].UserSpec.Name)
defer cleanupRBAC()
pod := restrictedPod("base", "harbor/platform/app:1", corev1.PullIfNotPresent)
createPodAndExpectAllowed(cs, ns.Name, pod)
ephemeral := corev1.EphemeralContainer{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "debug",
Image: "harbor/customer/debug:1",
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
},
}
Eventually(func() error {
current, err := cs.CoreV1().Pods(ns.Name).Get(context.Background(), pod.Name, metav1.GetOptions{})
if err != nil {
return err
}
current.Spec.EphemeralContainers = append(current.Spec.EphemeralContainers, ephemeral)
_, err = cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(
context.Background(),
current.Name,
current,
metav1.UpdateOptions{},
)
if err == nil {
return fmt.Errorf("expected UpdateEphemeralContainers to be denied, but it succeeded")
}
msg := err.Error()
for _, substring := range []string{
"ephemeralContainers[0]",
"harbor/customer/debug:1",
"denied",
"harbor/customer/.*",
} {
if !strings.Contains(msg, substring) {
return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
})
+8 -8
View File
@@ -20,9 +20,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
var serviceAccountPromotionClusterRoles = []string{
@@ -266,10 +266,10 @@ var _ = Describe("Promoting ServiceAccounts", Ordered, Label("config", "permissi
},
},
Spec: capsulev1beta2.TenantSpec{
Rules: []*api.NamespaceRuleBodyTenant{
Rules: []*rules.NamespaceRuleBodyTenant{
{
Permissions: api.NamespaceRulePermissionBody{
Promotions: []*api.NamespaceRulePromotionRule{
Permissions: rules.NamespaceRulePermissionBody{
Promotions: []*rules.NamespaceRulePromotionRule{
{
ClusterRoles: []string{"view"},
},
@@ -290,8 +290,8 @@ var _ = Describe("Promoting ServiceAccounts", Ordered, Label("config", "permissi
"environment": "prod",
},
},
Permissions: api.NamespaceRulePermissionBody{
Promotions: []*api.NamespaceRulePromotionRule{
Permissions: rules.NamespaceRulePermissionBody{
Promotions: []*rules.NamespaceRulePromotionRule{
{
ClusterRoles: []string{"prod-view"},
},
@@ -312,8 +312,8 @@ var _ = Describe("Promoting ServiceAccounts", Ordered, Label("config", "permissi
"environment": "dev",
},
},
Permissions: api.NamespaceRulePermissionBody{
Promotions: []*api.NamespaceRulePromotionRule{
Permissions: rules.NamespaceRulePermissionBody{
Promotions: []*rules.NamespaceRulePromotionRule{
{
ClusterRoles: []string{"dev-view"},
},
+2 -2
View File
@@ -13,9 +13,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
var _ = Describe("enforcing some defined ImagePullPolicy", Ordered, Label("tenant", "pods", "images", "policy"), func() {
@@ -37,7 +37,7 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Ordered, Label("tenan
},
},
},
ImagePullPolicies: []api.ImagePullPolicySpec{"Always", "IfNotPresent"},
ImagePullPolicies: []rules.ImagePullPolicySpec{"Always", "IfNotPresent"},
},
}
+2 -2
View File
@@ -13,9 +13,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
var _ = Describe("enforcing a defined ImagePullPolicy", Ordered, Label("tenant", "pods", "images", "policy"), func() {
@@ -37,7 +37,7 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Ordered, Label("tenant",
},
},
},
ImagePullPolicies: []api.ImagePullPolicySpec{"Always"},
ImagePullPolicies: []rules.ImagePullPolicySpec{"Always"},
},
}
@@ -90,16 +90,15 @@ spec:
name: alice
rules:
- enforce:
action: "deny"
registries:
- url: "harbor/.*"
policy:
- "Never"
- enforce:
action: "allow"
registries:
- url: "custom/.*"
- url: "harbor/customer/.*"
policy:
- "Never"
- namespaceSelector:
matchExpressions:
- key: env
@@ -107,6 +106,7 @@ spec:
values:
- "prod"
enforce:
action: "allow"
registries:
- url: "harbor/v2/customer-registry/prod-image/.*"
policy:
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package cache
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"strings"
"sync"
"github.com/projectcapsule/capsule/pkg/api"
)
type CompiledRegex struct {
ID string
Expression string
Negate bool
RE *regexp.Regexp
}
func (r *CompiledRegex) MatchString(value string) bool {
if r == nil || r.RE == nil {
return false
}
matched := r.RE.MatchString(value)
if r.Negate {
return !matched
}
return matched
}
type RegexCache struct {
mu sync.RWMutex
re map[string]*CompiledRegex
}
func NewRegexCache() *RegexCache {
return &RegexCache{
re: make(map[string]*CompiledRegex),
}
}
func (c *RegexCache) GetOrCompile(expr api.RegExpression) (*CompiledRegex, bool, error) {
if c == nil {
return nil, false, fmt.Errorf("regex cache is nil")
}
expression := strings.TrimSpace(expr.Expression)
if expression == "" {
return nil, false, fmt.Errorf("regex expression must not be empty")
}
id := HashRegex(expr)
c.mu.RLock()
compiled := c.re[id]
c.mu.RUnlock()
if compiled != nil {
return compiled, true, nil
}
re, err := regexp.Compile(expression)
if err != nil {
return nil, false, fmt.Errorf("invalid regex expression %q: %w", expression, err)
}
built := &CompiledRegex{
ID: id,
Expression: expression,
Negate: expr.Negate,
RE: re,
}
c.mu.Lock()
defer c.mu.Unlock()
if c.re == nil {
c.re = make(map[string]*CompiledRegex)
}
if compiled = c.re[id]; compiled != nil {
return compiled, true, nil
}
c.re[id] = built
return built, false, nil
}
func (c *RegexCache) Has(id string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.re[id]
return ok
}
func (c *RegexCache) Stats() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.re)
}
func (c *RegexCache) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.re = make(map[string]*CompiledRegex)
}
func HashRegex(expr api.RegExpression) string {
var b strings.Builder
b.WriteString(strings.TrimSpace(expr.Expression))
b.WriteString("\x1f")
if expr.Negate {
b.WriteString("1")
} else {
b.WriteString("0")
}
sum := sha256.Sum256([]byte(b.String()))
return hex.EncodeToString(sum[:])
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package cache
import (
"testing"
"github.com/projectcapsule/capsule/pkg/api"
)
func TestRegexCache_GetOrCompile(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expression api.RegExpression
value string
wantMatch bool
wantErr bool
wantCached bool
wantEntries int
}{
{
name: "compile matching regex",
expression: api.RegExpression{
Expression: `^ghcr\.io/projectcapsule/.*`,
},
value: "ghcr.io/projectcapsule/capsule:latest",
wantMatch: true,
wantErr: false,
wantCached: false,
wantEntries: 1,
},
{
name: "compile non matching regex",
expression: api.RegExpression{
Expression: `^ghcr\.io/projectcapsule/.*`,
},
value: "docker.io/library/nginx:latest",
wantMatch: false,
wantErr: false,
wantCached: false,
wantEntries: 1,
},
{
name: "compile negated matching regex",
expression: api.RegExpression{
Expression: `^ghcr\.io/projectcapsule/.*`,
Negate: true,
},
value: "ghcr.io/projectcapsule/capsule:latest",
wantMatch: false,
wantErr: false,
wantCached: false,
wantEntries: 1,
},
{
name: "compile negated non matching regex",
expression: api.RegExpression{
Expression: `^ghcr\.io/projectcapsule/.*`,
Negate: true,
},
value: "docker.io/library/nginx:latest",
wantMatch: true,
wantErr: false,
wantCached: false,
wantEntries: 1,
},
{
name: "reject empty expression",
expression: api.RegExpression{
Expression: "",
},
value: "ghcr.io/projectcapsule/capsule:latest",
wantErr: true,
wantEntries: 0,
},
{
name: "reject invalid regex",
expression: api.RegExpression{
Expression: `[`,
},
value: "ghcr.io/projectcapsule/capsule:latest",
wantErr: true,
wantEntries: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := NewRegexCache()
compiled, fromCache, err := c.GetOrCompile(tt.expression)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if compiled != nil {
t.Fatalf("expected nil compiled regex on error, got %#v", compiled)
}
if got := c.Stats(); got != tt.wantEntries {
t.Fatalf("expected %d cache entries, got %d", tt.wantEntries, got)
}
return
}
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if compiled == nil {
t.Fatal("expected compiled regex, got nil")
}
if fromCache != tt.wantCached {
t.Fatalf("expected fromCache=%t, got %t", tt.wantCached, fromCache)
}
if got := compiled.MatchString(tt.value); got != tt.wantMatch {
t.Fatalf("expected match=%t, got %t", tt.wantMatch, got)
}
if got := c.Stats(); got != tt.wantEntries {
t.Fatalf("expected %d cache entries, got %d", tt.wantEntries, got)
}
if !c.Has(compiled.ID) {
t.Fatalf("expected cache to contain regex id %q", compiled.ID)
}
})
}
}
func TestRegexCache_GetOrCompile_ReusesCachedRegex(t *testing.T) {
t.Parallel()
c := NewRegexCache()
expr := api.RegExpression{
Expression: `^ghcr\.io/projectcapsule/.*`,
}
first, fromCache, err := c.GetOrCompile(expr)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if fromCache {
t.Fatal("expected first lookup to build regex, got cache hit")
}
second, fromCache, err := c.GetOrCompile(expr)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !fromCache {
t.Fatal("expected second lookup to hit cache")
}
if first != second {
t.Fatal("expected cached regex pointer to be reused")
}
if got := c.Stats(); got != 1 {
t.Fatalf("expected 1 cache entry, got %d", got)
}
}
func TestRegexCache_HashRegex_UsesNegate(t *testing.T) {
t.Parallel()
positive := HashRegex(api.RegExpression{
Expression: `^ghcr\.io/.*`,
})
negative := HashRegex(api.RegExpression{
Expression: `^ghcr\.io/.*`,
Negate: true,
})
if positive == negative {
t.Fatal("expected different hashes for negated and non-negated expressions")
}
}
func TestRegexCache_Reset(t *testing.T) {
t.Parallel()
c := NewRegexCache()
compiled, _, err := c.GetOrCompile(api.RegExpression{
Expression: `^ghcr\.io/.*`,
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got := c.Stats(); got != 1 {
t.Fatalf("expected 1 cache entry, got %d", got)
}
c.Reset()
if got := c.Stats(); got != 0 {
t.Fatalf("expected 0 cache entries after reset, got %d", got)
}
if c.Has(compiled.ID) {
t.Fatalf("expected regex id %q to be removed after reset", compiled.ID)
}
}
func TestCompiledRegex_MatchString_NilSafe(t *testing.T) {
t.Parallel()
var compiled *CompiledRegex
if compiled.MatchString("ghcr.io/projectcapsule/capsule:latest") {
t.Fatal("expected nil compiled regex to return false")
}
compiled = &CompiledRegex{}
if compiled.MatchString("ghcr.io/projectcapsule/capsule:latest") {
t.Fatal("expected compiled regex with nil RE to return false")
}
}
+201 -28
View File
@@ -7,7 +7,6 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"sort"
"strings"
"sync"
@@ -15,6 +14,7 @@ import (
corev1 "k8s.io/api/core/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
type RuleSet struct {
@@ -25,29 +25,62 @@ type RuleSet struct {
}
type CompiledRule struct {
Registry string
RE *regexp.Regexp
Expression api.RegExpression
RegexID string
AllowedPolicy map[corev1.PullPolicy]struct{} // nil/empty => allow any
ValidateImages bool
ValidateVolumes bool
}
func (r *CompiledRule) AllowsPullPolicy(pullPolicy corev1.PullPolicy) bool {
if len(r.AllowedPolicy) == 0 {
return true
}
_, ok := r.AllowedPolicy[pullPolicy]
return ok
}
func (r *CompiledRule) MatchesTarget(target rules.RegistryValidationTarget) bool {
switch target {
case rules.ValidateImages:
return r.ValidateImages
case rules.ValidateVolumes:
return r.ValidateVolumes
default:
return false
}
}
type RegistryRuleSetCache struct {
regexCache *RegexCache
mu sync.RWMutex
rs map[string]*RuleSet
}
func NewRegistryRuleSetCache() *RegistryRuleSetCache {
func NewRegistryRuleSetCache(regexCache *RegexCache) *RegistryRuleSetCache {
if regexCache == nil {
regexCache = NewRegexCache()
}
return &RegistryRuleSetCache{
rs: make(map[string]*RuleSet),
regexCache: regexCache,
rs: make(map[string]*RuleSet),
}
}
func (c *RegistryRuleSetCache) GetOrBuild(specRules []api.OCIRegistry) (rs *RuleSet, fromCache bool, err error) {
func (c *RegistryRuleSetCache) GetOrBuild(specRules []rules.OCIRegistry) (rs *RuleSet, fromCache bool, err error) {
if len(specRules) == 0 {
return nil, false, nil
}
if c == nil {
return nil, false, fmt.Errorf("registry rule set cache is nil")
}
id := c.HashRules(specRules)
c.mu.RLock()
@@ -58,13 +91,12 @@ func (c *RegistryRuleSetCache) GetOrBuild(specRules []api.OCIRegistry) (rs *Rule
return rs, true, nil
}
// Build outside locks (regex compile etc.)
built, err := buildRuleSet(id, specRules)
// Build outside locks. Regex compilation is delegated to RegexCache.
built, err := c.buildRuleSet(id, specRules)
if err != nil {
return nil, false, err
}
// Insert with double-check
c.mu.Lock()
defer c.mu.Unlock()
@@ -72,7 +104,6 @@ func (c *RegistryRuleSetCache) GetOrBuild(specRules []api.OCIRegistry) (rs *Rule
c.rs = make(map[string]*RuleSet)
}
// Another goroutine may have inserted meanwhile
if rs = c.rs[id]; rs != nil {
return rs, true, nil
}
@@ -82,7 +113,115 @@ func (c *RegistryRuleSetCache) GetOrBuild(specRules []api.OCIRegistry) (rs *Rule
return built, false, nil
}
// Match matches a reference against target, regex and pullPolicy.
// Admission deny/allow/audit evaluation should usually use MatchReference instead,
// because it needs to distinguish "regex matched but pullPolicy is forbidden" from
// "regex did not match".
func (c *RegistryRuleSetCache) Match(
specRules []rules.OCIRegistry,
reference string,
pullPolicy corev1.PullPolicy,
target rules.RegistryValidationTarget,
) (*CompiledRule, error) {
rs, _, err := c.GetOrBuild(specRules)
if err != nil {
return nil, err
}
if rs == nil {
return nil, nil
}
return c.MatchRuleSet(rs, reference, pullPolicy, target)
}
// MatchRuleSet matches a reference against target, regex and pullPolicy.
func (c *RegistryRuleSetCache) MatchRuleSet(
rs *RuleSet,
reference string,
pullPolicy corev1.PullPolicy,
target rules.RegistryValidationTarget,
) (*CompiledRule, error) {
if c == nil {
return nil, fmt.Errorf("registry rule set cache is nil")
}
if c.regexCache == nil {
return nil, fmt.Errorf("regex cache is nil")
}
if rs == nil {
return nil, nil
}
for i := range rs.Compiled {
rule := &rs.Compiled[i]
if !rule.MatchesTarget(target) {
continue
}
if !rule.AllowsPullPolicy(pullPolicy) {
continue
}
compiled, _, err := c.regexCache.GetOrCompile(rule.Expression)
if err != nil {
return nil, err
}
if compiled.MatchString(reference) {
return rule, nil
}
}
return nil, nil
}
// MatchReference matches a reference against target and regex only.
// It intentionally does not check pullPolicy.
func (c *RegistryRuleSetCache) MatchReference(
rs *RuleSet,
reference string,
target rules.RegistryValidationTarget,
) (*CompiledRule, error) {
if c == nil {
return nil, fmt.Errorf("registry rule set cache is nil")
}
if c.regexCache == nil {
return nil, fmt.Errorf("regex cache is nil")
}
if rs == nil {
return nil, nil
}
for i := range rs.Compiled {
rule := &rs.Compiled[i]
if !rule.MatchesTarget(target) {
continue
}
compiled, _, err := c.regexCache.GetOrCompile(rule.Expression)
if err != nil {
return nil, err
}
if compiled.MatchString(reference) {
return rule, nil
}
}
return nil, nil
}
func (c *RegistryRuleSetCache) Stats() int {
if c == nil {
return 0
}
c.mu.RLock()
defer c.mu.RUnlock()
@@ -91,6 +230,10 @@ func (c *RegistryRuleSetCache) Stats() int {
// activeIDs: set of ids currently referenced by RuleStatus in cluster.
func (c *RegistryRuleSetCache) PruneActive(activeIDs map[string]struct{}) int {
if c == nil {
return 0
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -109,10 +252,10 @@ func (c *RegistryRuleSetCache) PruneActive(activeIDs map[string]struct{}) int {
return removed
}
func (c *RegistryRuleSetCache) HashRules(specRules []api.OCIRegistry) string {
func (c *RegistryRuleSetCache) HashRules(specRules []rules.OCIRegistry) string {
var b strings.Builder
b.Grow(len(specRules) * 64)
b.Grow(len(specRules) * 96)
const (
sepRule = "\n"
@@ -121,7 +264,7 @@ func (c *RegistryRuleSetCache) HashRules(specRules []api.OCIRegistry) string {
)
for _, r := range specRules {
url := strings.TrimSpace(r.Registry)
expr := r.Expression()
policies := make([]string, 0, len(r.Policy))
for _, p := range r.Policy {
@@ -137,7 +280,15 @@ func (c *RegistryRuleSetCache) HashRules(specRules []api.OCIRegistry) string {
sort.Strings(validations)
b.WriteString(url)
b.WriteString(strings.TrimSpace(expr.Expression))
b.WriteString(sepField)
if expr.Negate {
b.WriteString("1")
} else {
b.WriteString("0")
}
b.WriteString(sepField)
for i, p := range policies {
@@ -168,6 +319,10 @@ func (c *RegistryRuleSetCache) HashRules(specRules []api.OCIRegistry) string {
// Has is useful in tests and debugging.
func (c *RegistryRuleSetCache) Has(id string) bool {
if c == nil {
return false
}
c.mu.RLock()
defer c.mu.RUnlock()
@@ -177,13 +332,17 @@ func (c *RegistryRuleSetCache) Has(id string) bool {
}
func (c *RegistryRuleSetCache) Reset() {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.rs = make(map[string]*RuleSet)
}
// InsertForTest can be behind a build tag if you prefer, but it's fine to keep simple.
// InsertForTest can be behind a build tag if you prefer, but it is fine to keep simple.
//
//nolint:unused
func (c *RegistryRuleSetCache) insertForTest(id string) {
@@ -197,38 +356,52 @@ func (c *RegistryRuleSetCache) insertForTest(id string) {
c.rs[id] = &RuleSet{ID: id}
}
func buildRuleSet(id string, specRules []api.OCIRegistry) (*RuleSet, error) {
func (c *RegistryRuleSetCache) buildRuleSet(id string, specRules []rules.OCIRegistry) (*RuleSet, error) {
if c.regexCache == nil {
return nil, fmt.Errorf("regex cache is nil")
}
rs := &RuleSet{
ID: id,
Compiled: make([]CompiledRule, 0, len(specRules)),
}
for _, r := range specRules {
re, err := regexp.Compile(r.Registry)
expression := r.Expression()
compiled, _, err := c.regexCache.GetOrCompile(expression)
if err != nil {
return nil, fmt.Errorf("invalid registry regex %q: %w", r.Registry, err)
return nil, err
}
cr := CompiledRule{
Registry: r.Registry,
RE: re,
Expression: expression,
RegexID: compiled.ID,
}
if len(r.Policy) > 0 {
cr.AllowedPolicy = make(map[corev1.PullPolicy]struct{}, len(r.Policy))
for _, p := range r.Policy {
cr.AllowedPolicy[p] = struct{}{}
}
}
for _, v := range r.Validation {
switch v {
case api.ValidateImages:
cr.ValidateImages = true
rs.HasImages = true
case api.ValidateVolumes:
cr.ValidateVolumes = true
rs.HasVolumes = true
if len(r.Validation) == 0 {
cr.ValidateImages = true
cr.ValidateVolumes = true
rs.HasImages = true
rs.HasVolumes = true
} else {
for _, v := range r.Validation {
switch v {
case rules.ValidateImages:
cr.ValidateImages = true
rs.HasImages = true
case rules.ValidateVolumes:
cr.ValidateVolumes = true
rs.HasVolumes = true
}
}
}
+565 -433
View File
File diff suppressed because it is too large Load Diff
@@ -41,6 +41,7 @@ type CacheInvalidator struct {
TargetsCache *cache.CompiledTargetsCache[string]
JSONPathCache *cache.JSONPathCache
ImpersonationCache *cache.ImpersonationCache
RegexCache *cache.RegexCache
}
func (r *CacheInvalidator) NeedLeaderElection() bool {
@@ -171,6 +172,10 @@ func (r *CacheInvalidator) rebuildCaches(
) error {
var errs []error
if err := r.rebuildRegexCache(ctx, log); err != nil {
errs = append(errs, fmt.Errorf("rebuild Regex cache: %w", err))
}
if err := r.rebuildJSONPathCache(ctx, log); err != nil {
errs = append(errs, fmt.Errorf("rebuild JSONPath cache: %w", err))
}
@@ -0,0 +1,79 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package invalidator
import (
"context"
"fmt"
"github.com/go-logr/logr"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/cache"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rules"
)
func (r *CacheInvalidator) rebuildRegexCache(ctx context.Context, log logr.Logger) error {
ruleStatuses := &capsulev1beta2.RuleStatusList{}
if err := r.List(ctx, ruleStatuses); err != nil {
return err
}
log.V(5).Info("rebuilding regex cache",
"regexesBefore", r.RegexCache.Stats(),
"ruleStatuses", len(ruleStatuses.Items),
)
r.RegexCache.Reset()
expressions := make(map[string]api.RegExpression)
for i := range ruleStatuses.Items {
rs := &ruleStatuses.Items[i]
collectRegexExpressionsFromNamespaceRules(expressions, rs.Spec)
collectRegexExpressionsFromNamespaceRules(expressions, rs.Status.Rules)
}
for _, expr := range expressions {
if _, _, err := r.RegexCache.GetOrCompile(expr); err != nil {
return fmt.Errorf("build regex cache entry %q: %w", expr.Expression, err)
}
}
log.V(5).Info("rebuilt regex cache",
"uniqueExpressions", len(expressions),
"regexesAfter", r.RegexCache.Stats(),
)
return nil
}
func collectRegexExpressionsFromNamespaceRules(
set map[string]api.RegExpression,
r []*rules.NamespaceRuleBodyNamespace,
) {
for _, rule := range r {
collectRegexExpressionsFromNamespaceRule(set, rule)
}
}
func collectRegexExpressionsFromNamespaceRule(
set map[string]api.RegExpression,
rule *rules.NamespaceRuleBodyNamespace,
) {
if rule == nil {
return
}
for _, registry := range rule.Enforce.Registries {
expr := registry.RegExpression
if expr.Expression == "" {
continue
}
set[cache.HashRegex(expr)] = expr
}
}
@@ -5,6 +5,7 @@ package invalidator
import (
"context"
"fmt"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -25,26 +26,34 @@ func (r *CacheInvalidator) rebuildRuleStatusRegistryCache(ctx context.Context, l
}
log.V(5).Info("rebuilding registry cache from existing rules",
"rules", len(rsList.Items),
"cache_rules_before", r.RegistryCache.Stats(),
"ruleStatuses", len(rsList.Items),
"cacheRulesBefore", r.RegistryCache.Stats(),
)
r.RegistryCache.Reset()
for _, item := range rsList.Items {
regs := item.Status.Rule.Enforce.Registries
if len(regs) == 0 {
continue
}
for i := range rsList.Items {
item := &rsList.Items[i]
if _, _, err := r.RegistryCache.GetOrBuild(regs); err != nil {
return err
for _, rule := range item.Status.Rules {
if rule == nil || len(rule.Enforce.Registries) == 0 {
continue
}
if _, _, err := r.RegistryCache.GetOrBuild(rule.Enforce.Registries); err != nil {
return fmt.Errorf(
"build registry cache for RuleStatus %s/%s: %w",
item.Namespace,
item.Name,
err,
)
}
}
}
log.V(5).Info("rebuilt registry cache from existing rules",
"rules", len(rsList.Items),
"cache_rules_after", r.RegistryCache.Stats(),
"ruleStatuses", len(rsList.Items),
"cacheRulesAfter", r.RegistryCache.Stats(),
)
return nil
+47 -12
View File
@@ -26,8 +26,9 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
"github.com/projectcapsule/capsule/pkg/api"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
meta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
"github.com/projectcapsule/capsule/pkg/runtime/predicates"
)
@@ -64,17 +65,17 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
}
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
log := r.Log.WithValues("Request.Name", request.Name)
instance := &capsulev1beta2.RuleStatus{}
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
if apierrors.IsNotFound(err) {
r.Log.V(5).Info("request object not found, could have been deleted after reconcile request")
log.V(5).Info("request object not found, could have been deleted after reconcile request")
return reconcile.Result{}, nil
}
r.Log.Error(err, "error reading the object")
log.Error(err, "error reading the object")
return result, err
}
@@ -113,6 +114,15 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
err = nil
}()
// Best-Effort for Updating the status
if updateErr := r.updateReconcilingStatus(ctx, instance); updateErr != nil {
if caperrors.IgnoreGone(updateErr) {
return reconcile.Result{}, nil
}
log.Error(updateErr, "failed to update status")
}
// Reconcile
if err = r.reconcile(ctx, instance); err != nil {
err = fmt.Errorf("cannot collect available resources: %w", err)
@@ -125,27 +135,39 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
reconcileError = fmt.Errorf("had errors reconciling")
}
r.Log.V(4).Info("reconciling completed")
log.V(4).Info("reconciling completed")
return ctrl.Result{}, reconcileError
}
func (r Manager) reconcile(ctx context.Context, instance *capsulev1beta2.RuleStatus) (err error) {
out := api.NamespaceRuleBodyNamespace{}
func (r Manager) reconcile(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
ruleStatus := make([]*rules.NamespaceRuleBodyNamespace, 0, len(instance.Spec))
for _, rule := range instance.Spec {
if rule == nil {
continue
}
// Merge enforce body (for now: only registries)
// Preserve order: append in the order rules are declared.
if len(rule.Enforce.Registries) > 0 {
out.Enforce.Registries = append(out.Enforce.Registries, rule.Enforce.Registries...)
normalized := *rule
normalized.Enforce = rule.Enforce
normalized.Enforce.Registries = append(
[]rules.OCIRegistry(nil),
rule.Enforce.Registries...,
)
// Keep status compact: skip empty enforce blocks.
if len(normalized.Enforce.Registries) == 0 {
continue
}
ruleStatus = append(ruleStatus, &normalized)
}
instance.Status.Rule = out
instance.Status.Rules = ruleStatus
//nolint:staticcheck
instance.Status.Rule = rules.NamespaceRuleBodyNamespace{}
return nil
}
@@ -184,3 +206,16 @@ func (r *Manager) updateStatus(ctx context.Context, instance *capsulev1beta2.Rul
return nil
})
}
func (r *Manager) updateReconcilingStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
latest := &capsulev1beta2.RuleStatus{}
if err = r.reader.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
return err
}
latest.Status.Conditions.UpdateConditionByType(meta.NewReadyConditionReconcilingReason(instance))
return r.Status().Update(ctx, latest)
})
}
+3 -3
View File
@@ -13,8 +13,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/tenant"
)
@@ -44,7 +44,7 @@ func (r *Manager) ensureRuleStatus(
log logr.Logger,
tnt *capsulev1beta2.Tenant,
namespace *corev1.Namespace,
body *api.NamespaceRuleBodyNamespace,
body []*rules.NamespaceRuleBodyNamespace,
) error {
rule := &capsulev1beta2.RuleStatus{
ObjectMeta: metav1.ObjectMeta{
@@ -65,7 +65,7 @@ func (r *Manager) ensureRuleStatus(
rule.SetLabels(labels)
if body != nil {
rule.Spec = []*api.NamespaceRuleBodyNamespace{body}
rule.Spec = body
}
return controllerutil.SetControllerReference(tnt, rule, r.Scheme())
@@ -12,8 +12,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
"github.com/projectcapsule/capsule/pkg/api/rules"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
@@ -37,7 +37,7 @@ func (h *containerRegistryLegacyHandler) OnCreate(
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder)
@@ -52,7 +52,7 @@ func (h *containerRegistryLegacyHandler) OnUpdate(
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder)
@@ -66,7 +66,7 @@ func (h *containerRegistryLegacyHandler) OnDelete(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
+4 -4
View File
@@ -12,8 +12,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
"github.com/projectcapsule/capsule/pkg/api/rules"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
@@ -32,7 +32,7 @@ func (h *imagePullPolicy) OnCreate(
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder)
@@ -47,7 +47,7 @@ func (h *imagePullPolicy) OnUpdate(
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder)
@@ -61,7 +61,7 @@ func (h *imagePullPolicy) OnDelete(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
+4 -4
View File
@@ -14,8 +14,8 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/webhook/utils"
"github.com/projectcapsule/capsule/pkg/api"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
"github.com/projectcapsule/capsule/pkg/api/rules"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
@@ -34,7 +34,7 @@ func (h *priorityClass) OnCreate(
decoder admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
allowed := tnt.Spec.PriorityClasses
@@ -96,7 +96,7 @@ func (h *priorityClass) OnUpdate(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
@@ -110,7 +110,7 @@ func (h *priorityClass) OnDelete(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
+331 -139
View File
@@ -13,11 +13,12 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/events"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/cache"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/rules"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
@@ -43,25 +44,25 @@ func (h *registryHandler) OnCreate(
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
rule *api.NamespaceRuleBodyNamespace,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder, rule)
return h.validate(ctx, req, pod, tnt, recorder, ruleBlocks)
}
}
func (h *registryHandler) OnUpdate(
_ client.Client,
_ client.Reader,
old *corev1.Pod,
_ *corev1.Pod,
pod *corev1.Pod,
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
rule *api.NamespaceRuleBodyNamespace,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(req, pod, tnt, recorder, rule)
return h.validate(ctx, req, pod, tnt, recorder, ruleBlocks)
}
}
@@ -72,7 +73,7 @@ func (h *registryHandler) OnDelete(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
@@ -80,43 +81,47 @@ func (h *registryHandler) OnDelete(
}
func (h *registryHandler) validate(
ctx context.Context,
req admission.Request,
pod *corev1.Pod,
tnt *capsulev1beta2.Tenant,
recorder events.EventRecorder,
rule *api.NamespaceRuleBodyNamespace,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) *admission.Response {
if rule == nil || len(rule.Enforce.Registries) == 0 {
resp := admission.Allowed("no registry rules")
if h.cache == nil {
resp := admission.Errored(http.StatusInternalServerError, fmt.Errorf("registry rule set cache is nil"))
return &resp
}
rs, _, err := h.cache.GetOrBuild(rule.Enforce.Registries)
if err != nil {
resp := admission.Errored(http.StatusInternalServerError, err)
log.FromContext(ctx).V(5).Info(
"handling pod registry rules",
"pod", pod.Name,
"namespace", pod.Namespace,
"rules", len(ruleBlocks),
)
if len(ruleBlocks) == 0 {
return nil
}
warnings := make([]string, 0)
if resp := h.validateContainers(req, pod, tnt, recorder, ruleBlocks, &warnings); resp != nil {
return resp
}
if resp := h.validateVolumes(req, pod, tnt, recorder, ruleBlocks, &warnings); resp != nil {
return resp
}
if len(warnings) > 0 {
resp := admission.Allowed("registry rules audited")
resp.Warnings = append(resp.Warnings, warnings...)
return &resp
}
if rs == nil {
resp := admission.Allowed("no registry rules")
return &resp
}
if rs.HasImages {
if resp := h.validateContainers(req, pod, tnt, recorder, rs); resp != nil {
return resp
}
}
if rs.HasVolumes {
if resp := h.validateVolumes(req, pod, tnt, recorder, rs); resp != nil {
return resp
}
}
return nil
}
@@ -125,25 +130,62 @@ func (h *registryHandler) validateContainers(
pod *corev1.Pod,
tnt *capsulev1beta2.Tenant,
recorder events.EventRecorder,
rs *cache.RuleSet,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
warnings *[]string,
) *admission.Response {
for i := range pod.Spec.InitContainers {
c := pod.Spec.InitContainers[i]
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("initContainers[%d]", i)); resp != nil {
if resp := h.verifyOCIReference(
recorder,
req,
tnt,
pod,
ruleBlocks,
rules.ValidateImages,
c.Image,
c.ImagePullPolicy,
fmt.Sprintf("initContainers[%d]", i),
warnings,
); resp != nil {
return resp
}
}
for i := range pod.Spec.EphemeralContainers {
c := pod.Spec.EphemeralContainers[i]
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("ephemeralContainers[%d]", i)); resp != nil {
if resp := h.verifyOCIReference(
recorder,
req,
tnt,
pod,
ruleBlocks,
rules.ValidateImages,
c.Image,
c.ImagePullPolicy,
fmt.Sprintf("ephemeralContainers[%d]", i),
warnings,
); resp != nil {
return resp
}
}
for i := range pod.Spec.Containers {
c := pod.Spec.Containers[i]
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("containers[%d]", i)); resp != nil {
if resp := h.verifyOCIReference(
recorder,
req,
tnt,
pod,
ruleBlocks,
rules.ValidateImages,
c.Image,
c.ImagePullPolicy,
fmt.Sprintf("containers[%d]", i),
warnings,
); resp != nil {
return resp
}
}
@@ -156,7 +198,8 @@ func (h *registryHandler) validateVolumes(
pod *corev1.Pod,
tnt *capsulev1beta2.Tenant,
recorder events.EventRecorder,
rs *cache.RuleSet,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
warnings *[]string,
) *admission.Response {
for i := range pod.Spec.Volumes {
v := pod.Spec.Volumes[i]
@@ -166,16 +209,26 @@ func (h *registryHandler) validateVolumes(
ref := strings.TrimSpace(v.Image.Reference)
if ref == "" {
return ad.Deny(
return h.denyWithEvent(
recorder,
tnt,
pod,
evt.ReasonForbiddenContainerRegistry,
fmt.Sprintf("volume %q has empty image.reference", v.Name),
)
}
if resp := h.verifyOCIReference(
recorder, req, tnt, pod,
rs, api.ValidateVolumes,
ref, v.Image.PullPolicy,
recorder,
req,
tnt,
pod,
ruleBlocks,
rules.ValidateVolumes,
ref,
v.Image.PullPolicy,
fmt.Sprintf("volumes[%d](%s)", i, v.Name),
warnings,
); resp != nil {
return resp
}
@@ -184,136 +237,275 @@ func (h *registryHandler) validateVolumes(
return nil
}
type resolvedRegistryConfig struct {
allowed bool
allowedPolicy map[corev1.PullPolicy]struct{} // nil => no restriction
}
func resolveRegistryConfig(
rules []cache.CompiledRule,
ref string,
target api.RegistryValidationTarget,
) resolvedRegistryConfig {
var res resolvedRegistryConfig
for i := range rules {
r := rules[i]
switch target {
case api.ValidateImages:
if !r.ValidateImages { // adjust field name
continue
}
case api.ValidateVolumes:
if !r.ValidateVolumes { // adjust field name
continue
}
}
if !r.RE.MatchString(ref) { // adjust field name
continue
}
res.allowed = true
// only override pullpolicy restriction when explicitly set by a later matching rule
if len(r.AllowedPolicy) > 0 { // adjust field name
res.allowedPolicy = r.AllowedPolicy
}
}
return res
}
func (h *registryHandler) verifyOCIReference(
recorder events.EventRecorder,
req admission.Request,
tnt *capsulev1beta2.Tenant,
pod *corev1.Pod,
rs *cache.RuleSet,
target api.RegistryValidationTarget,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
target rules.RegistryValidationTarget,
reference string,
pullPolicy corev1.PullPolicy,
where string,
warnings *[]string,
) *admission.Response {
ref := strings.TrimSpace(reference)
if ref == "" {
msg := fmt.Sprintf("%s has empty reference", where)
recorder.Eventf(
pod,
return h.denyWithEvent(
recorder,
tnt,
corev1.EventTypeWarning,
pod,
evt.ReasonForbiddenContainerRegistry,
fmt.Sprintf("%s has empty reference", where),
)
}
evaluation, err := h.evaluateOCIReference(ruleBlocks, target, ref)
if err != nil {
resp := admission.Errored(http.StatusInternalServerError, err)
return &resp
}
if evaluation == nil {
return nil
}
for _, audit := range evaluation.Audits {
msg := fmt.Sprintf(
"%s reference %q matched audit registry rule %q",
where,
ref,
audit.Matched.Expression.Expression,
)
h.auditWithEvent(recorder, tnt, pod, msg)
if warnings != nil {
*warnings = append(*warnings, msg)
}
}
if evaluation.Decision == nil {
return nil
}
switch evaluation.Decision.Action {
case rules.ActionTypeAllow:
if resp := h.validateAllowedPullPolicy(
recorder,
tnt,
pod,
evaluation.Decision.Matched,
ref,
pullPolicy,
where,
); resp != nil {
return resp
}
return nil
case rules.ActionTypeDeny:
msg := fmt.Sprintf(
"%s reference %q is denied by registry rule %q",
where,
ref,
evaluation.Decision.Matched.Expression.Expression,
)
return h.denyWithEvent(
recorder,
tnt,
pod,
evt.ReasonForbiddenContainerRegistry,
evt.ActionValidationDenied,
msg,
)
return ad.Deny(msg)
}
// Match rules against the FULL OCI reference string.
// This avoids relying on parsing logic and supports nested paths, digests, etc.
cfg := resolveRegistryConfig(rs.Compiled, ref, target)
if !cfg.allowed {
msg := fmt.Sprintf("%s reference %q is not allowed", where, ref)
recorder.Eventf(
pod,
tnt,
corev1.EventTypeWarning,
evt.ReasonForbiddenContainerRegistry,
evt.ActionValidationDenied,
msg,
case rules.ActionTypeAudit:
msg := fmt.Sprintf(
"%s reference %q matched audit registry rule %q",
where,
ref,
evaluation.Decision.Matched.Expression.Expression,
)
return ad.Deny(msg)
h.auditWithEvent(recorder, tnt, pod, msg)
if warnings != nil {
*warnings = append(*warnings, msg)
}
return nil
default:
resp := admission.Errored(
http.StatusInternalServerError,
fmt.Errorf("unsupported namespace rule action %q", evaluation.Decision.Action),
)
return &resp
}
}
type registryDecision struct {
rules.RuleDecision
Matched *cache.CompiledRule
}
type registryEvaluation struct {
Decision *registryDecision
Audits []*registryDecision
}
func (h *registryHandler) evaluateOCIReference(
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
target rules.RegistryValidationTarget,
ref string,
) (*registryEvaluation, error) {
evaluation := &registryEvaluation{}
for _, rule := range ruleBlocks {
if rule == nil || len(rule.Enforce.Registries) == 0 {
continue
}
rs, _, err := h.cache.GetOrBuild(rule.Enforce.Registries)
if err != nil {
return nil, err
}
if rs == nil {
continue
}
matched, err := h.cache.MatchReference(rs, ref, target)
if err != nil {
return nil, err
}
if matched == nil {
continue
}
action := rule.Enforce.Action
if action == "" {
action = rules.ActionTypeDeny
}
decision := &registryDecision{
RuleDecision: rules.RuleDecision{
Action: action,
Rule: rule,
},
Matched: matched,
}
switch action {
case rules.ActionTypeAllow, rules.ActionTypeDeny:
// Last matching allow/deny wins.
evaluation.Decision = decision
case rules.ActionTypeAudit:
evaluation.Audits = append(evaluation.Audits, decision)
default:
return nil, fmt.Errorf("unsupported namespace rule action %q", action)
}
}
// No defaulting: enforce only if restricted; empty pullPolicy is rejected under restriction.
if cfg.allowedPolicy != nil {
allowed := formatAllowedPullPolicies(cfg.allowedPolicy)
return evaluation, nil
}
if pullPolicy == "" {
msg := fmt.Sprintf(
"%s reference %q must explicitly set pullPolicy (allowed: %s)",
where, ref, allowed,
)
func (h *registryHandler) validateAllowedPullPolicy(
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
pod *corev1.Pod,
matched *cache.CompiledRule,
ref string,
pullPolicy corev1.PullPolicy,
where string,
) *admission.Response {
if matched == nil || len(matched.AllowedPolicy) == 0 {
return nil
}
recorder.Eventf(
pod,
tnt,
corev1.EventTypeWarning,
evt.ReasonForbiddenPullPolicy,
evt.ActionValidationDenied,
msg,
)
allowed := formatAllowedPullPolicies(matched.AllowedPolicy)
return ad.Deny(msg)
}
if pullPolicy == "" {
msg := fmt.Sprintf(
"%s reference %q must explicitly set pullPolicy (allowed: %s)",
where,
ref,
allowed,
)
if _, ok := cfg.allowedPolicy[pullPolicy]; !ok {
msg := fmt.Sprintf(
"%s reference %q uses pullPolicy=%s which is not allowed (allowed: %s)",
where, ref, pullPolicy, allowed,
)
return h.denyWithEvent(
recorder,
tnt,
pod,
evt.ReasonForbiddenPullPolicy,
msg,
)
}
recorder.Eventf(
pod,
tnt,
corev1.EventTypeWarning,
evt.ReasonForbiddenPullPolicy,
evt.ActionValidationDenied,
msg,
)
if _, ok := matched.AllowedPolicy[pullPolicy]; !ok {
msg := fmt.Sprintf(
"%s reference %q uses pullPolicy=%s which is not allowed (allowed: %s)",
where,
ref,
pullPolicy,
allowed,
)
return ad.Deny(msg)
}
return h.denyWithEvent(
recorder,
tnt,
pod,
evt.ReasonForbiddenPullPolicy,
msg,
)
}
return nil
}
func (h *registryHandler) auditWithEvent(
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
pod *corev1.Pod,
msg string,
) {
recorder.Eventf(
pod,
tnt,
corev1.EventTypeWarning,
evt.ReasonForbiddenContainerRegistry,
evt.ActionValidationDenied,
msg,
)
}
func (h *registryHandler) denyWithEvent(
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
pod *corev1.Pod,
reason string,
msg string,
) *admission.Response {
recorder.Eventf(
pod,
tnt,
corev1.EventTypeWarning,
reason,
evt.ActionValidationDenied,
msg,
)
return ad.Deny(msg)
}
func formatAllowedPullPolicies(policies map[corev1.PullPolicy]struct{}) string {
if len(policies) == 0 {
return ""
+4 -4
View File
@@ -15,8 +15,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
"github.com/projectcapsule/capsule/pkg/api/rules"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
@@ -35,7 +35,7 @@ func (h *runtimeClass) OnCreate(
decoder admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
_ *api.NamespaceRuleBodyNamespace,
_ []*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(ctx, reader, recorder, req, pod, tnt)
@@ -50,7 +50,7 @@ func (h *runtimeClass) OnUpdate(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
@@ -64,7 +64,7 @@ func (h *runtimeClass) OnDelete(
admission.Decoder,
events.EventRecorder,
*capsulev1beta2.Tenant,
*api.NamespaceRuleBodyNamespace,
[]*rules.NamespaceRuleBodyNamespace,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
@@ -74,13 +74,11 @@ func ValidateRule(tnt *capsulev1beta2.Tenant, req admission.Request) *admission.
return nil
}
// Validate Rules
for i, rule := range tnt.Spec.Rules {
if rule == nil {
continue
}
// Validate NamespaceSelector (if provided)
if rule.NamespaceSelector != nil {
if _, err := metav1.LabelSelectorAsSelector(rule.NamespaceSelector); err != nil {
return ad.Deny(
@@ -89,11 +87,24 @@ func ValidateRule(tnt *capsulev1beta2.Tenant, req admission.Request) *admission.
}
}
// Validate Registries
for _, r := range rule.Enforce.Registries {
if _, err := regexp.Compile(r.Registry); err != nil {
for j, registry := range rule.Enforce.Registries {
expr := registry.Expression()
if expr.Expression == "" {
return ad.Deny(
fmt.Sprintf("unable to compile regex %q: %v", r.Registry, err),
fmt.Sprintf("rules[%d].enforce.registries[%d].exp must not be empty", i, j),
)
}
if _, err := regexp.Compile(expr.Expression); err != nil {
return ad.Deny(
fmt.Sprintf(
"rules[%d].enforce.registries[%d].exp %q is invalid: %v",
i,
j,
expr.Expression,
err,
),
)
}
}
-54
View File
@@ -1,54 +0,0 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// For future implementation where users might manage RuleStatus CRs themselves
// +kubebuilder:object:generate=true
type NamespaceRuleBodyNamespace struct {
// Enforcement for given rule
//+optional
Enforce NamespaceRuleEnforceBody `json:"enforce,omitzero"`
}
// Rules Distributed via Tenants
// +kubebuilder:object:generate=true
type NamespaceRuleBodyTenant struct {
NamespaceRuleBodyNamespace `json:",inline"`
// Select namespaces which are going to be targeted with this rule
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
// Permissions for given rule
//+optional
Permissions NamespaceRulePermissionBody `json:"permissions,omitzero"`
}
// +kubebuilder:object:generate=true
type NamespaceRuleEnforceBody struct {
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []OCIRegistry `json:"registries,omitempty"`
}
// +kubebuilder:object:generate=true
type NamespaceRulePermissionBody struct {
// Define Promotion Rules which distributed additional ClusterRoles across the Tenant
// for promoted ServiceAccounts.
Promotions []*NamespaceRulePromotionRule `json:"rules,omitempty"`
}
// +kubebuilder:object:generate=true
type NamespaceRulePromotionRule struct {
// ClusterRoles granted to the promoted ServiceAccounts across the Tenant
// kubebuilder:validation:Minimum=1
ClusterRoles []string `json:"clusterRoles,omitempty"`
// Match ServiceAccounts which are promoted which are granted these additional ClusterRoles
// across the Tenant
Selector *metav1.LabelSelector `json:"selector,omitempty"`
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api
// +kubebuilder:object:generate=true
type RegExpression struct {
// Expression used to evaluate regex
Expression string `json:"exp,omitempty"`
// Negate regular Expression
//+kubebuilder:default:=false
Negate bool `json:"negate,omitempty"`
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
const (
ActionTypeAllow ActionType = "allow"
ActionTypeDeny ActionType = "deny"
ActionTypeAudit ActionType = "audit"
)
// +kubebuilder:validation:Enum=allow;deny;audit
type ActionType string
type RuleDecision struct {
Action ActionType
Rule *NamespaceRuleBodyNamespace
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
import (
corev1 "k8s.io/api/core/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
type ImagePullPolicySpec string
func (i ImagePullPolicySpec) String() string {
return string(i)
}
// +kubebuilder:validation:Enum=pod/images;pod/volumes
type RegistryValidationTarget string
const (
ValidateImages RegistryValidationTarget = "pod/images"
ValidateVolumes RegistryValidationTarget = "pod/volumes"
)
// +kubebuilder:object:generate=true
type OCIRegistry struct {
api.RegExpression `json:",inline"`
// Deprecated: Use exp field
//
// OCI Registry endpoint, is treated as regular expression.
Registry string `json:"url,omitempty"`
// Allowed PullPolicy for the given registry. Supplying no value allows all policies.
// +optional
// +kubebuilder:validation:Items:Enum=Always;Never;IfNotPresent
Policy []corev1.PullPolicy `json:"policy,omitempty"`
// Requesting Resources
//+kubebuilder:default:={pod/images,pod/volumes}
Validation []RegistryValidationTarget `json:"validation,omitempty"`
}
func (r OCIRegistry) Expression() api.RegExpression {
if r.RegExpression.Expression != "" {
return r.RegExpression
}
return api.RegExpression{
Expression: r.Registry,
Negate: false,
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
// +kubebuilder:object:generate=true
type NamespaceRuleEnforceBody struct {
// Declare the action being performed on the enforcement rule:
// deny: On match, deny admission request
// allow: On match, allowed admission request
// audit: On match, audit (post event) of admission request
//+kubebuilder:default:=deny
Action ActionType `json:"action,omitempty"`
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []OCIRegistry `json:"registries,omitempty"`
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:generate=true
type NamespaceRulePermissionBody struct {
// Define Promotion Rules which distributed additional ClusterRoles across the Tenant
// for promoted ServiceAccounts.
Promotions []*NamespaceRulePromotionRule `json:"rules,omitempty"`
}
// +kubebuilder:object:generate=true
type NamespaceRulePromotionRule struct {
// ClusterRoles granted to the promoted ServiceAccounts across the Tenant
// kubebuilder:validation:Minimum=1
ClusterRoles []string `json:"clusterRoles,omitempty"`
// Match ServiceAccounts which are promoted which are granted these additional ClusterRoles
// across the Tenant
Selector *metav1.LabelSelector `json:"selector,omitempty"`
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// For future implementation where users might manage RuleStatus CRs themselves
// +kubebuilder:object:generate=true
type NamespaceRuleBodyNamespace struct {
// Enforcement for given rule
//+optional
Enforce NamespaceRuleEnforceBody `json:"enforce,omitzero"`
}
// Rules Distributed via Tenants
// +kubebuilder:object:generate=true
type NamespaceRuleBodyTenant struct {
NamespaceRuleBodyNamespace `json:",inline"`
// Select namespaces which are going to be targeted with this rule
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
// Permissions for given rule
//+optional
Permissions NamespaceRulePermissionBody `json:"permissions,omitzero"`
}
+150
View File
@@ -0,0 +1,150 @@
//go:build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package rules
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyNamespace) DeepCopyInto(out *NamespaceRuleBodyNamespace) {
*out = *in
in.Enforce.DeepCopyInto(&out.Enforce)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBodyNamespace.
func (in *NamespaceRuleBodyNamespace) DeepCopy() *NamespaceRuleBodyNamespace {
if in == nil {
return nil
}
out := new(NamespaceRuleBodyNamespace)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyTenant) DeepCopyInto(out *NamespaceRuleBodyTenant) {
*out = *in
in.NamespaceRuleBodyNamespace.DeepCopyInto(&out.NamespaceRuleBodyNamespace)
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
in.Permissions.DeepCopyInto(&out.Permissions)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBodyTenant.
func (in *NamespaceRuleBodyTenant) DeepCopy() *NamespaceRuleBodyTenant {
if in == nil {
return nil
}
out := new(NamespaceRuleBodyTenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody) {
*out = *in
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]OCIRegistry, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
if in == nil {
return nil
}
out := new(NamespaceRuleEnforceBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRulePermissionBody) DeepCopyInto(out *NamespaceRulePermissionBody) {
*out = *in
if in.Promotions != nil {
in, out := &in.Promotions, &out.Promotions
*out = make([]*NamespaceRulePromotionRule, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(NamespaceRulePromotionRule)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRulePermissionBody.
func (in *NamespaceRulePermissionBody) DeepCopy() *NamespaceRulePermissionBody {
if in == nil {
return nil
}
out := new(NamespaceRulePermissionBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRulePromotionRule) DeepCopyInto(out *NamespaceRulePromotionRule) {
*out = *in
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Selector != nil {
in, out := &in.Selector, &out.Selector
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRulePromotionRule.
func (in *NamespaceRulePromotionRule) DeepCopy() *NamespaceRulePromotionRule {
if in == nil {
return nil
}
out := new(NamespaceRulePromotionRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OCIRegistry) DeepCopyInto(out *OCIRegistry) {
*out = *in
out.RegExpression = in.RegExpression
if in.Policy != nil {
in, out := &in.Policy, &out.Policy
*out = make([]v1.PullPolicy, len(*in))
copy(*out, *in)
}
if in.Validation != nil {
in, out := &in.Validation, &out.Validation
*out = make([]RegistryValidationTarget, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRegistry.
func (in *OCIRegistry) DeepCopy() *OCIRegistry {
if in == nil {
return nil
}
out := new(OCIRegistry)
in.DeepCopyInto(out)
return out
}
+15 -111
View File
@@ -204,117 +204,6 @@ func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyNamespace) DeepCopyInto(out *NamespaceRuleBodyNamespace) {
*out = *in
in.Enforce.DeepCopyInto(&out.Enforce)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBodyNamespace.
func (in *NamespaceRuleBodyNamespace) DeepCopy() *NamespaceRuleBodyNamespace {
if in == nil {
return nil
}
out := new(NamespaceRuleBodyNamespace)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyTenant) DeepCopyInto(out *NamespaceRuleBodyTenant) {
*out = *in
in.NamespaceRuleBodyNamespace.DeepCopyInto(&out.NamespaceRuleBodyNamespace)
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
in.Permissions.DeepCopyInto(&out.Permissions)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBodyTenant.
func (in *NamespaceRuleBodyTenant) DeepCopy() *NamespaceRuleBodyTenant {
if in == nil {
return nil
}
out := new(NamespaceRuleBodyTenant)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody) {
*out = *in
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]OCIRegistry, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
if in == nil {
return nil
}
out := new(NamespaceRuleEnforceBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRulePermissionBody) DeepCopyInto(out *NamespaceRulePermissionBody) {
*out = *in
if in.Promotions != nil {
in, out := &in.Promotions, &out.Promotions
*out = make([]*NamespaceRulePromotionRule, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(NamespaceRulePromotionRule)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRulePermissionBody.
func (in *NamespaceRulePermissionBody) DeepCopy() *NamespaceRulePermissionBody {
if in == nil {
return nil
}
out := new(NamespaceRulePermissionBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRulePromotionRule) DeepCopyInto(out *NamespaceRulePromotionRule) {
*out = *in
if in.ClusterRoles != nil {
in, out := &in.ClusterRoles, &out.ClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Selector != nil {
in, out := &in.Selector, &out.Selector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRulePromotionRule.
func (in *NamespaceRulePromotionRule) DeepCopy() *NamespaceRulePromotionRule {
if in == nil {
return nil
}
out := new(NamespaceRulePromotionRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) {
*out = *in
@@ -399,6 +288,21 @@ func (in *PoolExhaustionResource) DeepCopy() *PoolExhaustionResource {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RegExpression) DeepCopyInto(out *RegExpression) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegExpression.
func (in *RegExpression) DeepCopy() *RegExpression {
if in == nil {
return nil
}
out := new(RegExpression)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceQuotaSpec) DeepCopyInto(out *ResourceQuotaSpec) {
*out = *in
+64 -18
View File
@@ -15,15 +15,42 @@ import (
"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/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/tenant"
)
type TypedHandlerWithTenantWithRuleset[T client.Object] interface {
OnCreate(c client.Client, reader client.Reader, obj T, decoder admission.Decoder, recorder events.EventRecorder, tnt *capsulev1beta2.Tenant, rule *api.NamespaceRuleBodyNamespace) Func
OnUpdate(c client.Client, reader client.Reader, obj T, old T, decoder admission.Decoder, recorder events.EventRecorder, tnt *capsulev1beta2.Tenant, rule *api.NamespaceRuleBodyNamespace) Func
OnDelete(c client.Client, reader client.Reader, obj T, decoder admission.Decoder, recorder events.EventRecorder, tnt *capsulev1beta2.Tenant, rule *api.NamespaceRuleBodyNamespace) Func
OnCreate(
c client.Client,
reader client.Reader,
obj T,
decoder admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) Func
OnUpdate(
c client.Client,
reader client.Reader,
old T,
obj T,
decoder admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) Func
OnDelete(
c client.Client,
reader client.Reader,
obj T,
decoder admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
ruleBlocks []*rules.NamespaceRuleBodyNamespace,
) Func
}
type TypedTenantWithRulesetHandler[T client.Object] struct {
@@ -31,7 +58,12 @@ type TypedTenantWithRulesetHandler[T client.Object] struct {
Handlers []TypedHandlerWithTenantWithRuleset[T]
}
func (h *TypedTenantWithRulesetHandler[T]) OnCreate(c client.Client, reader client.Reader, decoder admission.Decoder, recorder events.EventRecorder) Func {
func (h *TypedTenantWithRulesetHandler[T]) OnCreate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
tnt, err := h.resolveTenant(ctx, reader, req)
if err != nil {
@@ -47,13 +79,13 @@ func (h *TypedTenantWithRulesetHandler[T]) OnCreate(c client.Client, reader clie
return ErroredResponse(err)
}
rule, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
ruleBlocks, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
if err != nil {
return ErroredResponse(err)
}
for _, hndl := range h.Handlers {
if response := hndl.OnCreate(c, reader, obj, decoder, recorder, tnt, rule)(ctx, req); response != nil {
if response := hndl.OnCreate(c, reader, obj, decoder, recorder, tnt, ruleBlocks)(ctx, req); response != nil {
return response
}
}
@@ -62,7 +94,12 @@ func (h *TypedTenantWithRulesetHandler[T]) OnCreate(c client.Client, reader clie
}
}
func (h *TypedTenantWithRulesetHandler[T]) OnUpdate(c client.Client, reader client.Reader, decoder admission.Decoder, recorder events.EventRecorder) Func {
func (h *TypedTenantWithRulesetHandler[T]) OnUpdate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
tnt, err := h.resolveTenant(ctx, c, req)
if err != nil {
@@ -83,13 +120,13 @@ func (h *TypedTenantWithRulesetHandler[T]) OnUpdate(c client.Client, reader clie
return ErroredResponse(err)
}
rule, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
ruleBlocks, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
if err != nil {
return ErroredResponse(err)
}
for _, hndl := range h.Handlers {
if response := hndl.OnUpdate(c, reader, oldObj, newObj, decoder, recorder, tnt, rule)(ctx, req); response != nil {
if response := hndl.OnUpdate(c, reader, oldObj, newObj, decoder, recorder, tnt, ruleBlocks)(ctx, req); response != nil {
return response
}
}
@@ -98,7 +135,12 @@ func (h *TypedTenantWithRulesetHandler[T]) OnUpdate(c client.Client, reader clie
}
}
func (h *TypedTenantWithRulesetHandler[T]) OnDelete(c client.Client, reader client.Reader, decoder admission.Decoder, recorder events.EventRecorder) Func {
func (h *TypedTenantWithRulesetHandler[T]) OnDelete(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
tnt, err := h.resolveTenant(ctx, reader, req)
if err != nil {
@@ -114,13 +156,13 @@ func (h *TypedTenantWithRulesetHandler[T]) OnDelete(c client.Client, reader clie
return ErroredResponse(err)
}
rule, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
ruleBlocks, err := h.resolveRuleset(ctx, c, req, req.Namespace, tnt)
if err != nil {
return ErroredResponse(err)
}
for _, hndl := range h.Handlers {
if response := hndl.OnDelete(c, reader, obj, decoder, recorder, tnt, rule)(ctx, req); response != nil {
if response := hndl.OnDelete(c, reader, obj, decoder, recorder, tnt, ruleBlocks)(ctx, req); response != nil {
return response
}
}
@@ -129,7 +171,11 @@ func (h *TypedTenantWithRulesetHandler[T]) OnDelete(c client.Client, reader clie
}
}
func (h *TypedTenantWithRulesetHandler[T]) resolveTenant(ctx context.Context, c client.Reader, req admission.Request) (*capsulev1beta2.Tenant, error) {
func (h *TypedTenantWithRulesetHandler[T]) resolveTenant(
ctx context.Context,
c client.Reader,
req admission.Request,
) (*capsulev1beta2.Tenant, error) {
if req.Namespace == "" {
return nil, nil
}
@@ -137,15 +183,15 @@ func (h *TypedTenantWithRulesetHandler[T]) resolveTenant(ctx context.Context, c
return tenant.GetTenantByNamespace(ctx, c, req.Namespace)
}
// Resolve the corresponding managed ruleset for this namespace
// If not yet present try to calculate it.
// Resolve the corresponding managed ruleset for this namespace.
// If not yet present, try to calculate it.
func (h *TypedTenantWithRulesetHandler[T]) resolveRuleset(
ctx context.Context,
c client.Reader,
req admission.Request,
namespace string,
tnt *capsulev1beta2.Tenant,
) (*api.NamespaceRuleBodyNamespace, error) {
) ([]*rules.NamespaceRuleBodyNamespace, error) {
rs := &capsulev1beta2.RuleStatus{}
key := types.NamespacedName{
Namespace: namespace,
@@ -153,7 +199,7 @@ func (h *TypedTenantWithRulesetHandler[T]) resolveRuleset(
}
if err := c.Get(ctx, key, rs); err == nil {
return &rs.Status.Rule, nil
return rs.Status.Rules, nil
} else if !apierrors.IsNotFound(err) {
return nil, err
}
+24 -12
View File
@@ -13,8 +13,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
@@ -41,21 +41,19 @@ func BuildNamespaceRuleBodyStatus(
c client.Reader,
ns *corev1.Namespace,
tnt *capsulev1beta2.Tenant,
) (*api.NamespaceRuleBodyNamespace, error) {
out := &api.NamespaceRuleBodyNamespace{}
) ([]*rules.NamespaceRuleBodyNamespace, error) {
if tnt == nil || ns == nil {
return out, nil
return nil, nil
}
// Treat nil labels map as empty.
var nsLabels labels.Set
nsLabels := labels.Set{}
if ns.Labels != nil {
nsLabels = labels.Set(ns.Labels)
} else {
nsLabels = labels.Set{}
}
out := make([]*rules.NamespaceRuleBodyNamespace, 0, len(tnt.Spec.Rules))
for i, rule := range tnt.Spec.Rules {
if rule == nil {
continue
@@ -72,11 +70,25 @@ func BuildNamespaceRuleBodyStatus(
}
}
// Merge enforce body (for now: only registries)
// Preserve order: append in the order rules are declared.
if len(rule.Enforce.Registries) > 0 {
out.Enforce.Registries = append(out.Enforce.Registries, rule.Enforce.Registries...)
normalized := rules.NamespaceRuleBodyNamespace{
Enforce: rules.NamespaceRuleEnforceBody{
Action: rule.Enforce.Action,
Registries: append(
[]rules.OCIRegistry(nil),
rule.Enforce.Registries...,
),
},
}
if normalized.Enforce.Action == "" {
normalized.Enforce.Action = rules.ActionTypeDeny
}
if len(normalized.Enforce.Registries) == 0 {
continue
}
out = append(out, &normalized)
}
return out, nil