mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
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:
co-authored by
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
Hristo Hristov
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
lnx01
CorentinPtrl
lnx01
lnx01
lnx01
lnx01
Alan
lnx01
lnx01
lnx01
lnx01
lnx01
Copilot Autofix powered by AI
lnx01
lnx01
lnx01
lnx01
Sander Tervoert
Akash Kumar
Marc Brugger
alan747271363-art
parent
e2aa5016f2
commit
327296a5b0
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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/)
|
||||
//
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Vendored
+135
@@ -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[:])
|
||||
}
|
||||
Vendored
+234
@@ -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")
|
||||
}
|
||||
}
|
||||
Vendored
+201
-28
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+565
-433
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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 := ®istryEvaluation{}
|
||||
|
||||
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 := ®istryDecision{
|
||||
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 ""
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user