Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
Repository: open-policy-agent/kube-mgmt
Branch: master
Commit: 39dabfd6337c
Files: 101
Total size: 250.2 KB
Directory structure:
gitextract_9hv5t40p/
├── .dockerignore
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── build.yaml
│ ├── cache.yaml
│ └── release.yaml
├── .gitignore
├── .ko.yaml
├── LICENSE
├── README.md
├── charts/
│ └── opa-kube-mgmt/
│ ├── Chart.yaml
│ ├── README.md
│ ├── templates/
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── ingressroute.yaml
│ │ ├── mgmt-token-secret.yaml
│ │ ├── poddisruptionbudget.yaml
│ │ ├── rbac-mgmt-replicate.yaml
│ │ ├── rbac-mgmt.yaml
│ │ ├── rbac-sar.yaml
│ │ ├── secret-opa-config.yaml
│ │ ├── service.yaml
│ │ ├── serviceaccount.yaml
│ │ ├── servicemonitor.yaml
│ │ └── webhookconfiguration.yaml
│ ├── values.schema.json
│ └── values.yaml
├── cmd/
│ └── kube-mgmt/
│ ├── flag.go
│ ├── flag_test.go
│ └── main.go
├── devbox.json
├── devspace.yaml
├── docs/
│ ├── admission-control-1.7.md
│ ├── admission-control-crd.md
│ ├── admission-control-secure.md
│ └── tls-1.7.md
├── examples/
│ └── service_validation/
│ ├── README.md
│ ├── admission_controller.yaml
│ └── install.sh
├── go.mod
├── go.sum
├── internal/
│ └── expect/
│ ├── client.go
│ ├── request.go
│ └── script.go
├── justfile
├── pkg/
│ ├── configmap/
│ │ └── configmap.go
│ ├── data/
│ │ ├── generic.go
│ │ ├── generic_test.go
│ │ └── types.go
│ ├── dynamicdata/
│ │ ├── dynamicdata.go
│ │ └── dynamicdata_test.go
│ ├── opa/
│ │ ├── opa.go
│ │ └── opa_test.go
│ ├── types/
│ │ └── types.go
│ └── version/
│ └── version.go
└── test/
├── e2e/
│ ├── custom_config/
│ │ ├── 1_bundle_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── custom_mgmt_token/
│ │ ├── 1_policy_loaded.hurl
│ │ ├── 2_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── default/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policy_loaded.hurl
│ │ ├── 3_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── fixture-labels.yaml
│ ├── fixture-multi.yaml
│ ├── fixture-replication.yaml
│ ├── fixture.yaml
│ ├── labels/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policy_loaded.hurl
│ │ ├── 3_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── multi/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policies_loaded.hurl
│ │ ├── 3_policy_unloaded.hurl
│ │ ├── 4_policies_reloaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── replicate/
│ │ ├── 1_replication.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ └── replicate_auto/
│ ├── .gitignore
│ ├── 1_replication.hurl
│ ├── bundle/
│ │ ├── .manifest
│ │ └── main.rego
│ ├── chainsaw-test.yaml
│ └── values.yaml
├── lint/
│ ├── images.yaml
│ ├── sa.yaml
│ ├── service.yaml
│ └── tsc.yaml
└── unit/
├── health.yaml
├── kube-mgmt_args.yaml
├── rbac_cm.yaml
├── rbac_replicate.yaml
├── sa.yaml
├── service.yaml
└── tsc.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# exclude everything, then re-include only what the Go build needs
*
!go.mod
!go.sum
!cmd/
!pkg/
!internal/
================================================
FILE: .editorconfig
================================================
root = true
[*.{sh,yaml,md}]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
max_line_length = 120
[justfile]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
max_line_length = 120
================================================
FILE: .github/workflows/build.yaml
================================================
name: Build
on:
workflow_dispatch:
push:
paths-ignore:
- "docs/**"
- "logo/**"
- "examples/**"
- "README.md"
- "charts/opa-kube-mgmt/README.md"
branches:
- "master"
pull_request:
branches:
- "master"
- "feat/*"
- "fix/*"
jobs:
build_job:
name: Build
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- uses: actions/checkout@v6
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/go/bin
~/.local/share/helm/plugins
key: go-tools-${{ hashFiles('devbox.json', 'go.mod', 'go.sum') }}
restore-keys: |
go-tools-
- uses: jetify-com/devbox-install-action@v0.15.0
with:
enable-cache: true
- name: lint and unit test
run: devbox run -- just test
- name: publish helm lint report
uses: mikepenz/action-junit-report@v6
if: always()
with:
report_paths: "build/test-results/helm-unittest/lint.xml"
check_name: "Helm Lint Tests"
fail_on_failure: true
detailed_summary: true
include_passed: true
- name: publish helm unit test report
uses: mikepenz/action-junit-report@v6
if: always()
with:
report_paths: "build/test-results/helm-unittest/unit.xml"
check_name: "Helm Unit Tests"
fail_on_failure: true
detailed_summary: true
include_passed: true
- name: e2e test
run: devbox run -- just all && devbox run -- just test-e2e-all
- name: publish e2e test report
uses: mikepenz/action-junit-report@v6
if: always()
with:
report_paths: "build/test-results/chainsaw/*.xml"
check_name: "E2E Tests"
fail_on_failure: true
detailed_summary: true
include_passed: true
- name: failure logs
if: ${{ failure() }}
run: |
echo "---------------------------------------"
kubectl get all
echo "---------------------------------------"
kubectl describe po kube-mgmt-opa-kube-mgmt || true
echo "---------------------------------------"
kubectl logs -l app=kube-mgmt-opa-kube-mgmt -c opa --tail=-1
echo "---------------------------------------"
kubectl logs -l app=kube-mgmt-opa-kube-mgmt -c mgmt --tail=-1
echo "---------------------------------------"
================================================
FILE: .github/workflows/cache.yaml
================================================
name: Update cache
on:
push:
branches:
- master
workflow_dispatch:
schedule:
- cron: '0 6 */6 * *'
jobs:
update_cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/go/bin
~/.local/share/helm/plugins
key: go-tools-${{ hashFiles('devbox.json', 'go.mod', 'go.sum') }}
restore-keys: |
go-tools-
- uses: jetify-com/devbox-install-action@v0.15.0
with:
enable-cache: true
- run: |
eval "$(devbox shellenv -c . --init-hook)"
devbox run -- go mod download
================================================
FILE: .github/workflows/release.yaml
================================================
name: Release
permissions:
packages: write
contents: write
on:
workflow_dispatch: {}
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
docker_job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jetify-com/devbox-install-action@v0.15.0
with:
enable-cache: true
- uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: build and publish image, create chart archive
run: devbox run -- devspace build --profile release
- name: upload helm artifact
uses: actions/upload-artifact@v7
with:
name: "helm"
path: "opa-kube-mgmt-*.tgz"
helm_job:
runs-on: ubuntu-latest
needs: docker_job
steps:
- uses: actions/checkout@v6
with:
ref: gh-pages
- name: download helm artifact
uses: actions/download-artifact@v8
id: download
with:
name: helm
path: /tmp/helm
- name: update helm index
run: |
helm repo index /tmp/helm --merge ./charts/index.yaml
mv -f /tmp/helm/* ./charts
- name: publish index and chart
uses: actions-js/push@v1.5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
================================================
FILE: .gitignore
================================================
./kube-mgmt
bin
.go
*.tgz
.idea
.vscode/settings.json
.devspace/
build/
================================================
FILE: .ko.yaml
================================================
defaultBaseImage: alpine:3.23.4
builds:
- id: kube-mgmt
main: ./cmd/kube-mgmt
ldflags:
- -X github.com/open-policy-agent/kube-mgmt/pkg/version.Version={{.Env.KO_VERSION}}
- -X github.com/open-policy-agent/kube-mgmt/pkg/version.Git={{.Env.KO_COMMIT}}
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
#  kube-mgmt
`kube-mgmt` manages policies / data of [Open Policy Agent](https://github.com/open-policy-agent/opa)
instances in Kubernetes.
Use `kube-mgmt` to:
* Load policies and/or static data into OPA instance from `ConfigMap`.
* Replicate Kubernetes resources
including [CustomResourceDefinitions (CRDs)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) into OPA instance.
## Deployment Guide
Both `OPA` and `kube-mgmt` can be installed using [opa-kube-mgmt](
https://artifacthub.io/packages/helm/opa-kube-mgmt/opa-kube-mgmt) Helm chart.
Follow [README](charts/opa-kube-mgmt/README.md) to install it into K8s cluster.
## Policies and data loading
`kube-mgmt` automatically discovers policies and JSON data
stored in `ConfigMaps` in Kubernetes and loads them into OPA.
`kube-mgmt` assumes a `ConfigMap` contains policy or JSON data if the `ConfigMap` is:
- Created in a namespace listed in the `--namespaces` option.
If you specify `--namespaces=*` then `kube-mgmt` will look for policies in ALL namespaces.
- Labelled with `openpolicyagent.org/policy=rego` for policies
- Labelled with `openpolicyagent.org/data=opa` for JSON data
Policies or data discovery and loading can be disabled using `--enable-policy=false` or `--enable-data=false` flags respectively.
Label names and their values can be configured using `--policy-label`, `--policy-value`, `--data-label`, `--data-value` CLI options.
When a `ConfigMap` has been successfully loaded into OPA,
the `openpolicyagent.org/kube-mgmt-status` annotation is set to `{"status": "ok"}`.
If loading fails for some reason (e.g., because of a parse error), the
`openpolicyagent.org/kube-mgmt-status` annotation is set to `{"status": "error", "error": ...}`
where the `error` field contains details about the failure.
Data loaded out of ConfigMaps is laid out as follows:
```
<namespace>/<name>/<key>
```
For example, if the following ConfigMap was created:
```yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: hello-data
namespace: opa
labels:
openpolicyagent.org/data: opa
data:
x.json: |
{"a": [1,2,3,4]}
```
Note: "x.json" may be any key.
You could refer to the data inside your policies as follows:
```rego
data.opa["hello-data"]["x.json"].a[0] # evaluates to 1
```
## K8s resource replication
> [!WARNING]
> K8s resource replication requires global cluster permission with `ClusterRole` and `ClusterRoleBinding`.
`kube-mgmt` can be configured to replicate Kubernetes resources into OPA so that
you can express policies over an eventually consistent cache of Kubernetes
state.
Replication is enabled with the following options:
```bash
# Replicate namespace-level resources. May be specified multiple times.
--replicate=<[group/]version/resource>
# Replicate cluster-level resources. May be specified multiple times.
--replicate-cluster=<[group/]version/resource>
```
By default resources are replicated from all namespaces.
Use `--replicate-ignore-namespaces` option to exclude particular namespaces from replication.
Kubernetes resources replicated into OPA are laid out as follows:
```
<replicate-path>/<resource>/<namespace>/<name> # namespace scoped
<replicate-path>/<resource>/<name> # cluster scoped
```
- `<replicate-path>` is configurable (via `--replicate-path`) and
defaults to `kubernetes`.
- `<resource>` is the Kubernetes resource plural, e.g., `nodes`,
`pods`, `services`, etc.
- `<namespace>` is the namespace of the Kubernetes resource.
- `<name>` is the name of the Kubernetes resource.
For example, to search for services with the label `"foo"` you could write:
```
some namespace, name
service := data.kubernetes.services[namespace][name]
service.metadata.labels["foo"]
```
An alternative way to visualize the layout is as single JSON document:
```json
{
"kubernetes": {
"services": {
"default": {
"example-service": {...},
"another-service": {...},
}
}
}
}
}
```
The example below would replicate Deployments, Services, and Nodes into OPA:
```bash
--replicate=apps/v1beta/deployments
--replicate=v1/services
--replicate-cluster=v1/nodes
```
Custom Resource Definitions can also be replicated using the same `--replicate` and `--replicate-cluster` options.
## Admission Control
To get started with admission control policy enforcement in Kubernetes 1.9 or later see the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial. For older versions of Kubernetes, see [Admission Control (1.7)](./docs/admission-control-1.7.md).
In the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial, OPA is **NOT** running with an authorization policy configured and hence clients can read and write policies in OPA. When deploying OPA in an insecure environment, it is recommended to configure `authentication` and `authorization` on the OPA daemon. For an example of how OPA can be securely deployed as an admission controller see [Admission Control Secure](./docs/admission-control-secure.md).
## OPA API Endpoints and Least-privilege Configuration
`kube-mgmt` is a privileged component that can load policy and data into OPA.
Other clients connecting to the OPA API only need to query for policy decisions.
To load policy and data into OPA, `kube-mgmt` uses the following OPA API
endpoints:
* `PUT v1/policy/<path>` - upserting policies
* `DELETE v1/policy/<path>` - deleting policies
* `PUT v1/data/<path>` - upserting data
* `PATCH v1/data/<path>` - updating and removing data
Many users configure OPA with a simple API authorization policy that restricts
access to the OPA APIs:
```rego
package system.authz
# Deny access by default.
default allow = false
# Allow anonymous access to decision `data.example.response`
#
# NOTE: the specific decision differs depending on your policies.
# NOTE: depending on how callers are configured, they may only require this or the default decision below.
allow {
input.path == ["v0", "data", "example", "response"]
input.method == "POST"
}
# Allow anonymous access to default decision.
allow {
input.path == [""]
input.method == "POST"
}
# This is only used for health check in liveness and readiness probe
allow {
input.path == ["health"]
input.method == "GET"
}
# This is only used for prometheus metrics
allow {
input.path == ["metrics"]
input.method == "GET"
}
# This is used by kube-mgmt to PUT/PATCH against /v1/data and PUT/DELETE against /v1/policies.
#
# NOTE: The $TOKEN value is replaced at deploy-time with the actual value that kube-mgmt will use. This is typically done by an initContainer.
allow {
input.identity == "$TOKEN"
}
```
## Development
### Environment setup
This project uses [devbox](https://www.jetify.com/docs/devbox/installing-devbox/) to provide a fully isolated,
reproducible development environment. All required tools (Go, just, OPA CLI, staticcheck, and others)
are managed by devbox at pinned versions — no manual installation needed.
To enter the development shell:
```bash
devbox shell
```
This project uses `just` as a command runner, configured in [justfile](./justfile).
Run `just` without arguments to list all available recipes.
### Running the application locally
`kube-mgmt` runs in a local [k3d](https://k3d.io) Kubernetes cluster. Create the cluster once before first use:
```bash
just all
```
Start and stop `kube-mgmt` application with:
```bash
just up
just down
```
Delete local k8s cluster
```sh
just 3d-down
```
### Tests
The project has three categories of tests.
#### Go unit tests
Standard Go tests using the `testing` package:
```bash
just test-go
```
#### Helm chart unit tests
Chart rendering tests implemented with the [helm-unittest](https://github.com/helm-unittest/helm-unittest) plugin:
```bash
just test-helm
```
#### End-to-end tests
E2E tests deploy `kube-mgmt` to the local k3d cluster via [devspace](https://devspace.sh) and validate behavior using
[chainsaw](https://kyverno.github.io/chainsaw/) (Kubernetes-native test framework) and [hurl](https://hurl.dev)
(HTTP assertions). Each scenario is a directory under `test/e2e/`.
Run a single scenario (shows an interactive picker when no argument is given):
```bash
just test-e2e [test/e2e/<scenario>]
```
Run all scenarios sequentially:
```bash
just test-e2e-all
```
#### Linting
```bash
just lint
```
Runs `go vet` and [staticcheck](https://staticcheck.io) for Go code, and helm-unittest lint rules for the Helm chart.
#### Run all checks
```bash
just test
```
Runs lint, Go unit tests, and Helm chart unit tests.
### Release
To release a new version, create a [GitHub release](https://github.com/open-policy-agent/kube-mgmt/releases)
with a tag that follows the [semantic versioning convention](https://semver.org/).
Once the tag is pushed, the CI pipeline automatically builds and publishes all release artifacts:
Docker images for all supported architectures and the Helm chart.
================================================
FILE: charts/opa-kube-mgmt/Chart.yaml
================================================
apiVersion: v1
appVersion: 0.0.0 # managed by git tag
version: 0.0.0 # managed by git tag
description: Manage OPA in Kubernetes with kube-mgmt sidecar.
name: opa-kube-mgmt
keywords:
- opa
- admission control
- policy
- kubernetes
- security
home: https://www.openpolicyagent.org
icon: https://raw.githubusercontent.com/open-policy-agent/opa/master/logo/logo.png
annotations:
artifacthub.io/links: |
- name: OPA source code
url: https://github.com/open-policy-agent/opa
- name: kube-mgmt source code
url: https://github.com/open-policy-agent/kube-mgmt
================================================
FILE: charts/opa-kube-mgmt/README.md
================================================
# Manage OPA in Kubernetes with kube-mgmt sidecar.
[OPA](https://www.openpolicyagent.org) is an open-source general-purpose policy
engine designed for cloud-native environments.
## Overview
This helm chart installs `OPA` together with `kube-mgmt` sidecar,
that allows to manage OPA policies and data via Kubernetes ``ConfigMaps`.
Optionally, the chart allows to install a [Kubernetes admission
controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/).
## Installation
### Prerequisites
- Kubernetes 1.9 (or newer) for validating and mutating webhook admission
controller support.
- Optional, cert-manager (https://docs.cert-manager.io/en/latest/)
### Default Installation
If you just want to see something run, install the chart with default configuration.
```sh
helm repo add opa https://open-policy-agent.github.io/kube-mgmt/charts
helm repo update
helm upgrade -i -n opa --create-namespace opa opa/opa-kube-mgmt
```
Once installed, the OPA will download a sample bundle from https://www.openpolicyagent.org.
It contains a simple policy that restricts the hostnames that can be specified on Ingress objects created in the
`opa-example` namespace.
You can download the bundle and inspect it yourself:
```sh
mkdir example && cd example
curl -s -L https://www.openpolicyagent.org/bundles/kubernetes/admission | tar xzv
```
### Installation from GitHub Packages (GHCR)
The Helm chart and Docker image are also published to GitHub Container Registry (GHCR).
Install the chart using OCI:
```sh
helm upgrade -i -n opa --create-namespace opa \
oci://ghcr.io/open-policy-agent/helm/opa-kube-mgmt --version <version>
```
The `kube-mgmt` Docker image is also published to GHCR. To pull it directly:
```sh
# latest
docker pull ghcr.io/open-policy-agent/docker/opa-kube-mgmt:latest
# specific version
docker pull ghcr.io/open-policy-agent/docker/opa-kube-mgmt:<version>
```
To use the GHCR image when installing the chart:
```sh
helm upgrade -i -n opa --create-namespace opa \
oci://ghcr.io/open-policy-agent/helm/opa-kube-mgmt \
--set mgmt.image.repository=ghcr.io/open-policy-agent/docker/opa-kube-mgmt \
--set mgmt.image.tag=latest
```
## Configuration
All configuration settings are contained and described in [values.yaml](values.yaml).
You should set the URL and credentials for the OPA to use to download policies.
The URL should identify an HTTP endpoint that implements the [OPA Bundle
API](https://www.openpolicyagent.org/docs/bundles.html).
- `opa.services.controller.url` specifies the base URL of the OPA control plane.
- `opa.services.controller.credentials.bearer.token` specifies a bearer token
for the OPA to use to authenticate with the control plane.
For more information on OPA-specific configuration see the [OPA Configuration
Reference](https://www.openpolicyagent.org/docs/configuration.html).
================================================
FILE: charts/opa-kube-mgmt/templates/_helpers.tpl
================================================
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "opa.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "opa.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "opa.sarfullname" -}}
{{- $name := (include "opa.fullname" . | trunc 59 | trimSuffix "-") -}}
{{- printf "%s-sar" $name -}}
{{- end -}}
{{- define "opa.mgmtfullname" -}}
{{- $name := (include "opa.fullname" . | trunc 58 | trimSuffix "-") -}}
{{- printf "%s-mgmt" $name -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "opa.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Define standard labels for frequently used metadata.
*/}}
{{- define "opa.labels.standard" -}}
app: {{ template "opa.fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "opa.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "opa.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}
{{- define "opa.selfSignedIssuer" -}}
{{ printf "%s-selfsign" (include "opa.fullname" .) }}
{{- end -}}
{{- define "opa.rootCAIssuer" -}}
{{ printf "%s-ca" (include "opa.fullname" .) }}
{{- end -}}
{{- define "opa.rootCACertificate" -}}
{{ printf "%s-ca" (include "opa.fullname" .) }}
{{- end -}}
{{- define "opa.servingCertificate" -}}
{{ printf "%s-webhook-tls" (include "opa.fullname" .) }}
{{- end -}}
{{/*
Detect the version of cert manager crd that is installed
Error if CRD is not available
*/}}
{{- define "opa.certManagerApiVersion" -}}
{{- if (.Capabilities.APIVersions.Has "cert-manager.io/v1") -}}
cert-manager.io/v1
{{- else if (.Capabilities.APIVersions.Has "cert-manager.io/v1beta1") -}}
cert-manager.io/v1beta1
{{- else -}}
{{- fail "cert-manager CRD does not appear to be installed" }}
{{- end -}}
{{- end -}}
{{/*
Detect the available version of admissionregistration
*/}}
{{- define "opa.admissionregistrationApiVersion" -}}
{{- if (.Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1") -}}
admissionregistration.k8s.io/v1
{{- else -}}
admissionregistration.k8s.io/v1beta1
{{- end -}}
{{- end -}}
{{- define "opa.mgmt.image" -}}
{{- $tag := .Values.mgmt.image.tag | default .Chart.AppVersion -}}
{{ printf "%s:%s" .Values.mgmt.image.repository $tag }}
{{- end -}}
{{- define "opa.dnsPolicy" -}}
{{- if .Values.dnsPolicyOverride -}}
dnsPolicy: "{{ .Values.dnsPolicyOverride }}"
{{ else if .Values.hostNetwork.enabled -}}
dnsPolicy: "ClusterFirstWithHostNet"
{{ end -}}
{{ end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "opa.fullname" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ template "opa.fullname" . }}
{{- with .Values.deploymentStrategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
metadata:
annotations:
{{- if .Values.opa }}
checksum/config: {{ tpl (toYaml .Values.opa) . | sha256sum }}
{{- end }}
{{- if .Values.admissionController.enabled }}
checksum/webhookconfiguration: {{ include (print $.Template.BasePath "/webhookconfiguration.yaml" ) . | sha256sum }}
{{- end }}
{{- if .Values.annotations }}
{{ toYaml .Values.annotations | indent 8 }}
{{- end }}
labels:
app: {{ template "opa.fullname" . }}
name: {{ template "opa.fullname" . }}
spec:
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- if or .Values.authz.enabled .Values.bootstrapPolicies}}
initContainers:
- name: initpolicy
image: {{ include "opa.mgmt.image" . }}
imagePullPolicy: {{ .Values.mgmt.image.pullPolicy }}
resources:
{{ toYaml .Values.mgmt.resources | indent 12 }}
command:
- /bin/sh
- -c
- |
{{- if .Values.authz.enabled }}
{{- if .Values.authz.mgmtToken}}
cat /mgmt-token-secret/mgmt-token > /bootstrap/mgmt-token
{{- else }}
tr -dc 'A-F0-9' < /dev/urandom | dd bs=1 count=32 2>/dev/null > /bootstrap/mgmt-token
{{- end }}
TOKEN=`cat /bootstrap/mgmt-token`
cat > /bootstrap/authz.rego <<EOF
package system.authz
import rego.v1
default allow := false
# Allow anonymous access to the default policy decision.
allow if { input.path = [""]; input.method == "POST" }
allow if { input.path = [""]; input.method == "GET" }
# This is only used for health check in liveness and readiness probe
allow if { input.path = ["health"]; input.method == "GET" }
{{- if .Values.prometheus.enabled }}
# This allows metrics to be scraped by prometheus
allow if { input.path = ["metrics"]; input.method == "GET" }
{{- end }}
allow if { input.identity == "$TOKEN" }
EOF
{{- end }}
{{- range $policyName, $policy := .Values.bootstrapPolicies }}
cat > /bootstrap/{{ $policyName }}.rego <<EOF
{{ $policy | indent 12 }}
EOF
{{- end }}
volumeMounts:
- name: bootstrap
mountPath: /bootstrap
{{- if .Values.authz.mgmtToken}}
- name: mgmt-token-secret
mountPath: /mgmt-token-secret
readOnly: true
{{- end }}
{{- end }}
{{- if .Values.hostNetwork.enabled }}
hostNetwork: true
{{- end }}
{{- include "opa.dnsPolicy" . | nindent 6 -}}
containers:
- name: opa
ports:
- name: opa
containerPort: {{ .Values.port }}
{{- if .Values.prometheus.enabled }}
- name: diag
containerPort: {{ .Values.prometheus.port }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- if .Values.extraEnv }}
{{ toYaml .Values.extraEnv | indent 12 }}
{{- end }}
resources:
{{ toYaml .Values.resources | indent 12 }}
args:
- "run"
- "--server"
{{- if .Values.opa }}
- "--config-file=/config/config.yaml"
{{- end }}
{{- if .Values.useHttps }}
- "--tls-cert-file=/certs/tls.crt"
- "--tls-private-key-file=/certs/tls.key"
{{- end }}
- "--addr=0.0.0.0:{{ .Values.port }}"
- "--log-level={{ .Values.logLevel }}"
- "--log-format={{ .Values.logFormat }}"
{{- if .Values.authz.enabled }}
- "--authentication=token"
- "--authorization=basic"
- "--ignore=.*"
{{- end }}
{{- if .Values.prometheus.enabled }}
- "--diagnostic-addr=http://0.0.0.0:{{ .Values.prometheus.port }}"
{{- end }}
{{- if or .Values.authz.enabled .Values.bootstrapPolicies }}
- "/bootstrap"
{{- end }}
{{- range .Values.extraArgs }}
- {{ . }}
{{- end }}
volumeMounts:
{{- if .Values.useHttps }}
- name: certs
readOnly: true
mountPath: /certs
{{- end }}
{{- if .Values.opa }}
- name: config
readOnly: true
mountPath: /config
{{- end }}
{{- if or .Values.authz.enabled .Values.bootstrapPolicies }}
- name: bootstrap
readOnly: true
mountPath: /bootstrap
{{- end }}
{{- if .Values.extraVolumeMounts }}
{{ toYaml .Values.extraVolumeMounts | indent 12}}
{{- end }}
readinessProbe:
httpGet:
path: /health
scheme: {{ .Values.useHttps | ternary "HTTPS" "HTTP" }}
port: opa
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
scheme: {{ .Values.useHttps | ternary "HTTPS" "HTTP" }}
port: opa
initialDelaySeconds: 10
periodSeconds: 15
{{- if .Values.mgmt.enabled }}
- name: mgmt
image: {{ include "opa.mgmt.image" . }}
imagePullPolicy: {{ .Values.mgmt.image.pullPolicy }}
startupProbe:
{{ toYaml .Values.mgmt.startupProbe | nindent 12 }}
env:
{{- if .Values.mgmt.extraEnv }}
{{ toYaml .Values.mgmt.extraEnv | indent 12 }}
{{- end }}
resources:
{{ toYaml .Values.mgmt.resources | nindent 12 }}
args:
{{- if .Values.authz.enabled }}
- --opa-auth-token-file=/bootstrap/mgmt-token
{{- end }}
- --opa-url={{ .Values.useHttps | ternary "https" "http" }}://127.0.0.1:{{ .Values.port }}/v1
- --opa-allow-insecure
- "--namespaces={{ coalesce .Values.mgmt.namespaces (list .Release.Namespace) | join "," }}"
- "--enable-data={{ .Values.mgmt.data.enabled }}"
- "--enable-policies={{ .Values.mgmt.policies.enabled }}"
- "--replicate-path={{ .Values.mgmt.replicate.path }}"
{{- range .Values.mgmt.replicate.namespace }}
- "--replicate={{ . }}"
{{- end }}
{{- range .Values.mgmt.replicate.cluster }}
- "--replicate-cluster={{ . }}"
{{- end }}
{{- if .Values.mgmt.replicate.ignoreNs }}
- "--replicate-ignore-namespaces={{ .Values.mgmt.replicate.ignoreNs | join "," }}"
{{- else }}
- "--replicate-ignore-namespaces="
{{- end }}
{{- if .Values.mgmt.replicate.auto }}
- "--opa-config=/config/config.yaml"
- "--health-endpoint=0.0.0.0:8000"
{{- end }}
{{- range .Values.mgmt.extraArgs }}
- {{ . }}
{{- end }}
volumeMounts:
{{- if or .Values.authz.enabled .Values.bootstrapPolicies }}
- name: bootstrap
readOnly: true
mountPath: /bootstrap
{{- end }}
{{- if .Values.opa }}
- name: config
readOnly: true
mountPath: /config
{{- end }}
{{- if .Values.extraVolumeMounts }}
{{ toYaml .Values.extraVolumeMounts | indent 12}}
{{- end }}
{{- if .Values.mgmt.replicate.auto }}
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 15
{{- end }}
{{- end }}
{{- if .Values.sar.enabled }}
- name: sarproxy
image: {{ .Values.sar.image.repository }}:{{ .Values.sar.image.tag }}
imagePullPolicy: {{ .Values.sar.image.pullPolicy }}
resources:
{{ toYaml .Values.sar.resources | indent 12 }}
command:
- kubectl
- proxy
- --accept-paths=^/apis/authorization.k8s.io/v1/subjectaccessreviews$
{{- end }}
{{- if .Values.extraContainers }}
{{ toYaml .Values.extraContainers | indent 8}}
{{- end }}
{{- if .Values.securityContext.enabled }}
securityContext:
{{- range $key, $val := .Values.securityContext }}
{{- if ne $key "enabled" }}
{{ $key }}: {{ toYaml $val | nindent 10 }}
{{- end }}
{{- end }}
{{- end }}
serviceAccountName: {{ template "opa.serviceAccountName" .}}
volumes:
{{- if .Values.useHttps }}
- name: certs
secret:
secretName: {{ template "opa.fullname" . }}-cert
{{- end }}
{{- if .Values.opa }}
- name: config
secret:
secretName: {{ template "opa.fullname" . }}-config
{{- end }}
{{- if or .Values.authz.enabled .Values.bootstrapPolicies}}
- name: bootstrap
emptyDir: {}
{{- if .Values.authz.mgmtToken}}
- name: mgmt-token-secret
secret:
secretName: {{.Values.authz.mgmtToken.secretName}}
items:
- key: {{ .Values.authz.mgmtToken.secretKey | default "mgmtToken" }}
path: mgmt-token
{{- end }}
{{- end }}
{{- if .Values.extraVolumes }}
{{ toYaml .Values.extraVolumes | indent 8}}
{{- end }}
affinity:
{{ toYaml .Values.affinity | indent 8 }}
nodeSelector:
{{ toYaml .Values.nodeSelector | indent 8 }}
tolerations:
{{ toYaml .Values.tolerations | indent 8 }}
topologySpreadConstraints:
{{ toYaml .Values.topologySpreadConstraints | indent 8 }}
================================================
FILE: charts/opa-kube-mgmt/templates/ingressroute.yaml
================================================
{{- if .Values.e2e }}
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: {{ include "opa.fullname" . }}-websecure
spec:
entryPoints:
- websecure
routes:
- match: HostSNI(`*`)
services:
- name: {{ include "opa.fullname" . }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.port }}
tls:
passthrough: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: {{ include "opa.fullname" . }}-web
spec:
entryPoints:
- web
routes:
- match: HostSNI(`*`)
services:
- name: {{ include "opa.fullname" . }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.port }}
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/templates/mgmt-token-secret.yaml
================================================
{{- if .Values.e2eMgmtTokenSecret -}}
apiVersion: v1
kind: Secret
metadata:
name: mgmt-token-secret
type: Opaque
stringData:
mgmtToken: mgmt-token-secret-value
{{- end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/poddisruptionbudget.yaml
================================================
{{- if .Values.podDisruptionBudget.enabled }}
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
name: {{ template "opa.fullname" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
app: {{ template "opa.fullname" . }}
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/templates/rbac-mgmt-replicate.yaml
================================================
{{- if and .Values.rbac.create .Values.mgmt.enabled -}}
{{- $arr := concat .Values.mgmt.replicate.cluster .Values.mgmt.replicate.namespace -}}
{{- if or (gt (len $arr) 0) .Values.mgmt.replicate.auto }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: "{{ template "opa.mgmtfullname" . }}-repl"
rules:
{{- with .Values.rbac.extraRules }}
{{ . | toYaml | nindent 2 }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: "{{ template "opa.mgmtfullname" . }}-repl"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: "{{ template "opa.mgmtfullname" . }}-repl"
subjects:
- kind: ServiceAccount
name: {{ template "opa.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/rbac-mgmt.yaml
================================================
{{- define "opa.rbac.cm.rules" -}}
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "update", "patch"]
{{- end -}}
{{- if and .Values.rbac.create .Values.mgmt.enabled -}}
{{- if eq (.Values.mgmt.namespaces | join ",") "*" }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" . }}
{{ include "opa.rbac.cm.rules" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "opa.mgmtfullname" . }}
subjects:
- kind: ServiceAccount
name: {{ template "opa.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- else if and (eq (kindOf .Values.mgmt.namespaces) "slice") (gt (len .Values.mgmt.namespaces) 0) }}
{{- range .Values.mgmt.namespaces }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app: {{ template "opa.name" $ }}
chart: {{ template "opa.chart" $ }}
release: {{ $.Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" $ }}
namespace: {{ . }}
{{ include "opa.rbac.cm.rules" $ }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: {{ template "opa.name" $ }}
chart: {{ template "opa.chart" $ }}
release: {{ $.Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" $ }}
namespace: {{ . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ template "opa.mgmtfullname" $ }}
subjects:
- kind: ServiceAccount
name: {{ template "opa.serviceAccountName" $ }}
namespace: {{ $.Release.Namespace }}
{{- end }}
{{- else }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" . }}
namespace: {{ .Release.Namespace }}
{{ include "opa.rbac.cm.rules" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
release: {{ .Release.Name }}
component: mgmt
name: {{ template "opa.mgmtfullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ template "opa.mgmtfullname" . }}
subjects:
- kind: ServiceAccount
name: {{ template "opa.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/rbac-sar.yaml
================================================
{{- if (and .Values.rbac.create .Values.sar.enabled) -}}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
component: sar
name: {{ template "opa.sarfullname" . }}
rules:
- apiGroups:
- "authorization.k8s.io"
resources:
- subjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
component: sar
name: {{ template "opa.sarfullname" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ template "opa.sarfullname" . }}
subjects:
- kind: ServiceAccount
name: {{ template "opa.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/secret-opa-config.yaml
================================================
{{- if .Values.opa -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "opa.fullname" . }}-config
labels:
{{ include "opa.labels.standard" . | indent 4 }}
type: Opaque
data:
config.yaml: {{ toYaml .Values.opa | b64enc }}
{{- end -}}
================================================
FILE: charts/opa-kube-mgmt/templates/service.yaml
================================================
kind: Service
apiVersion: v1
metadata:
name: {{ template "opa.fullname" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
app: {{ template "opa.fullname" . }}
ports:
- name: opa
port: {{ .Values.port }}
targetPort: opa
{{- if .Values.prometheus.enabled }}
- name: diag
port: {{ .Values.prometheus.port }}
targetPort: diag
{{- end }}
{{- if .Values.extraPorts }}
{{ toYaml .Values.extraPorts | indent 2}}
{{- end }}
{{- if .Values.service.trafficDistribution }}
trafficDistribution: {{ .Values.service.trafficDistribution }}
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/templates/serviceaccount.yaml
================================================
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "opa.serviceAccountName" .}}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{ toYaml . }}
{{- end }}
labels:
app: {{ template "opa.fullname" . }}
chart: {{ template "opa.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/templates/servicemonitor.yaml
================================================
{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.prometheus.enabled .Values.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app: {{ template "opa.name" . }}
chart: {{ template "opa.chart" . }}
heritage: {{ .Release.Service }}
{{- if not .Values.serviceMonitor.additionalLabels.release }}
release: {{ .Release.Name }}
{{- end }}
{{- if .Values.serviceMonitor.additionalLabels }}
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4}}
{{- end }}
name: {{ template "opa.fullname" . }}
{{- if .Values.serviceMonitor.namespace }}
namespace: {{ .Values.serviceMonitor.namespace }}
{{- end }}
spec:
endpoints:
- port: diag
interval: {{ .Values.serviceMonitor.interval }}
jobLabel: {{ template "opa.fullname" . }}
namespaceSelector:
matchNames:
- {{ .Release.Namespace }}
selector:
matchLabels:
app: {{ template "opa.fullname" . }}
release: {{ .Release.Name }}
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/templates/webhookconfiguration.yaml
================================================
{{- $cn := printf "%s.%s.svc" ( include "opa.fullname" . ) .Release.Namespace }}
{{- $ca := genCA "opa-admission-ca" 3650 -}}
{{- $cert := genSignedCert $cn nil (list $cn) 3650 $ca -}}
{{- if .Values.admissionController.enabled }}
kind: {{ .Values.admissionController.kind }}
apiVersion: {{ include "opa.admissionregistrationApiVersion" . }}
metadata:
name: {{ template "opa.fullname" . }}
annotations:
{{- if .Values.certManager.enabled }}
certmanager.k8s.io/inject-ca-from: {{ printf "%s/%s" .Release.Namespace (include "opa.rootCACertificate" .) | quote }}
cert-manager.io/inject-ca-from: {{ printf "%s/%s" .Release.Namespace (include "opa.rootCACertificate" .) | quote }}
{{- end }}
{{- if .Values.admissionController.annotations }}
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
webhooks:
- name: webhook.openpolicyagent.org
admissionReviewVersions: ["v1", "v1beta1"]
{{- with .Values.admissionController.namespaceSelector }}
namespaceSelector:
{{ toYaml . | indent 6 }}
{{ end }}
failurePolicy: {{ .Values.admissionController.failurePolicy }}
rules:
{{ toYaml .Values.admissionController.rules | indent 6 }}
clientConfig:
{{- if .Values.useHttps }}
{{- if not .Values.certManager.enabled }}
{{- if .Values.generateCerts }}
caBundle: {{ b64enc $ca.Cert }}
{{- else }}
caBundle: {{ b64enc .Values.CA }}
{{- end }}
{{- end }}
{{- end }}
service:
name: {{ template "opa.fullname" . }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.port }}
sideEffects: {{ .Values.admissionController.sideEffect }}
{{- if .Values.timeoutSeconds }}
timeoutSeconds: {{ .Values.timeoutSeconds }}
{{- end }}
{{- end }}
{{/* HTTP certificates */}}
{{- if .Values.useHttps }}
{{- if .Values.certManager.enabled }}
--- {{/* Create a selfsigned Issuer, in order to create a root CA certificate for signing webhook serving certificates */}}
apiVersion: {{ include "opa.certManagerApiVersion" . }}
kind: Issuer
metadata:
{{- if .Values.admissionController.annotations }}
annotations:
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
name: {{ include "opa.selfSignedIssuer" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
selfSigned: {}
--- {{/* Generate a CA Certificate used to sign certificates for the webhook */}}
apiVersion: {{ include "opa.certManagerApiVersion" . }}
kind: Certificate
metadata:
{{- if .Values.admissionController.annotations }}
annotations:
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
name: {{ include "opa.rootCACertificate" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
secretName: {{ include "opa.rootCACertificate" . }}
duration: {{ .Values.certManager.rootCACertificateDuration | quote }}
issuerRef:
name: {{ include "opa.selfSignedIssuer" . }}
commonName: "ca.webhook.opa"
isCA: true
--- {{/* Create an Issuer that uses the above generated CA certificate to issue certs */}}
apiVersion: {{ include "opa.certManagerApiVersion" . }}
kind: Issuer
metadata:
{{- if .Values.admissionController.annotations }}
annotations:
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
name: {{ include "opa.rootCAIssuer" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
ca:
secretName: {{ include "opa.rootCACertificate" . }}
--- {{/* Finally, generate a serving certificate for the webhook to use */}}
apiVersion: {{ include "opa.certManagerApiVersion" . }}
kind: Certificate
metadata:
{{- if .Values.admissionController.annotations }}
annotations:
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
name: {{ include "opa.servingCertificate" . }}
labels:
{{ include "opa.labels.standard" . | indent 4 }}
spec:
secretName: {{ template "opa.fullname" . }}-cert
duration: {{ .Values.certManager.servingCertificateDuration | quote }}
issuerRef:
name: {{ include "opa.rootCAIssuer" . }}
dnsNames:
- {{ include "opa.fullname" . }}
- {{ include "opa.fullname" . }}.{{ .Release.Namespace }}
- {{ include "opa.fullname" . }}.{{ .Release.Namespace }}.svc
{{- else }}
--- {{/* create static secret */}}
apiVersion: v1
kind: Secret
metadata:
{{- if .Values.admissionController.annotations }}
annotations:
{{ toYaml .Values.admissionController.annotations | indent 4 }}
{{- end }}
name: {{ template "opa.fullname" . }}-cert
labels:
app: {{ template "opa.fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
type: Opaque
data:
{{- if .Values.generateCerts }}
tls.crt: {{ b64enc $cert.Cert }}
tls.key: {{ b64enc $cert.Key }}
{{- else }}
tls.crt: {{ b64enc .Values.cert }}
tls.key: {{ b64enc .Values.key }}
{{- end }}
{{- end }}
{{- end }}
================================================
FILE: charts/opa-kube-mgmt/values.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/open-policy-agent/kube-mgmt",
"title": "kube-mgmt helm values",
"definitions": {
"image": {
"type": "object", "title": "OPA docker image configuration", "required": ["repository", "tag"],
"properties": {
"repository": {"type": "string"},
"tag": {"type": "string"},
"pullPolicy": {"type": "string", "default": "IfNotPresent"}
}
}
},
"type": "object", "required": ["image", "mgmt"], "additionalProperties": true,
"properties": {
"image": {"$ref": "#/definitions/image"},
"mgmt": {
"type": "object", "additionalProperties": true, "required": ["image", "enabled"],
"properties": {
"enabled": {"type": "boolean", "default": true},
"image": {"$ref": "#/definitions/image"}
}
},
"serviceAccount": {
"type": "object",
"properties": {
"create": {"type": "boolean", "default": true},
"annotations": {"type": "object", "additionalProperties": {"type": "string"}, "default": {}},
"name": {"type": ["string", "null"], "default": null}
}
},
"service": {
"type": "object",
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {"type": "string"},
"default": {}
},
"trafficDistribution": {
"type": ["null","string"],
"enum": ["PreferClose", "PreferSameNode", "PreferSameZone", null],
"default": null
}
}
},
"topologySpreadConstraints": {
"type": "array",
"items": {
"type": "object",
"required": ["maxSkew", "topologyKey", "whenUnsatisfiable"],
"properties": {
"maxSkew": {
"type": "integer",
"minimum": 1
},
"topologyKey": {
"type": "string",
"minLength": 1
},
"whenUnsatisfiable": {
"type": "string",
"enum": ["DoNotSchedule", "ScheduleAnyway"]
},
"labelSelector": {
"type": "object",
"properties": {
"matchLabels": {
"type": "object",
"additionalProperties": {"type": "string"}
},
"matchExpressions": {
"type": "array",
"items": {
"type": "object",
"required": ["key", "operator"],
"properties": {
"key": {"type": "string"},
"operator": {"type": "string", "enum": ["In", "NotIn", "Exists", "DoesNotExist"]},
"values": {"type": "array", "items": {"type": "string"}}
}
}
}
}
},
"matchLabelKeys": {
"type": "array",
"items": {"type": "string"}
},
"minDomains": {
"type": "integer",
"minimum": 1
},
"nodeAffinityPolicy": {
"type": "string",
"enum": ["Honor", "Ignore"]
},
"nodeTaintsPolicy": {
"type": "string",
"enum": ["Honor", "Ignore"]
}
}
},
"default": []
}
}
}
================================================
FILE: charts/opa-kube-mgmt/values.yaml
================================================
# Default values for opa.
# -----------------------
#
# OPA configuration file. See https://www.openpolicyagent.org/docs/configuration.html for more details.
opa: {}
# Setup the webhook using cert-manager
certManager:
enabled: false
rootCACertificateDuration: 43800h # 5y
servingCertificateDuration: 8760h # 1y
# Expose the prometheus scraping endpoint
prometheus:
enabled: false
port: 8182
## ServiceMonitor consumed by prometheus-operator
serviceMonitor:
## If the operator is installed in your cluster, set to true to create a Service Monitor Entry
enabled: false
interval: "15s"
## Namespace in which the service monitor is created
# namespace: monitoring
# Added to the ServiceMonitor object so that prometheus-operator is able to discover it
## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec
additionalLabels: {}
# Service configuration
service:
# Annotations to add to the service
annotations: {}
# Configure trafficDistribution if needed.
# trafficDistribution: PreferSameZone
# Annotations in the deployment template
annotations: {}
# Bootstrap policies to load upon startup
# Define policies in the form of:
# <policyName> : |-
# <regoBody>
# For example, to mask the entire input body in the decision logs:
# bootstrapPolicies:
# log: |-
# package system.log
# mask["/input"]
bootstrapPolicies: {}
# Admission controller configuration.
admissionController:
enabled: false
# To enforce mutating policies, change to MutatingWebhookConfiguration.
kind: ValidatingWebhookConfiguration
# To set annotations on all admissionController resources (Secret/Certificate/Issuer/AdmissionController)
# annotations:
# example: value
# To _fail closed_ on failures, change to Fail. During initial testing, we
# recommend leaving the failure policy as Ignore.
failurePolicy: Ignore
# Adds a namespace selector to the admission controller webhook
namespaceSelector:
matchExpressions:
- {key: openpolicyagent.org/webhook, operator: NotIn, values: [ignore]}
# SideEffectClass for the webhook, setting to NoneOnDryRun enables dry-run.
# Only None and NoneOnDryRun are permitted for admissionregistration.k8s.io/v1.
sideEffect: None
# To restrict the kinds of operations and resources that are subject to OPA
# policy checks, see the settings below. By default, all resources and
# operations are subject to OPA policy checks.
rules:
- operations: ["*"]
apiGroups: ["*"]
apiVersions: ["*"]
resources: ["*"]
# The helm Chart will automatically generate a CA and server certificate for
# the OPA. If you want to supply your own certificates, set the field below to
# false and add the PEM encoded CA certificate and server key pair below.
#
# WARNING: The common name name in the server certificate MUST match the
# hostname of the service that exposes the OPA to the apiserver. For example.
# if the service name is created in the "default" nanamespace with name "opa"
# the common name MUST be set to "opa.default.svc".
#
# If the common name is not set correctly, the apiserver will refuse to
# communicate with the OPA.
generateCerts: true
CA: ""
cert: ""
key: ""
# Controls a PodDisruptionBudget for the OPA pod. Suggested use if having opa
# always running for admission control is important
podDisruptionBudget:
enabled: false
minAvailable: 1
# maxUnavailable: 1
authz:
# Disable if you don't want authorization.
# Mostly useful for debugging.
enabled: true
# Used for setting the mgmt token used for authz instead of auto generated default
# mgmtToken:
# secretName: name of the secret
# secretKey: (optional) key from the secret - default value is: "mgmtToken"
# Use hostNetwork setting on OPA pod
hostNetwork:
enabled: false
# OPA docker image configuration.
image:
repository: openpolicyagent/opa
tag: 1.3.0
pullPolicy: IfNotPresent
# One or more secrets to be used when pulling images
imagePullSecrets: []
# - registrySecretName
# Should OPA use TLS or not.
useHttps: true
# Port to which the opa pod will bind itself,
port: 8181
extraArgs: []
# Extra environment variables to be loaded into the OPA container
extraEnv: []
mgmt:
enabled: true
image:
repository: openpolicyagent/kube-mgmt
tag: "" # appVersion is used by default, set to desired value to override
pullPolicy: IfNotPresent
extraArgs: []
extraEnv: []
resources: {}
# if empty - the current namespaces is watched
# if `*` - all namespaces are watched
namespaces: []
# kube-mgmt container will wait until OPA container comes to running state.
# Configure values for the startup probe, where kube-mgmt queries for the health
# of OPA container before it starts.
startupProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8181 # Port on which OPA is configured
scheme: HTTPS
initialDelaySeconds: 20
successThreshold: 1
timeoutSeconds: 10
data:
enabled: true
policies:
enabled: true
# NOTE IF you use these, remember to update the RBAC rules below to allow
# permissions to replicate these things
replicate:
cluster: []
# - [group/]version/resource
namespace: []
# - [group/]version/resource
path: kubernetes
ignoreNs: []
# Turn on auto-replication. kube-mgmt will apply OPA configuration file
# and analyze any configured bundles to determine which Kubernetes
# resources to replicate into OPA's in-memory store.
auto: false
# Log level for OPA ('debug', 'info', 'error') (app default=info)
logLevel: info
# Log format for OPA ('text', 'json') (app default=text)
logFormat: json
# Number of OPA replicas to deploy. OPA maintains an eventually consistent
# cache of policies and data. If you want high availability you can deploy two
# or more replicas.
replicas: 1
# To control how the OPA is scheduled on the cluster, set the affinity,
# tolerations and nodeSelector values below. For example, to deploy OPA onto
# the master nodes, 1 replica per node:
#
# affinity:
# podAntiAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# - labelSelector:
# matchExpressions:
# - key: "app"
# operator: In
# values:
# - opa
# topologyKey: "kubernetes.io/hostname"
# tolerations:
# - key: "node-role.kubernetes.io/master"
# effect: NoSchedule
# operator: Exists
# nodeSelector:
# kubernetes.io/role: "master"
affinity: {}
tolerations: []
nodeSelector: {}
# To control pod distribution across topology domains, set topologySpreadConstraints
# below.
#
# topologySpreadConstraints:
# - maxSkew: 1
# topologyKey: topology.kubernetes.io/zone
# whenUnsatisfiable: DoNotSchedule
# labelSelector:
# matchLabels:
# app: opa
topologySpreadConstraints: []
# To control the CPU and memory resource limits and requests for OPA, set the
# field below.
resources: {}
rbac:
# should ClusterRole for kube-mgmt be created
create: true
# extra rules to be added to a ClusterRole
extraRules: []
# - apiGroups: [""]
# resources: ["configmaps"]
# verbs: ["*"]
serviceAccount:
# Specifies whether a ServiceAccount should be created
create: true
# Annotations for the ServiceAccount
annotations: {}
# The name of the ServiceAccount to use.
# If not set and create is true, a name is generated using the fullname template
name:
# This proxy allows opa to make Kubernetes SubjectAccessReview checks against the
# Kubernetes API. You can get a rego function at github.com/open-policy-agent/library
sar:
enabled: false
image:
repository: lachlanevenson/k8s-kubectl
tag: latest
pullPolicy: IfNotPresent
resources: {}
# Set a priorityClass using priorityClassName
# priorityClassName:
# Timeout for a webhook call in seconds.
# Starting in kubernetes 1.14 you can set the timeout and it is
# encouraged to use a small timeout for webhooks. If the webhook call times out, the request
# the request is handled according to the webhook'sfailure policy.
# timeoutSeconds: 20
securityContext:
enabled: false
runAsNonRoot: true
runAsUser: 1
deploymentStrategy: {}
# rollingUpdate:
# maxSurge: 1
# maxUnavailable: 0
# type: RollingUpdate
extraContainers: []
## Additional containers to be added to the opa pod.
# - name: example-app
# image: example/example-app:latest
# args:
# - "run"
# - "--port=11811"
# - "--config=/etc/example-app-conf/config.yaml"
# - "--opa-endpoint=https://localhost:443"
# ports:
# - name: http
# containerPort: 11811
# protocol: TCP
# volumeMounts:
# - name: example-app-auth-config
# mountPath: /etc/example-app-conf
extraVolumes: []
## Additional volumes to the opa pod.
# - name: example-app-auth-config
# secret:
# secretName: example-app-auth-config
extraVolumeMounts: []
## Mounting config for using the additional volumes
# - name: example-app-auth-config
# mountPath: /mount/path
extraPorts: []
## Additional ports to the opa services. Useful to expose extra container ports.
# - port: 11811
# protocol: TCP
# name: http
# targetPort: http
================================================
FILE: cmd/kube-mgmt/flag.go
================================================
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package main
import (
"errors"
"fmt"
"strings"
)
type groupVersionKind struct {
Group string
Version string
Kind string
}
var errBadFormat = errors.New("format: group/version/kind")
func (gvk groupVersionKind) String() string {
if gvk.Group != "" {
return fmt.Sprintf("%v/%v/%v", gvk.Group, gvk.Version, gvk.Kind)
}
return fmt.Sprintf("%v/%v", gvk.Version, gvk.Kind)
}
func (gvk *groupVersionKind) Parse(value string) error {
parts := strings.SplitN(value, "/", 3)
for i := range parts {
if len(parts[i]) == 0 {
return errBadFormat
}
parts[i] = strings.ToLower(parts[i])
}
if len(parts) < 2 {
return errBadFormat
}
if len(parts) == 2 {
gvk.Version = parts[0]
gvk.Kind = parts[1]
} else {
gvk.Group = parts[0]
gvk.Version = parts[1]
gvk.Kind = parts[2]
}
return nil
}
type gvkFlag []groupVersionKind
func (f *gvkFlag) String() string {
return fmt.Sprint(*f)
}
func (f *gvkFlag) Set(value string) error {
var gvk groupVersionKind
if err := gvk.Parse(value); err != nil {
return err
}
*f = append(*f, gvk)
return nil
}
func (f *gvkFlag) Type() string {
return "[group/]version/resource"
}
================================================
FILE: cmd/kube-mgmt/flag_test.go
================================================
package main
import (
"errors"
"reflect"
"testing"
"github.com/open-policy-agent/kube-mgmt/pkg/configmap"
"github.com/spf13/cobra"
)
func TestFlagParsing(t *testing.T) {
var f gvkFlag
badPaths := []string{
"foo/bar/",
"foo",
}
for _, tc := range badPaths {
if err := f.Set(tc); err == nil {
t.Fatalf("Expected error from %v", tc)
}
}
expected := gvkFlag{
{"example.org", "foo", "bar"},
}
if err := f.Set("example.org/Foo/bar"); err != nil || !reflect.DeepEqual(expected, f) {
t.Fatalf("Expected %v but got: %v (err: %v)", expected, f, err)
}
expected = append(expected, groupVersionKind{"example.org", "bar", "baz"})
if err := f.Set("example.org/Bar/baz"); err != nil || !reflect.DeepEqual(expected, f) {
t.Fatalf("Expected %v but got: %v (err: %v)", expected, f, err)
}
expected = append(expected, groupVersionKind{"", "v2", "corge"})
if err := f.Set("v2/corge"); err != nil || !reflect.DeepEqual(expected, f) {
t.Fatalf("Expected %v but got: %v (err: %v)", expected, f, err)
}
}
func TestFlagString(t *testing.T) {
var f gvkFlag
expected := "[example.org/foo/bar]"
if err := f.Set("example.org/foo/bar"); err != nil || f.String() != expected {
t.Fatalf("Exepcted %v but got: %v (err: %v)", expected, f.String(), err)
}
}
func TestPolicyFlags(t *testing.T) {
tt := []struct {
name string
flag string
value string
expectFullFlag string
err error
}{
{
name: "valid",
flag: "openpolicyagent.org/policy",
value: "rego",
expectFullFlag: "openpolicyagent.org/policy=rego",
err: nil,
},
{
name: "invalidFlag",
flag: "-foo",
value: "rego",
expectFullFlag: "",
err: errors.New(`key: Invalid value: "-foo": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`),
},
{
name: "invalidValue",
flag: "foo",
value: "-rego",
expectFullFlag: "",
err: errors.New(`values[0][foo]: Invalid value: "-rego": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`),
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rootCmd := &cobra.Command{
Use: "test",
Short: "test",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
var params params
rootCmd.Flags().StringVarP(¶ms.policyLabel, "policy-label", "", "", "replace label openpolicyagent.org/policy")
rootCmd.Flags().StringVarP(¶ms.policyValue, "policy-value", "", "", "replace value rego")
rootCmd.SetArgs([]string{"--policy-label=" + tc.flag, "--policy-value=" + tc.value})
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if rootCmd.Flag("policy-label").Value.String() != "" || rootCmd.Flag("policy-value").Value.String() != "" {
err := configmap.CustomLabel(params.policyLabel, params.policyValue)
if err != nil {
if tc.err.Error() != err.Error() {
t.Errorf("exp: %v\ngot: %v\n", tc.err.Error(), err.Error())
t.FailNow()
}
}
}
return nil
}
rootCmd.Execute()
})
}
}
================================================
FILE: cmd/kube-mgmt/main.go
================================================
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"path"
"strings"
"github.com/open-policy-agent/kube-mgmt/pkg/configmap"
"github.com/open-policy-agent/kube-mgmt/pkg/data"
"github.com/open-policy-agent/kube-mgmt/pkg/dynamicdata"
"github.com/open-policy-agent/kube-mgmt/pkg/opa"
"github.com/open-policy-agent/kube-mgmt/pkg/types"
"github.com/open-policy-agent/kube-mgmt/pkg/version"
//lint:ignore SA1019 using OPA v0.x to ensure backwards compatible with pre-1.0 bundles
"github.com/open-policy-agent/opa/logging"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
type params struct {
version bool
kubeconfigFile string
opaURL string
opaAuth string
opaAuthFile string
opaCAFile string
opaAllowInsecure bool
policyLabel string
policyValue string
dataLabel string
dataValue string
enablePolicies bool
enableData bool
namespaces []string
opaConfigFile string
replicateCluster gvkFlag
replicateNamespace gvkFlag
replicatePath string
logLevel string
replicateIgnoreNs []string
analysisEntrypoint string
healthEndpoint string
}
func main() {
var params params
commandName := path.Base(os.Args[0])
rootCmd := &cobra.Command{
Use: commandName,
Short: fmt.Sprintf("%v manages OPA on top of Kubernetes", commandName),
FParseErrWhitelist: cobra.FParseErrWhitelist{
UnknownFlags: true,
},
Run: func(cmd *cobra.Command, args []string) {
if params.version {
fmt.Println("Version:", version.Version)
fmt.Println("Git:", version.Git)
} else {
run(¶ms)
}
},
}
// Miscellaenous options.
rootCmd.Flags().BoolVarP(¶ms.version, "version", "v", false, "print version and exit")
rootCmd.Flags().StringVarP(¶ms.kubeconfigFile, "kubeconfig", "", "", "set path to kubeconfig manually")
rootCmd.Flags().StringVarP(¶ms.opaURL, "opa-url", "", "http://localhost:8181/v1", "set URL of OPA API endpoint")
rootCmd.Flags().StringVarP(¶ms.opaAuth, "opa-auth-token", "", "", "set authentication token for OPA API endpoint")
rootCmd.Flags().StringVarP(¶ms.opaAuthFile, "opa-auth-token-file", "", "", "set file containing authentication token for OPA API endpoint")
rootCmd.Flags().StringVarP(¶ms.opaCAFile, "opa-ca-file", "", "", "set file containing certificate authority for OPA certificate")
rootCmd.Flags().BoolVarP(¶ms.opaAllowInsecure, "opa-allow-insecure", "", false, "allow insecure https connections to OPA")
rootCmd.Flags().StringVar(¶ms.logLevel, "log-level", "info", "set log level {debug, info, warn}")
// policy / data
rootCmd.Flags().BoolVarP(¶ms.enablePolicies, "enable-policies", "", true, "whether to automatically discover policies from labelled ConfigMaps")
rootCmd.Flags().StringVar(¶ms.policyLabel, "policy-label", "openpolicyagent.org/policy", "label name for filtering ConfigMaps with policies")
rootCmd.Flags().StringVar(¶ms.policyValue, "policy-value", "rego", "label value for filtering ConfigMaps with policies")
rootCmd.Flags().BoolVarP(¶ms.enableData, "enable-data", "", true, "whether to automatically discover data from labelled ConfigMaps")
rootCmd.Flags().StringVar(¶ms.dataLabel, "data-label", "openpolicyagent.org/data", "label name for filtering ConfigMaps with data")
rootCmd.Flags().StringVar(¶ms.dataValue, "data-value", "opa", "label value for filtering ConfigMaps with data")
rootCmd.Flags().StringSliceVarP(¶ms.namespaces, "namespaces", "", []string{""}, "namespaces to load policies and data from")
// replication
rootCmd.Flags().VarP(¶ms.replicateNamespace, "replicate", "", "replicate namespace-level resources")
rootCmd.Flags().VarP(¶ms.replicateCluster, "replicate-cluster", "", "replicate cluster-level resources")
rootCmd.Flags().StringVarP(¶ms.replicatePath, "replicate-path", "", "kubernetes", "set path to replicate data into")
rootCmd.Flags().StringSliceVarP(¶ms.replicateIgnoreNs, "replicate-ignore-namespaces", "", []string{""}, "namespaces that are ignored by replication")
rootCmd.Flags().StringVarP(¶ms.opaConfigFile, "opa-config", "", "", "set file containing OPA configuration to enable data replication based on configured bundles")
rootCmd.Flags().StringVarP(¶ms.analysisEntrypoint, "analysis-entrypoint", "", "main/main", "set decision to analyze for dynamic data replication configuration (requires --opa-config)")
rootCmd.Flags().StringVarP(¶ms.healthEndpoint, "health-endpoint", "", "", "set health check listening endpoint (e.g., localhost:8000)")
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if rootCmd.Flag("policy-label").Value.String() != "" || rootCmd.Flag("policy-value").Value.String() != "" {
err := configmap.CustomLabel(params.policyLabel, params.policyValue)
if err != nil {
logrus.Fatalf("Invalid --policy-label:%v || --policy-value:%v, %v", params.policyLabel, params.policyValue, err)
}
}
if rootCmd.Flag("data-label").Value.String() != "" || rootCmd.Flag("data-value").Value.String() != "" {
err := configmap.CustomLabel(params.dataLabel, params.dataValue)
if err != nil {
logrus.Fatalf("Invalid --data-label:%v || --data-value:%v, %v", params.dataLabel, params.dataValue, err)
}
}
return nil
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(params *params) {
switch params.logLevel {
case "debug":
logrus.SetLevel(logrus.DebugLevel)
case "info":
logrus.SetLevel(logrus.InfoLevel)
case "warn":
logrus.SetLevel(logrus.WarnLevel)
default:
logrus.Fatalf("Invalid log level %v", params.logLevel)
}
kubeconfig, err := loadRESTConfig(params.kubeconfigFile)
if err != nil {
logrus.Fatalf("Failed to load kubeconfig: %v", err)
}
if params.opaAuthFile != "" && params.opaAuth != "" {
logrus.Fatalf("You can not use both --opa-auth-token and --opa-auth-token-file")
}
if params.opaAuthFile != "" {
file, err := os.ReadFile(params.opaAuthFile)
if err != nil {
logrus.Fatalf("Failed to read opa auth token file %s", params.opaAuthFile)
}
params.opaAuth = strings.Split(string(file), "\n")[0]
}
if params.opaAllowInsecure && params.opaCAFile != "" {
logrus.Fatalf("You can not use both --opa-allow-insecure and --opa-ca-file")
}
if params.opaAllowInsecure {
config := &tls.Config{InsecureSkipVerify: params.opaAllowInsecure}
http.DefaultTransport.(*http.Transport).TLSClientConfig = config
}
if params.opaCAFile != "" {
rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
certs, err := os.ReadFile(params.opaCAFile)
if err != nil {
logrus.Fatalf("Failed to read opa certificate authority file %s", params.opaCAFile)
}
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
logrus.Println("No certs appended, using system certs only")
}
config := &tls.Config{RootCAs: rootCAs}
http.DefaultTransport.(*http.Transport).TLSClientConfig = config
}
if params.enablePolicies || params.enableData {
sync := configmap.New(
kubeconfig,
opa.New(params.opaURL, params.opaAuth),
configmap.DefaultConfigMapMatcher(
params.namespaces,
params.enablePolicies,
params.enableData,
params.policyLabel,
params.policyValue,
params.dataLabel,
params.dataValue,
),
)
_, err = sync.Run(params.namespaces)
if err != nil {
logrus.Fatalf("Failed to start configmap sync: %v", err)
}
}
if len(params.replicateCluster)+len(params.replicateNamespace) > 0 {
client, err := dynamic.NewForConfig(kubeconfig)
if err != nil {
logrus.Fatalf("Failed to get dynamic client: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opts := data.WithIgnoreNamespaces(params.replicateIgnoreNs)
for _, gvk := range params.replicateCluster {
sync := data.NewFromInterface(client, opa.New(params.opaURL, params.opaAuth).Prefix(params.replicatePath), getResourceType(gvk, false), opts)
go sync.RunContext(ctx)
}
for _, gvk := range params.replicateNamespace {
sync := data.NewFromInterface(client, opa.New(params.opaURL, params.opaAuth).Prefix(params.replicatePath), getResourceType(gvk, true), opts)
go sync.RunContext(ctx)
}
}
var sync *dynamicdata.Sync
if params.opaConfigFile != "" {
logger := logging.New()
switch params.logLevel {
case "debug":
logger.SetLevel(logging.Debug)
case "info":
logger.SetLevel(logging.Info)
case "error":
logger.SetLevel(logging.Error)
}
sync, err = dynamicdata.New(params.opaConfigFile, params.analysisEntrypoint, params.opaURL, params.opaAuth, params.replicateIgnoreNs, params.replicatePath, kubeconfig, logger)
if err != nil {
logrus.Fatalf("Failed to create dynamic synchronizer: %v", err)
}
go sync.Run(context.Background())
}
if params.healthEndpoint != "" {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if sync == nil || sync.Ready() {
logrus.Debugf("health check: READY")
w.WriteHeader(http.StatusOK)
} else {
logrus.Debugf("health check: NOT READY")
w.WriteHeader(http.StatusInternalServerError)
}
})
server := &http.Server{
Addr: params.healthEndpoint,
Handler: mux,
}
logrus.Infof("Starting health server on %v", params.healthEndpoint)
if err := server.ListenAndServe(); err != nil {
logrus.Fatalf("Error starting health server: %v", err)
}
}()
}
quit := make(chan struct{})
<-quit
}
func loadRESTConfig(path string) (*rest.Config, error) {
if path != "" {
return clientcmd.BuildConfigFromFlags("", path)
}
return rest.InClusterConfig()
}
func getResourceType(gvk groupVersionKind, namespaced bool) types.ResourceType {
return types.ResourceType{
Namespaced: namespaced,
Group: gvk.Group,
Version: gvk.Version,
Resource: gvk.Kind,
}
}
================================================
FILE: devbox.json
================================================
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json",
"packages": [
"go@1.24",
"just@1",
"kubectl@1.33",
"k3d@5.8",
"kubernetes-helm@3",
"go-tools@2026",
"open-policy-agent@1",
"hurl@7",
"fzf@0",
"devspace@6",
"yq-go@4",
"ko@0",
"oras@1"
],
"env": {
"GOPATH": "$HOME/go/",
"PATH": "$PATH:$HOME/go/bin"
},
"shell": {
"init_hook": [
"command -v chainsaw &>/dev/null || go install github.com/kyverno/chainsaw@v0.2.13",
"helm plugin list | grep -q unittest || helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.0.3"
]
}
}
================================================
FILE: devspace.yaml
================================================
version: v2beta1
name: opa-kube-mgmt
vars:
DEVSPACE_FLAGS: "-n default --no-warn"
KO_PLATFORMS: "linux/amd64"
KO_EXTRA_TAGS: ""
images:
default:
image: localhost:5001/openpolicyagent/kube-mgmt
tags:
- $(git describe --tags --always --dirty)
custom:
command: |
export KO_DOCKER_REPO=${runtime.images.default.image}
export KO_VERSION=${runtime.images.default.tag}
export KO_COMMIT=${DEVSPACE_GIT_COMMIT}
ko build --bare --tags ${KO_VERSION}${KO_EXTRA_TAGS} --platform=${KO_PLATFORMS} ./cmd/kube-mgmt
deployments:
default:
namespace: default
helm:
releaseName: ${DEVSPACE_NAME}
chart:
path: charts/${DEVSPACE_NAME}
values:
e2e: true
mgmt:
image:
repository: ${runtime.images.default.image}
tag: ${runtime.images.default.tag}
valuesFiles:
- "${E2E_TEST}/values.yaml"
upgradeArgs:
- "--wait"
- "--install"
hooks:
- name: "helm package and copy to ghcr"
events: ["after:build:default"]
disabled: true
command: |
helm package charts/${DEVSPACE_NAME} \
--version ${runtime.images.default.tag} --app-version ${runtime.images.default.tag}
helm push ${DEVSPACE_NAME}-${runtime.images.default.tag}.tgz oci://ghcr.io/open-policy-agent/helm
- name: "copy docker image to ghcr"
events: ["after:build:default"]
disabled: true
command: |
oras cp docker.io/${runtime.images.default.image}:${runtime.images.default.tag} \
ghcr.io/open-policy-agent/docker/${DEVSPACE_NAME}:${runtime.images.default.tag},latest
- name: "e2e cleanup"
events: ["before:deploy:default"]
command: |
kubectl delete cm -l kube-mgmt/e2e=true -n ${DEVSPACE_NAMESPACE} --ignore-not-found
kubectl delete svc -l kube-mgmt/e2e=true -n ${DEVSPACE_NAMESPACE} --ignore-not-found
profiles:
- name: release
patches:
- op: replace
path: images.default.image
value: openpolicyagent/kube-mgmt
- op: replace
path: vars.KO_PLATFORMS
value: "linux/amd64,linux/arm64"
- op: replace
path: vars.KO_EXTRA_TAGS
value: ",latest"
- op: replace
path: hooks[0].disabled
value: false
- op: replace
path: hooks[1].disabled
value: false
================================================
FILE: docs/admission-control-1.7.md
================================================
# Admission Control (1.7 and 1.8)
**Note: Admission Control has undergone changes in Kubernetes 1.7 through 1.9. If you are running Kubernetes 1.9, see [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) instead.**
To use OPA as an [Admission Controller](https://kubernetes.io/docs/admin/admission-controllers/#what-are-they) in Kubernetes 1.7 or 1.8, follow the steps in [External Admission Webhooks](https://kubernetes.io/docs/admin/extensible-admission-controllers/#external-admission-webhooks) to enable webhooks in the Kubernetes API server. Once you have configured the Kubernetes API server and generated the necessary certificates you can start `kube-mgmt` with the following options:
```bash
--register-admission-controller
--admission-controller-ca-cert-file=/path/to/ca/cert.pem
--admission-controller-service-name=<name-of-opa-service>
--admission-controller-service-namespace=<namespace-of-opa-service>
```
In addition to the command line arguments above, you must provide `--pod-name` and `--pod-namespace` using [Kubernetes' Downward API](https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/). The example manifest below shows how to set these.
You will need to create Secrets containing the server certificate and private key as well as the CA certificate:
```bash
kubectl create secret generic opa-ca --from-file=ca.crt
kubectl create secret tls opa-server --cert=server.crt --key=server.key
```
> See [Generating TLS Certificates](./tls-1.7.md) below for examples of how to generate the certificate files.
The example below shows how to deploy OPA and enable admission control:
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: opa
name: opa
spec:
replicas: 1
template:
metadata:
labels:
app: opa
name: opa
spec:
containers:
- name: opa
image: openpolicyagent/opa
args:
- "run"
- "--server"
- "--tls-cert-file=/certs/tls.crt"
- "--tls-private-key-file=/certs/tls.key"
- "--addr=0.0.0.0:443"
- "--insecure-addr=127.0.0.1:8181"
volumeMounts:
- readOnly: true
mountPath: /certs
name: opa-server
- name: kube-mgmt
image: openpolicyagent/kube-mgmt:0.6
args:
- "--pod-name=$(MY_POD_NAME)"
- "--pod-namespace=$(MY_POD_NAMESPACE)"
- "--register-admission-controller"
- "--admission-controller-ca-cert-file=/certs/ca.crt"
- "--admission-controller-service-name=opa"
- "--admission-controller-service-namespace=$(MY_POD_NAMESPACE)"
volumeMounts:
- readOnly: true
mountPath: /certs
name: opa-ca
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumes:
- name: opa-server
secret:
secretName: opa-server
- name: opa-ca
secret:
secretName: opa-ca
---
kind: Service
apiVersion: v1
metadata:
name: opa
spec:
clusterIP: 10.0.0.222
selector:
app: opa
ports:
- name: https
protocol: TCP
port: 443
targetPort: 443
```
Admission control policies must produce a document at `/system/main` that
represents the admission control decision (i.e., allow or deny).
#### Example Policy
To test that admission control is working, define a policy that rejects the
request if the `test-reject` label is found:
```ruby
package system
main = {
"apiVersion": "admission.k8s.io/v1alpha1",
"kind": "AdmissionReview",
"status": status,
}
default status = {"allowed": true}
status = reject {
input.spec.operation = "CREATE"
input.spec.object.labels["test-reject"]
}
reject = {
"allowed": false,
"status": {
"reason": "testing rejection"
}
}
```
================================================
FILE: docs/admission-control-crd.md
================================================
# Admission Control For Custom Resources
In the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial we have seen how OPA can be deployed as an admission controller to enforce custom policies on Kubernetes objects. In that tutorial, policies were enforced on native Kubernetes objects such as ingresses.
## Goal
This tutorial will show how OPA can be used to enforce polices on custom resources. A custom resource is an extension of the Kubernetes API that is not necessarily available on every Kubernetes cluster. More inforation on Kubernetes custom resources is available [here](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).
The additional steps that need to be taken to achieve this are:
1. Define a role for reading Kubernetes custom resources.
2. Grant OPA/kube-mgmt permissions to read Kubernetes custom resources.
3. Configure `kube-mgmt` to load Kubernetes custom resources into OPA.
## Prerequisites
Same as the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial.
## Steps
### 1. Start minikube
```bash
minikube start
```
Follow the steps in the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial to create the `opa` namespace and configure TLS.
### 2. Create a CustomResourceDefinition
Save the following CustomResourceDefinition to **resourcedefinition.yaml**:
```yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: cats.opa.example.com
spec:
group: opa.example.com
version: "v1"
scope: Namespaced
names:
plural: cats
singular: cat
kind: Cat
shortNames:
- ct
```
And create it:
```bash
kubectl create -f resourcedefinition.yaml
```
### 3. Deploy OPA on top of Kubernetes
Use the **admission-controlller.yaml** file from the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial to deploy OPA as an admission controller with the following changes:
1. Define a role for reading the Kubernetes custom resource created in the previous step.
```yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: crd-reader
rules:
- apiGroups: ["opa.example.com"]
resources: ["cats"]
verbs: ["get", "list", "watch"]
```
2. Grant OPA/kube-mgmt permissions to the above role.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: opa-crd-reader
roleRef:
kind: ClusterRole
name: crd-reader
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
name: system:serviceaccounts:opa
apiGroup: rbac.authorization.k8s.io
```
3. Update the `kube-mgmt` container spec to load the Kubernetes custom resources into OPA.
```yaml
name: kube-mgmt
args:
- "--replicate=opa.example.com/v1/cats" # replicate custom resources
```
Now follow the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial to deploy OPA on top of Kubernetes and register OPA as an admission controller.
### 4. Define a policy and load it into OPA via Kubernetes
Create a policy that rejects objects of kind `Cat` from sharing the same cat name.
**name-conflicts.rego**:
```ruby
package kubernetes.admission
import data.kubernetes.cats
# Cat names must be unique.
deny[msg] {
input.request.kind.kind = "Cat"
input.request.operation = "CREATE"
name := input.request.object.spec.name
cat := cats[other_ns][other_cat]
cat.spec.name == name
msg = sprintf("duplicate cat name %q (conflicts with %v/%v)", [name, other_ns, other_cat])
}
```
```bash
kubectl create configmap name-conflicts --from-file=name-conflicts.rego
```
### 5. Exercise the policy
Define two objects of kind `Cat`. The first one will be permitted and the second will be rejected.
**cat.yaml**:
```yaml
apiVersion: "opa.example.com/v1"
kind: Cat
metadata:
name: my-new-cat-object
spec:
name: Whiskers
```
**cat-duplicate.yaml**:
```yaml
apiVersion: "opa.example.com/v1"
kind: Cat
metadata:
name: my-duplicate-cat-object
spec:
name: Whiskers
```
Finally, try to create both `Cat` objects:
```bash
kubectl create -f cat.yaml
kubectl create -f cat-duplicate.yaml
```
The second object will be rejected since an object with the cat name `Whiskers` was created earlier.
================================================
FILE: docs/admission-control-secure.md
================================================
# Admission Control Secure
In the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial we have seen
how OPA can be deployed as an admission controller. In that tutorial, OPA is not configured to `authenticate` and `authorize` client requests.
## Goal
This tutorial will show how to securely deploy OPA as an admission controller. The additional steps that need to be taken to achieve this are:
1. Start `OPA` with authentication and authorization enabled using the `--authentication` and `--authorization` options respectively.
2. Volume mount OPA's startup authorization policy into the OPA container.
3. Start `kube-mgmt` with `Bearer` token flag using the `--opa-auth-token-file` option.
4. Configure `kube-mgmt` to load polices stored in ConfigMaps that are created in the `opa` namespace and are labelled `openpolicyagent.org/policy=rego`.
5. Configure the Kubernetes API server to use `Bearer` token.
## Prerequisites
Same as the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial.
## Steps
### 1. Configure Kubernetes API server
OPA will `authenticate` clients by extracting the `Bearer` token from the incoming API requests. Hence the Kubernetes API server needs to be configured
to send a `Bearer` token in all requests to OPA. To do this, the API server must be provided with an admission control configuration file via the `--admission-control-config-file`
flag during startup. This means the configuration file should be present inside the minikube VM at a location which is accessible to the API server pod.
Start minikube:
```bash
minikube start
```
`ssh` into the minikube VM and place the configuration files (**admission-control-config.yaml** and **kube-config.yaml**) below
inside `/var/lib/minikube/certs`. This directory is accessible inside the API server pod.
**admission-control-config.yaml**
```yaml
apiVersion: apiserver.k8s.io/v1alpha1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionWebhook
configuration:
apiVersion: apiserver.config.k8s.io/v1alpha1
kind: WebhookAdmission
kubeConfigFile: /var/lib/minikube/certs/kube-config.yaml
```
**kube-config.yaml**
```yaml
apiVersion: v1
kind: Config
users:
# '*' is the default match.
- name: '*'
user:
token: <apiserver_secret_token>
```
With the above configuration, all requests the API server makes to OPA will include a `Bearer` token. You will need to generate the `Bearer`
token (`<apiserver_secret_token>`) and later include it in OPA's startup authorization policy so that OPA can verify the identity of the API server.
Now exit the minikube VM and stop it:
```bash
minikube stop
```
Start minikube by passing information about the admission control configuration file to the API server:
```bash
minikube start --extra-config=apiserver.admission-control-config-file=/var/lib/minikube/certs/admission-control-config.yaml
```
Make sure that the minikube ingress addon is enabled:
```bash
minikube addons enable ingress
```
Follow the steps in the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html) tutorial to create the `opa`
namespace and configure TLS. Now use the **admission-controller.yaml** file from the tutorial to deploy OPA as an admission controller with the following changes:
1. Use the below `opa` and `kube-mgmt` container spec which enables OPA's security features and configures `kube-mgmt` to include a `Bearer` token
in calls to OPA. We also volume mount OPA's startup authorization policy `authz.rego` inside the OPA container in the `/policies` directory.
```yaml
spec:
containers:
- name: opa
image: openpolicyagent/opa:0.42.1
args:
- "run"
- "--server"
- "--tls-cert-file=/certs/tls.crt"
- "--tls-private-key-file=/certs/tls.key"
- "--addr=0.0.0.0:443"
- "--addr=http://127.0.0.1:8181"
- "--authentication=token"
- "--authorization=basic"
- "/policies/authz.rego" # authorization policy used on startup
- "--ignore=.*" # exclude hidden dirs created by Kubernetes
volumeMounts:
- readOnly: true
mountPath: /certs
name: opa-server
- readOnly: true
mountPath: /policies
name: inject-policy
- name: kube-mgmt
image: openpolicyagent/kube-mgmt:7.0.6
args:
- "--replicate-cluster=v1/namespaces"
- "--replicate=extensions/v1beta1/ingresses"
- "--opa-auth-token-file=/policies/token"
volumeMounts:
- readOnly: true
mountPath: /policies
name: inject-policy
volumes:
- name: opa-server
secret:
secretName: opa-server
- name: inject-policy
secret:
secretName: inject-policy
```
2. Include the Secret that contains OPA's startup authorization policy.
```bash
cat > authz.rego <<EOF
package system.authz
default allow = false
allow {
"kube-mgmt" = input.identity
}
allow {
<apiserver_secret_token> = input.identity
}
EOF
kubectl create secret generic inject-policy -n opa --from-file=authz.rego --from-literal=token=kube-mgmt
```
If you have liveness or readiness probes configured on the OPA server for `/health` you will need to add the following `allow` rule
to ensure Kubernetes can still access these endpoints.
```
# Allow anonymouse access to /health otherwise K8s get 403 and kills pod.
allow {
input.path = ["health"]
}
```
3. Label the `opa-default-system-main` ConfigMap.
```yaml
---
kind: ConfigMap
apiVersion: v1
metadata:
name: opa-default-system-main
namespace: opa
labels:
openpolicyagent.org/policy: rego
data:
main: |
package system
import data.kubernetes.admission
main = {
"apiVersion": "admission.k8s.io/v1beta1",
"kind": "AdmissionReview",
"response": response,
}
default response = {"allowed": true}
response = {
"allowed": false,
"status": {
"reason": reason,
},
} {
reason = concat(", ", admission.deny)
reason != ""
}
```
When OPA starts, the `kube-mgmt` container will load Kubernetes Namespace and Ingress objects into OPA. `kube-mgmt` will automatically
discover policies stored in ConfigMaps in Kubernetes and load them into OPA. `kube-mgmt` assumes a ConfigMap contains policies if the ConfigMap is:
- Created in a namespace listed in the `--namespaces` option. Default namespace is `opa`.
- Labelled with `openpolicyagent.org/policy=rego`.
`kube-mgmt` is started with the `--opa-auth-token-file` flag and hence all requests made to OPA will include a `Bearer` token(`kube-mgmt` in this case).
You can now follow the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admission-control.html)
tutorial to deploy OPA on top of Kubernetes and test admission control. **Make sure to label the ConfigMap when you store a policy inside it.**
================================================
FILE: docs/tls-1.7.md
================================================
# Generating TLS Certificates (1.7)
External Admission Controllers must be secured with TLS. At a minimum you must:
- Provide the Kubernetes API server with a client key to use for
webhook calls (`client.key` and `client.crt` below).
- Provide OPA with a server key so that the Kubernetes API server can
authenticate it (`server.key` and `server.crt` below).
- Provide `kube-mgmt` with the CA certificate to register with the Kubernetes
API server (`ca.crt` below).
Follow the steps below to generate the necessary files for test purposes.
First, generate create the required OpenSSL configuration files:
**client.conf**:
```
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
```
**server.conf**:
```
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.0.0.222
```
> The subjectAltName/IP address in the certificate MUST match the one configured
> on the Kubernetes Service.
Finally, generate the CA and client/server key pairs.
```bash
# Create a certificate authority
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -days 100000 -out ca.crt -subj "/CN=admission_ca"
# Create a server certiticate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=admission_server" -config server.conf
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=admission_client" -config client.conf
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 100000 -extensions v3_req -extfile client.conf
```
If you are using minikube, you can specify the client TLS credentials with the following `minikube start` options:
```
--extra-config=apiserver.ProxyClientCertFile=/path/to/client.crt # in VM
--extra-config=apiserver.ProxyClientKeyFile=/path/to/client.key # in VM
```
================================================
FILE: examples/service_validation/README.md
================================================
# Kubernetes Admission Control for preventing open AWS LoadBalancers
Kubernetes Service objects of type [LoadBalancer](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/) on AWS create an Elastic LoadBalancer or Network LoadBalancer. However, if not properly configured, these LoadBalancers can be open to the world exposing EC2 instances behind them to security breaches.
OPA can provide a good ValidationWebhook for ensuring that Service objects of type LoadBalancer do not accidentally create a LoadBalancer open to the world.
## Goals
This tutorial shows how to create validation webhooks for Service objects and enforcing the LoadBalancer policies.
- Kubernetes Service objects of type LoadBalancer that do not have `spec.loadBalancerSourceRanges` are rejected.
- Users are required to explicitly set `spec.loadBalancerSourceRanges`. If users want to create LoadBalancers that are actually open to the world, they should explicitly set `spec.loadBalancerSourceRanges` to `0.0.0.0/0`.
## Prerequisites
This tutorial has been tested with Kubernetes 1.10 running on AWS with RBAC enabled. But it should work with Kubernetes 1.9 or higher.
## Steps
### The simplest way to setup opa and policies would be to run the install.sh script.
```bash
$ ./install.sh
```
Otherwise, here are the detailed steps:
### 1. Start Kubernetes with ValidatingAdmissionWebhook admission controller enabled.
### 2. Create the namespace called `opa` in it.
```bash
kubectl create namespace opa
```
### 3. Create the SSL certs required for the webhook. Same as [this](https://github.com/open-policy-agent/opa/blob/master/docs/book/kubernetes-admission-control.md#3-deploy-opa-on-top-of-kubernetes)
```bash
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -days 100000 -out ca.crt -subj "/CN=admission_ca"
```
Generate the TLS key and certificate for OPA:
```bash
cat >server.conf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
EOF
```
```bash
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=opa.opa.svc" -config server.conf
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 100000 -extensions v3_req -extfile server.conf
```
> Note: the Common Name value you give to openssl MUST match the name of the OPA service created below.
Create a Secret to store the TLS credentials for OPA:
```bash
kubectl create secret tls opa-server --cert=server.crt --key=server.key
```
In the admission_controller.yaml file in this example, replace the REPLACE_WITH_SECRET with the base64 encoded
```bash
kubectl apply -f ./examples/service_validation/admission-controller.yaml
```
This creates the OPA deployment, the validation webhook as well as the config map which has the policy.
### 4. Exercise the policy
Create a service object and ensure that it is enforcing the policy.
**service_invalid.yaml**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: no-whitelist-ips
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
type: LoadBalancer
```
**service_valid.yaml**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: whitelist-ips
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
type: LoadBalancer
loadBalancerSourceRanges:
- 10.0.0.0/8
```
```bash
kubectl create -f service_invalid.yaml
kubectl create -f service_valid.yaml
```
This tutorial showed how you can leverage OPA to enforce admission control of Service objects to prevent accidentally exposing AWS resources to the world.
================================================
FILE: examples/service_validation/admission_controller.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
name: opa-sa
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: opa-rolebinding
subjects:
- kind: ServiceAccount
name: opa-sa
namespace: opa
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
kind: Service
apiVersion: v1
metadata:
name: opa
spec:
selector:
app: opa
ports:
- name: https
protocol: TCP
port: 443
targetPort: 443
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: opa
name: opa
spec:
replicas: 1
template:
metadata:
labels:
app: opa
name: opa
spec:
serviceAccountName: opa-sa
containers:
- name: opa
image: openpolicyagent/opa:0.8.0
args:
- "run"
- "--server"
- "--tls-cert-file=/certs/tls.crt"
- "--tls-private-key-file=/certs/tls.key"
- "--addr=0.0.0.0:443"
- "--insecure-addr=127.0.0.1:8181"
volumeMounts:
- readOnly: true
mountPath: /certs
name: opa-server
- name: kube-mgmt
image: openpolicyagent/kube-mgmt:0.12.1
volumes:
- name: opa-server
secret:
secretName: opa-server
---
kind: ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
metadata:
name: opa-validating-webhook
webhooks:
- name: validating-webhook.openpolicyagent.org
admissionReviewVersions: ["v1beta1"]
rules:
- operations: ["CREATE"]
apiGroups: ["*"]
apiVersions: ["v1"]
resources: ["services"]
clientConfig:
caBundle: REPLACE_WITH_SECRET
service:
namespace: opa
name: opa
sideEffects: None
---
kind: ConfigMap
apiVersion: v1
metadata:
name: service-check
data:
main: |
package kubernetes.admission
import data.kubernetes.namespaces
deny[msg] {
input.request.kind.kind = "Service"
input.request.operation = "CREATE"
servicetype = input.request.object.spec.type
contains(servicetype, "LoadBalancer")
not input.request.object.spec.loadBalancerSourceRanges
msg = sprintf("Rejecting service of type %q without specifying spec.loadBalancerSourceRanges", [servicetype])
}
---
kind: ConfigMap
apiVersion: v1
metadata:
name: opa-default-system-main
data:
main: |
package system
import data.kubernetes.admission
main = {
"apiVersion": "admission.k8s.io/v1beta1",
"kind": "AdmissionReview",
"response": response,
}
default response = {"allowed": true}
response = {
"allowed": false,
"status": {
"reason": reason,
},
} {
reason = concat(", ", admission.deny)
reason != ""
}
================================================
FILE: examples/service_validation/install.sh
================================================
#!/bin/bash
set -ex
OUT_DIR=/tmp/opa
rm -rf ${OUT_DIR}; mkdir -p ${OUT_DIR}
openssl genrsa -out ${OUT_DIR}/ca.key 2048
openssl req -x509 -new -nodes -key ${OUT_DIR}/ca.key -days 100000 -out ${OUT_DIR}/ca.crt -subj "/CN=admission_ca"
cat >${OUT_DIR}/server.conf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
EOF
openssl genrsa -out ${OUT_DIR}/server.key 2048
openssl req -new -key ${OUT_DIR}/server.key -out ${OUT_DIR}/server.csr -subj "/CN=opa.opa.svc" -config ${OUT_DIR}/server.conf
openssl x509 -req -in ${OUT_DIR}/server.csr -CA ${OUT_DIR}/ca.crt -CAkey ${OUT_DIR}/ca.key -CAcreateserial -out ${OUT_DIR}/server.crt -days 100000 -extensions v3_req -extfile ${OUT_DIR}/server.conf
install_namespace=opa
caBundle=$(base64 ${OUT_DIR}/ca.crt)
cp admission_controller.yaml ${OUT_DIR}/admission_controller.yaml
gsed -i "s/REPLACE_WITH_SECRET/${caBundle}/" ${OUT_DIR}/admission_controller.yaml
kubectl create namespace ${install_namespace}
kubectl create secret tls opa-server --cert=${OUT_DIR}/server.crt --key=${OUT_DIR}/server.key --namespace ${install_namespace}
kubectl apply -f ${OUT_DIR}/admission_controller.yaml --namespace ${install_namespace}
================================================
FILE: go.mod
================================================
module github.com/open-policy-agent/kube-mgmt
go 1.24.11
require (
github.com/open-policy-agent/opa v1.5.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
)
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/containerd/v2 v2.1.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
github.com/vektah/gqlparser/v2 v2.5.26 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/sync v0.14.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect; indire4ct
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM=
github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y=
github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/open-policy-agent/opa v1.5.1 h1:LTxxBJusMVjfs67W4FoRcnMfXADIGFMzpqnfk6D08Cg=
github.com/open-policy-agent/opa v1.5.1/go.mod h1:bYbS7u+uhTI+cxHQIpzvr5hxX0hV7urWtY+38ZtjMgk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM=
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4=
github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
================================================
FILE: internal/expect/client.go
================================================
package expect
import (
"encoding/json"
"errors"
opa_client "github.com/open-policy-agent/kube-mgmt/pkg/opa"
)
// Client emulates OPA Client API
type Client struct {
PrefixList []string
// This function will be called on every request
actor func(req Request, value interface{}) error
}
// Prefix implements Data
func (f *Client) Prefix(path string) opa_client.Data {
f.PrefixList = append(f.PrefixList, path)
return f
}
// PatchData implements Data.
func (f *Client) PatchData(path string, op string, value *interface{}) (err error) {
req := Request{
req: patchRequest,
path: path,
op: op,
}
var actualValue interface{}
if value != nil {
actualValue = *value
}
return f.actor(req, actualValue)
}
// PutData implements Data
func (f *Client) PutData(path string, value interface{}) (err error) {
req := Request{
req: putRequest,
path: path,
}
return f.actor(req, value)
}
var errNotSupported = errors.New("PostData not supported")
// PostData implements Data. Currently not supported.
func (*Client) PostData(string, interface{}) (json.RawMessage, error) {
return nil, errNotSupported
}
// InsertPolicy implements Policies
func (f *Client) InsertPolicy(path string, value []byte) (err error) {
req := Request{
req: insertPolicyRequest,
path: path,
}
return f.actor(req, value)
}
// DeletePolicy implements Policies
func (f *Client) DeletePolicy(path string) (err error) {
req := Request{
req: deletePolicyRequest,
path: path,
}
return f.actor(req, nil)
}
================================================
FILE: internal/expect/request.go
================================================
package expect
import (
"fmt"
"reflect"
"time"
)
type request string
const (
patchRequest request = "PatchData"
putRequest request = "PutData"
insertPolicyRequest request = "InsertPolicy"
deletePolicyRequest request = "DeletePolicy"
noRequest request = "Nothing"
)
// Request represents an operation against the mock client.
type Request struct {
req request
path string
op string
value []byte
interval time.Duration // Only applies to script.Expect(Nothing())
}
// Equals compares two requests
func (expected Request) Equals(actual Request) bool {
return expected.req == actual.req &&
expected.path == actual.path &&
(expected.req != patchRequest || expected.op == actual.op) &&
(expected.value == nil || reflect.DeepEqual(expected.value, actual.value))
}
// String implements fmt.Stringer
func (r Request) String() string {
if r.value != nil {
return fmt.Sprintf("{req: %q, path: %q, op: %q, value: %q}", r.req, r.path, r.op, string(r.value))
}
return fmt.Sprintf("{req: %q, path: %q, op: %q}", r.req, r.path, r.op)
}
func optional(expected ...[]byte) []byte {
if len(expected) > 0 {
return expected[0]
}
return nil
}
// PutData describes a PutData request with an optional expected value
// (expected value can be omitted)
func PutData(path string, expected ...[]byte) Request {
return Request{
req: putRequest,
path: path,
value: optional(expected...),
}
}
// PatchData describes a PatchData request with an optional expected value
// (expected value can be omitted)
func PatchData(path string, op string, expected ...[]byte) Request {
return Request{
req: patchRequest,
path: path,
op: op,
value: optional(expected...),
}
}
// InsertPolicy describes a InsertPolicy request with an optional expected value
// (expected value can be omitted)
func InsertPolicy(path string, expected ...[]byte) Request {
return Request{
req: insertPolicyRequest,
path: path,
value: optional(expected...),
}
}
// DeletePolicy describes a DeletePolicy request with an optional expected value
// (expected value can be omitted)
func DeletePolicy(path string) Request {
return Request{
req: deletePolicyRequest,
path: path,
}
}
// Nothing describes an empty action. The client must not get any request for the given time
func Nothing(duration time.Duration) Request {
return Request{
req: noRequest,
interval: duration,
}
}
// Action performed when an expected Request arrives.
// The request will return the result of invoking the Action.
type Action func() error
// Step combines a Request and an Action
type Step struct {
Request
Action
}
// Do turns a Request into a Step
func (req Request) Do(action Action) Step {
return Step{
Request: req,
Action: action,
}
}
// DoError is a shortcut for Do(func() error { return err })
func (req Request) DoError(err error) Step {
return Step{
Request: req,
Action: func() error {
return err
},
}
}
// End is a shortcut for Do(nil)
func (req Request) End() Step {
return Step{
Request: req,
}
}
================================================
FILE: internal/expect/script.go
================================================
package expect
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"time"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
// Script is a sequence of expected Requests for a Client,
// and the Actions to perform on each Request.
type Script []Step
// String implements fmt.Stringer
func (s Script) String() string {
return s.strings("\n")
}
// Play creates a client with the script provided, and runs the show.
// When the script ends, the show is cancelled and the final state
// of the client returned.
func Play(t *testing.T, script Script, show func(ctx context.Context, client *Client)) *Client {
steps := len(script)
if steps <= 0 || show == nil {
return nil
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
// Arrange to cancel the context on the last step
last := script[steps-1]
script[steps-1].Action = func() error {
defer cancel()
if last.Action == nil {
return nil
}
return last.Action()
}
var (
actor func(req Request, value interface{}) error
improvise func(cursor int)
cursor int = 0
)
actor = func(req Request, value interface{}) error {
if cursor >= len(script) {
t.Fatalf("Expected at most %d steps, got one more request %v", len(script), req)
}
// Save the actual value received to req. We do it
// here because we have the *testing.T instance, and
// can call t.Fatal if conversions fail.
if req.req == insertPolicyRequest {
req.value = value.([]byte)
} else {
req.value = MustRoundTrip(t, value)
}
// Check that the request matches the cue
cue := script[cursor]
if !cue.Equals(req) {
seq := script[:cursor+1].strings("\n\t")
t.Fatalf("Expected sequence:\n\t%v\nError at step %d, got:\n\t%v", seq, cursor, req)
}
cursor++
if cursor < len(script) && script[cursor].req == noRequest {
// If the next update is timed, schedule it.
go improvise(cursor)
}
if cue.Action == nil {
return nil
}
return cue.Action()
}
// improvise triggers the step without any external input
improvise = func(cursor int) {
<-time.After(script[cursor].interval)
actor(script[cursor].Request, nil)
}
client := &Client{actor: actor}
if script[0].req == noRequest {
// boot the script if the first step is a wait.
go improvise(0)
}
show(ctx, client)
if deadline, ok := ctx.Deadline(); ok && deadline.Before(time.Now()) {
t.Fatalf("Test %s failed because of timeout", t.Name())
}
return client
}
// MustMarshal marshals the objet to JSON, calls t.Fatal on error
func MustMarshal(t *testing.T, obj interface{}) []byte {
t.Helper()
data, err := json.Marshal(obj)
if err != nil {
t.Fatalf("error marshalling JSON: %s", err)
}
return data
}
// MustUmnarshal unmarshals the objet from JSON, calls t.Fatal on error
func MustUnmarshal(t *testing.T, data []byte) interface{} {
t.Helper()
var result interface{}
if len(data) > 0 {
err := json.Unmarshal(data, &result)
if err != nil {
t.Fatalf("error unmarshalling JSON: %s", err)
}
}
return result
}
// mustRoundtrip marshals the object to JSON consistently.
//
// Kubernetes objects have custom marshallers that output the
// json in a custom order. So comparing the marshalled representation of
// a kubernetes object with that of a map or an *unstructured.Unstructured
// built from that same object will fail.
//
// MustRoundTrip will make sure the generated string is comparable.
func MustRoundTrip(t *testing.T, obj interface{}) []byte {
return MustMarshal(t, MustUnmarshal(t, MustMarshal(t, obj)))
}
// MustEqual compares the values and calls t.Fatal on error
func MustEqual(t *testing.T, result, expected interface{}) {
t.Helper()
if !reflect.DeepEqual(result, expected) {
t.Fatalf("Expected:\n\n%q\n\nActual:\n\n%q\n", expected, result)
}
}
// MustKey gets the mentaNamespaceKey of an object
func MustKey(t *testing.T, obj runtime.Object) string {
t.Helper()
path, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
t.Fatalf("Failed to get path from object %v: %v", obj, err)
}
return path
}
// strings formats the Script as a list of strings for printing
func (s Script) strings(sep string) string {
steps := make([]string, 0, len(s))
for cursor, step := range s {
steps = append(steps, fmt.Sprintf("%d: %s", cursor, step.String()))
}
return strings.Join(steps, sep)
}
================================================
FILE: justfile
================================================
K3D := "kube-mgmt"
TEST_RESULTS := 'build/test-results'
@_default:
@just --list
# golang linter
[group('code quality')]
lint-go:
go vet ./...
staticcheck ./...
# helm linter
[group('code quality')]
lint-helm filter="*":
#!/usr/bin/env -S bash -euo pipefail
mkdir -p {{TEST_RESULTS}}/helm-unittest
helm unittest -f '../../test/lint/{{filter}}.yaml' \
--output-file {{TEST_RESULTS}}/helm-unittest/lint.xml --output-type JUnit charts/opa-kube-mgmt
# run all linters
[group('code quality')]
lint: lint-go lint-helm
# run helm unit tests
[group('code quality')]
test-helm filter="*":
#!/usr/bin/env -S bash -euo pipefail
mkdir -p {{TEST_RESULTS}}/helm-unittest
helm unittest -f '../../test/unit/{{filter}}.yaml' \
--output-file {{TEST_RESULTS}}/helm-unittest/unit.xml --output-type JUnit charts/opa-kube-mgmt
# run golang unit tests
[group('code quality')]
test-go:
go test ./...
# run linters and unit tests
[group('code quality')]
test: lint test-go test-helm
@_token:
kubectl exec deploy/opa-kube-mgmt -n default -c mgmt -- cat /bootstrap/mgmt-token
# run e2e test using chainsaw and hurl
[group('code quality')]
test-e2e E2E_TEST="": _ctx
#!/usr/bin/env -S bash -euo pipefail
SCENARIO="{{E2E_TEST}}"
if [ -z "$SCENARIO" ]; then
SCENARIO=$(find test/e2e/ -mindepth 1 -maxdepth 1 -type d | sort | fzf --header "Select e2e scenario")
fi
devspace purge
devspace deploy --var E2E_TEST="$SCENARIO"
mkdir -p {{TEST_RESULTS}}/chainsaw
OPA_TOKEN=$(just _token 2>/dev/null || true) chainsaw test "$SCENARIO" --quiet --namespace default \
--report-format JUNIT-TEST \
--report-name "$(basename "$SCENARIO")" --report-path {{TEST_RESULTS}}/chainsaw
# run all e2e tests
[group('code quality')]
test-e2e-all:
#!/usr/bin/env -S bash -euo pipefail
for E in $(find test/e2e/ -name 'chainsaw-test.yaml'|xargs -n1 dirname|sort); do
just test-e2e "${E}"
done
# start kube-mgmt in local k8s cluster
[group('deployment')]
@up: _ctx
devspace deploy --var E2E_TEST=test/e2e/default
# stop kube-mgmt in local k8s cluster
[group('deployment')]
@down: _ctx
devspace purge --force-purge && rm -rf .devspace/
@_ctx:
kubectl config use-context k3d-{{K3D}}
_bundle:
#!/usr/bin/env -S bash -euo pipefail
opa build -b ./test/e2e/replicate_auto/bundle -o ./test/e2e/replicate_auto/bundle.tar.gz
kubectl delete configmap -n default bundle --ignore-not-found
kubectl create configmap -n default bundle --from-file ./test/e2e/replicate_auto/bundle.tar.gz
# delete local k8s cluster
[group('deployment')]
@k3d-down:
k3d cluster delete {{K3D}} || true
# (re) create local k8s cluster using k3d
[group('deployment')]
all: k3d-down && _ctx _bundle
#!/usr/bin/env -S bash -euo pipefail
echo '
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: {{K3D}}
servers: 1
agents: 0
image: rancher/k3s:v1.33.9-k3s1
registries:
create:
name: k3d-{{K3D}}-registry
host: "0.0.0.0"
hostPort: "5001"
config: |
mirrors:
"localhost:5001":
endpoint:
- http://k3d-{{K3D}}-registry:5000
ports:
- port: 8080:80
nodeFilters: ["loadbalancer"]
- port: 8443:443
nodeFilters: ["loadbalancer"]
options:
k3s:
extraArgs:
- arg: "--disable=local-storage,metrics-server"
nodeFilters: ["server:*"]
' | k3d cluster create --config /dev/stdin
kubectl config set-context k3d-{{K3D}} --namespace default
docker login -u {{K3D}} -p {{K3D}} localhost:5001
kubectl wait --for=create crd/ingressroutetcps.traefik.io --timeout=2m
sleep 3
kubectl wait --for=condition=Established crd/ingressroutetcps.traefik.io --timeout=30s
================================================
FILE: pkg/configmap/configmap.go
================================================
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package configmap
import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"sort"
"strconv"
"strings"
"time"
"github.com/open-policy-agent/kube-mgmt/pkg/opa"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
)
const (
defaultRetries = 2
statusAnnotationKey = "openpolicyagent.org/kube-mgmt-status"
retriesAnnotationKey = "openpolicyagent.org/kube-mgmt-retries"
// Special namespace in Kubernetes federation that holds scheduling policies.
// commented because staticcheck: 'const kubeFederationSchedulingPolicy is unused (U1000)'
// kubeFederationSchedulingPolicy = "kube-federation-scheduling-policy"
resyncPeriod = time.Second * 60
syncResetBackoffMin = time.Second
syncResetBackoffMax = time.Second * 30
)
// Label validator
func CustomLabel(key, value string) error {
_, err := labels.NewRequirement(key, selection.Equals, []string{value})
if err != nil {
return err
}
return nil
}
// DefaultConfigMapMatcher returns a function that will match configmaps in
// specified namespaces and/or with a policy or data label. The first bool return
// value specifies a policy/data match and the second bool indicates if the configmap
// contains a policy.
func DefaultConfigMapMatcher(namespaces []string, enablePolicies, enableData bool, policyLabelKey, policyLabelValue, dataLabelKey, dataLabelValue string) func(*v1.ConfigMap) (bool, bool) {
return func(cm *v1.ConfigMap) (bool, bool) {
var match, isPolicy bool
if enableData {
match = matchesNamespace(cm, namespaces) && matchesLabel(cm, dataLabelKey, dataLabelValue)
}
if !match && enablePolicies {
match = matchesNamespace(cm, namespaces) && matchesLabel(cm, policyLabelKey, policyLabelValue)
if match {
isPolicy = true
}
}
return match, isPolicy
}
}
func matchesLabel(cm *v1.ConfigMap, labelKey, labelValue string) bool {
return cm.Labels[labelKey] == labelValue
}
func matchesNamespace(cm *v1.ConfigMap, namespaces []string) bool {
for _, ns := range namespaces {
if ns == cm.Namespace || ns == "*" {
return true
}
}
return false
}
// Sync replicates policies or data stored in the API server as ConfigMaps into OPA.
type Sync struct {
kubeconfig *rest.Config
opa opa.Client
clientset *kubernetes.Clientset
matcher func(*v1.ConfigMap) (bool, bool)
}
// New returns a new Sync that can be started.
func New(kubeconfig *rest.Config, opa opa.Client, matcher func(*v1.ConfigMap) (bool, bool)) *Sync {
cpy := *kubeconfig
cpy.GroupVersion = &schema.GroupVersion{
Version: "v1",
}
cpy.APIPath = "/api"
cpy.ContentType = runtime.ContentTypeJSON
scheme := runtime.NewScheme()
cpy.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)}
builder := runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
*cpy.GroupVersion,
&metav1.ListOptions{},
&metav1.Status{},
&v1.ConfigMapList{},
&v1.ConfigMap{})
return nil
})
builder.AddToScheme(scheme)
return &Sync{
kubeconfig: &cpy,
opa: opa,
matcher: matcher,
}
}
// Run starts the synchronizer. To stop the synchronizer send a message to the
// channel.
func (s *Sync) Run(namespaces []string) (chan struct{}, error) {
client, err := rest.RESTClientFor(s.kubeconfig)
if err != nil {
return nil, err
}
s.clientset, err = kubernetes.NewForConfig(s.kubeconfig)
if err != nil {
return nil, err
}
quit := make(chan struct{})
logrus.Infof("Policy/data ConfigMap processor connected to K8s: namespaces=%v", namespaces)
for _, namespace := range namespaces {
if namespace == "*" {
namespace = v1.NamespaceAll
}
listerWatcher := cache.NewListWatchFromClient(
client,
"configmaps",
namespace,
fields.Everything())
_, controller := cache.NewInformerWithOptions(cache.InformerOptions{
ListerWatcher: listerWatcher,
ObjectType: &v1.ConfigMap{},
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: s.add,
UpdateFunc: s.update,
DeleteFunc: s.delete,
},
ResyncPeriod: 0, // Set to 0 as in the original code
})
go controller.Run(quit)
}
return quit, nil
}
func (s *Sync) add(obj interface{}) {
cm := obj.(*v1.ConfigMap)
if match, isPolicy := s.matcher(cm); match {
logrus.Debugf("OnAdd cm=%v/%v, isPolicy=%v", cm.Namespace, cm.Name, isPolicy)
s.syncAdd(cm, isPolicy)
}
}
func (s *Sync) update(oldObj, obj interface{}) {
oldCm, cm := oldObj.(*v1.ConfigMap), obj.(*v1.ConfigMap)
if match, isPolicy := s.matcher(cm); match {
logrus.Debugf("OnUpdate cm=%v/%v, isPolicy=%v, oldVer=%v, newVer=%v",
cm.Namespace, cm.Name, isPolicy, oldCm.GetResourceVersion(), cm.GetResourceVersion())
if cm.GetResourceVersion() != oldCm.GetResourceVersion() {
newFp, oldFp := fingerprint(cm), fingerprint(oldCm)
rtrVal := cm.Annotations[retriesAnnotationKey]
logrus.Debugf("OnUpdate cm=%v/%v, retries=%v, oldFp=%v, newFp=%v", cm.Namespace, cm.Name, rtrVal, oldFp, newFp)
if newFp != oldFp || rtrVal != "0" {
s.syncAdd(cm, isPolicy)
}
}
} else {
// check if the label was removed
if match, isPolicy := s.matcher(oldCm); match {
s.syncRemove(oldCm, isPolicy)
}
}
}
func (s *Sync) delete(obj interface{}) {
if d, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = d.Obj
}
cm := obj.(*v1.ConfigMap)
if match, isPolicy := s.matcher(cm); match {
logrus.Debugf("OnDelete cm=%v/%v", cm.Namespace, cm.Name)
s.syncRemove(cm, isPolicy)
}
}
func (s *Sync) syncAdd(cm *v1.ConfigMap, isPolicy bool) {
path := fmt.Sprintf("%v/%v", cm.Namespace, cm.Name)
logrus.Debugf("Adding cm=%v, isPolicy=%v", path, isPolicy)
// sort keys so that errors, if any, are always in the same order
sortedKeys := make([]string, 0, len(cm.Data))
for key := range cm.Data {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
var syncErr errList
for _, key := range sortedKeys {
value := cm.Data[key]
id := fmt.Sprintf("%v/%v", path, key)
var err error
if isPolicy {
err = s.opa.InsertPolicy(id, []byte(value))
logrus.Infof("Added policy %v, err=%v", id, err)
} else {
// We don't need to know the JSON structure, just pass it
// directly to the OPA data store.
var data map[string]interface{}
if err = json.Unmarshal([]byte(value), &data); err != nil {
logrus.Errorf("Failed to parse JSON data in configmap with id=%s", id)
} else {
err = s.opa.PutData(id, data)
logrus.Infof("Added data %v, err=%v", id, err)
}
}
if err != nil {
syncErr = append(syncErr, err)
}
}
if syncErr != nil {
var retries int = 0
if isPolicy {
if rStr, ok := cm.Annotations[retriesAnnotationKey]; ok {
r, err := strconv.Atoi(rStr)
if err == nil && r > 0 {
retries = r - 1
logrus.Debugf("Adding policies error cm=%v, old retry=%v, new retry=%v", path, rStr, retries)
} else if err == nil && r == 0 {
retries = defaultRetries
logrus.Debugf("Adding policies error cm=%v, old retry=%v, new retry=%v", path, rStr, retries)
}
} else {
retries = defaultRetries
logrus.Debugf("Adding policies error cm=%v, no retry annotation, new retry=%v", path, retries)
}
}
s.setAnnotations(cm, status{
Status: "error",
Error: syncErr,
}, retries)
} else {
s.setAnnotations(cm, status{
Status: "ok",
}, 0)
}
}
func (s *Sync) syncRemove(cm *v1.ConfigMap, isPolicy bool) {
logrus.Debugf("Attempting to remove cm=%v/%v, isPolicy=%v", cm.Namespace, cm.Name, isPolicy)
path := fmt.Sprintf("%v/%v", cm.Namespace, cm.Name)
for key := range cm.Data {
id := fmt.Sprintf("%v/%v", path, key)
if isPolicy {
if err := s.opa.DeletePolicy(id); err != nil {
logrus.Errorf("Failed to delete policy %v: %v", id, err)
}
} else {
if err := s.opa.PatchData(path, "remove", nil); err != nil {
logrus.Errorf("Failed to remove %v (will reset OPA data and resync in %v): %v", id, resyncPeriod, err)
s.syncReset(id)
}
}
}
}
func (s *Sync) setAnnotations(cm *v1.ConfigMap, st status, retries int) {
bs, err := json.Marshal(st)
if err != nil {
logrus.Errorf("Failed to serialize status for cm=%v/%v, err=%v", cm.Namespace, cm.Name, err)
return
}
patch := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
statusAnnotationKey: string(bs),
retriesAnnotationKey: strconv.Itoa(retries),
},
},
}
bs, err = json.Marshal(patch)
if err != nil {
logrus.Errorf("Failed to serialize patch for %v/%v: %v", cm.Namespace, cm.Name, err)
return
}
_, err = s.clientset.CoreV1().ConfigMaps(cm.Namespace).Patch(context.TODO(), cm.Name, types.StrategicMergePatchType, bs, metav1.PatchOptions{})
if err != nil {
logrus.Errorf("Failed to %v for %v/%v: %v", statusAnnotationKey, cm.Namespace, cm.Name, err)
}
}
func (s *Sync) syncReset(id string) {
logrus.Debugf("Attempting to reset %v", id)
d := syncResetBackoffMin
for {
if err := s.opa.PutData("/", map[string]interface{}{}); err != nil {
logrus.Errorf("Failed to reset OPA data for %v (will retry after %v): %v", id, d, err)
} else {
return
}
time.Sleep(d)
d = d * 2
if d > syncResetBackoffMax {
d = syncResetBackoffMax
}
}
}
// fingerprint for the labels and data of a configmap.
func fingerprint(cm *v1.ConfigMap) uint64 {
hash := fnv.New64a()
data := json.NewEncoder(hash)
data.Encode(cm.Labels)
data.Encode(cm.Data)
return hash.Sum64()
}
// errList is an error type that can marshal a list of errors to json
type errList []error
var (
// Make sure we implement the proper interfaces
_ error = errList{}
_ json.Marshaler = errList{}
)
type status struct {
Status string `json:"status"`
Error errList `json:"error,omitempty"`
}
// MarshalJSON implements json.Marshaler
func (m errList) MarshalJSON() ([]byte, error) {
if len(m) <= 0 {
return []byte(`""`), nil
}
list := make([]json.RawMessage, 0, len(m))
for _, err := range m {
if b, marshalErr := json.Marshal(err); marshalErr == nil {
list = append(list, b)
} else {
// fallback to quoted .Error() string if marshalling fails
list = append(list, []byte(fmt.Sprintf("%q", err.Error())))
}
}
if len(list) == 1 {
return list[0], nil // for backward compatibility
}
return json.Marshal(list)
}
// Error implements error
func (m errList) Error() string {
if len(m) <= 0 {
return ""
}
text := make([]string, 0, len(m))
for _, err := range m {
text = append(text, err.Error())
}
return strings.Join(text, "\n")
}
================================================
FILE: pkg/data/generic.go
================================================
// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package data
import (
"context"
"fmt"
"strings"
"sync"
"time"
opa_client "github.com/open-policy-agent/kube-mgmt/pkg/opa"
"github.com/open-policy-agent/kube-mgmt/pkg/types"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
// The min/max amount of time to wait when resetting the synchronizer.
const (
backoffMax = time.Second * 30
backoffMin = time.Second
jitterFactor = 1.2
FieldMeta = "metadata.namespace!="
)
// GenericSync replicates Kubernetes resources into OPA as raw JSON.
type GenericSync struct {
createError error // to support deprecated calls to New / Run
client dynamicClient
opa opa_client.Data
ns types.ResourceType
limiter workqueue.TypedRateLimiter[any]
jitterFactor float64
ignoreNamespaces []string
mu sync.Mutex
ready bool
}
// New returns a new GenericSync that can be started.
// Deprecated: Please Use NewFromInterface instead.
func New(kubeconfig *rest.Config, opa opa_client.Data, ns types.ResourceType) *GenericSync {
client, err := dynamic.NewForConfig(kubeconfig)
if err != nil {
return &GenericSync{createError: err}
}
return NewFromInterface(client, opa, ns)
}
type Option func(s *GenericSync)
// NewFromInterface returns a new GenericSync that can be started.
func NewFromInterface(client dynamic.Interface, opa opa_client.Data, ns types.ResourceType, opts ...Option) *GenericSync {
s := &GenericSync{
client: dynamicClient{client},
ns: ns,
opa: opa.Prefix(ns.Resource),
jitterFactor: jitterFactor,
}
for _, opt := range opts {
opt(s)
}
if s.limiter == nil { // Use default rateLimiter if not configured
s.limiter = workqueue.NewTypedItemExponentialFailureRateLimiter[any](backoffMin, backoffMax)
}
return s
}
// WithIgnoreNamespaces provides a list of namespaces to ignore
func WithIgnoreNamespaces(ignoreNamespaces []string) Option {
return func(s *GenericSync) {
s.ignoreNamespaces = ignoreNamespaces
}
}
// WithBackoff tunes the values of exponential backoff and jitter factor
func WithBackoff(min, max time.Duration, jitterFactor float64) Option {
return func(s *GenericSync) {
s.limiter = workqueue.NewTypedItemExponentialFailureRateLimiter[any](min, max)
s.jitterFactor = jitterFactor
}
}
// Run starts the synchronizer. To stop the synchronizer send a message to the
// channel.
// Deprecated: Please use RunContext instead.
func (s *GenericSync) Run() (chan struct{}, error) {
// To support legacy way of creating GenericSync from *rest.Config
if s.createError != nil {
return nil, s.createError
}
quit := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
go func() { // propagate cancel signal from channel to context
<-quit
cancel()
}()
go s.RunContext(ctx)
return quit, nil
}
// RunContext starts the synchronizer in the foreground.
// To stop the synchronizer, cancel the context.
func (s *GenericSync) RunContext(ctx context.Context) error {
if s.createError != nil {
return s.createError
}
store, queue := s.setup(ctx)
go func() {
<-ctx.Done()
queue.ShutDown()
}()
s.loop(store, queue)
return nil
}
func (s *GenericSync) Ready() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.ready
}
// setup the store and queue for this GenericSync instance
func (s *GenericSync) setup(ctx context.Context) (cache.Store, workqueue.TypedDelayingInterface[any]) {
ignoreNs := s.ignoreNs()
resource := s.client.ResourceFor(s.ns, metav1.NamespaceAll)
queue := workqueue.NewNamedDelayingQueue(s.ns.String())
store, controller := cache.NewInformerWithOptions(cache.InformerOptions{
ListerWatcher: &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = ignoreNs
return resource.List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = ignoreNs
return resource.Watch(ctx, options)
},
},
ObjectType: &unstructured.Unstructured{},
Handler: resourceEventQueue{queue},
ResyncPeriod: 0,
})
start, quit := time.Now(), ctx.Done()
go controller.Run(quit)
for !cache.WaitForCacheSync(quit, controller.HasSynced) {
logrus.Warnf("Failed to sync cache for %v, retrying...", s.ns)
}
if controller.HasSynced() {
logrus.Infof("Initial informer sync for %v completed, took %v", s.ns, time.Since(start))
}
return store, queue
}
func (s *GenericSync) ignoreNs() string {
var ignoreNs string
if !s.ns.Namespaced {
return ignoreNs
}
if len(s.ignoreNamespaces) >= 1 {
for _, ns := range s.ignoreNamespaces {
ignoreNs = FieldMeta + ns + "," + ignoreNs
}
}
ignoreNs = strings.TrimSuffix(ignoreNs, ",")
return ignoreNs
}
// resourceEventQueue is a cache.ResourceEventHandler that queues all events
type resourceEventQueue struct {
workqueue.Interface
}
// OnAdd implements ResourceHandler
func (q resourceEventQueue) OnAdd(obj interface{}, isInInitialList bool) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
logrus.Warnf("failed to retrieve key: %v", err)
return
}
q.Add(key)
}
func (q resourceEventQueue) resourceVersionMatch(oldObj, newObj interface{}) bool {
var (
oldMeta metav1.Object
newMeta metav1.Object
err error
)
oldMeta, err = meta.Accessor(oldObj)
if err == nil {
newMeta, err = meta.Accessor(newObj)
}
if err != nil {
logrus.Warnf("failed to retrieve meta: %v", err)
return false
}
return newMeta.GetResourceVersion() == oldMeta.GetResourceVersion()
}
// OnUpdate implements ResourceHandler
func (q resourceEventQueue) OnUpdate(oldObj, newObj interface{}) {
if !q.resourceVersionMatch(oldObj, newObj) { // Avoid sync flood on relist. We don't use resync.
q.OnAdd(newObj, false)
}
}
// OnDelete implements ResourceHandler
func (q resourceEventQueue) OnDelete(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
logrus.Warnf("failed to retrieve key: %v", err)
return
}
q.Add(key)
}
const initPath = ""
// loop starts replicating Kubernetes resources into OPA. If an error occurs
// during the replication process, this function will backoff and reload
// all resources into OPA from scratch.
func (s *GenericSync) loop(store cache.Store, queue workqueue.TypedDelayingInterface[any]) {
logrus.Infof("Syncing %v.", s.ns)
defer func() {
logrus.Infof("Sync for %v finished. Exiting.", s.ns)
}()
var delay time.Duration
for !queue.ShuttingDown() {
queue.AddAfter(initPath, delay) // this special path will trigger a full load
syncDone := false // discard everything until initPath
var err error
for err == nil {
key, shuttingDown := queue.Get()
if shuttingDown {
return
}
err = s.processNext(store, key.(string), &syncDone)
if key == initPath && syncDone {
s.limiter.Forget(initPath)
}
queue.Done(key)
}
delay := wait.Jitter(s.limiter.When(initPath), s.jitterFactor)
logrus.Errorf("Sync for %v failed, trying again in %v. Reason: %v", s.ns, delay, err)
}
}
func (s *GenericSync) processNext(store cache.Store, path string, syncDone *bool) error {
// On receiving the initPath, load a full dump of the data store
if path == initPath {
if *syncDone {
return nil
}
start, list := time.Now(), store.List()
if err := s.syncAll(list); err != nil {
return err
}
s.mu.Lock()
s.ready = true
s.mu.Unlock()
logrus.Infof("Loaded %d resources of kind %v into OPA. Took %v", len(list), s.ns, time.Since(start))
*syncDone = true // sync is now Done
return nil
}
// Ignore updates queued before the initial load
if !*syncDone {
return nil
}
obj, exists, err := store.GetByKey(path)
if err != nil {
return fmt.Errorf("store error: %w", err)
}
if exists {
if err := s.opa.PutData(path, obj); err != nil {
return fmt.Errorf("add event: %w", err)
}
} else {
if err := s.opa.PatchData(path, "remove", nil); err != nil {
return fmt.Errorf("delete event: %w", err)
}
}
return nil
}
func (s *GenericSync) syncAll(objs []interface{}) error {
// Build a list of patches to apply.
payload, err := generateSyncPayload(objs, s.ns.Namespaced)
if err != nil {
return err
}
return s.opa.PutData("/", payload)
}
func generateSyncPayload(objs []interface{}, namespaced bool) (map[string]interface{}, error) {
combined := make(map[string]interface{}, len(objs))
for _, obj := range objs {
path, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
return nil, err
}
// Ensure the path in the map up to our value exists
// We make some assumptions about the paths that do exist
// being the correct types due to the expected uniform
// paths for each of the similar object types being
// sync'd with the GenericSync instance.
segments := strings.Split(path, "/")
dir := combined
for i := 0; i < len(segments)-1; i++ {
next, ok := combined[segments[i]]
if !ok {
next = map[string]interface{}{}
dir[segments[i]] = next
}
dir = next.(map[string]interface{})
}
dir[segments[len(segments)-1]] = obj
}
return combined, nil
}
================================================
FILE: pkg/data/generic_test.go
================================================
package data
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/open-policy-agent/kube-mgmt/internal/expect"
"github.com/open-policy-agent/kube-mgmt/pkg/types"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/scheme"
)
type testCase struct {
Label string
ResourceType types.ResourceType
Prefix string
Objs []runtime.Object
Expected string
}
// NewFakeDynamicClient builds a new FakeDynamicClient
func newFakeDynamicClient(t *testing.T, objs ...runtime.Object) dynamicClient {
sc := runtime.NewScheme()
if err := scheme.AddToScheme(sc); err != nil {
t.Fatalf("Failed to build initial scheme: %v", err)
}
return dynamicClient{resourceInterface: fake.NewSimpleDynamicClient(sc, objs...)}
}
func TestGenericSync(t *testing.T) {
t.Parallel()
testCases := []testCase{
{
Label: "Single Cluster Resource",
ResourceType: types.ResourceType{
Namespaced: false,
Resource: "nodes",
Version: "v1",
},
Prefix: "",
Objs: []runtime.Object{
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
},
Expected: `{
"node1":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node1",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
}
}`,
},
{
Label: "Single Cluster Resource With Prefix",
ResourceType: types.ResourceType{
Namespaced: false,
Resource: "nodes",
Version: "v1",
},
Prefix: "kube",
Objs: []runtime.Object{
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
},
Expected: `{
"node1":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node1",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
}
}`,
},
{
Label: "Multiple Cluster Resources With Prefix",
ResourceType: types.ResourceType{
Namespaced: false,
Resource: "nodes",
Version: "v1",
},
Prefix: "kube",
Objs: []runtime.Object{
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node3",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
},
Expected: `{
"node1":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node1",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
},
"node2":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node2",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
},
"node3":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node3",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
}
}`,
},
{
Label: "Single Namespaced Resource",
ResourceType: types.ResourceType{
Namespaced: true,
Resource: "pods",
Version: "v1",
},
Prefix: "",
Objs: []runtime.Object{
&apiv1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns1",
ResourceVersion: "0",
},
Spec: apiv1.PodSpec{},
Status: apiv1.PodStatus{},
},
},
Expected: `{
"ns1":{
"pod1":{
"apiVersion": "v1",
"kind": "Pod",
"metadata":{
"creationTimestamp":null,
"name":"pod1",
"namespace":"ns1",
"resourceVersion":"0"
},
"spec":{
"containers":null
},
"status":{
}
}
}
}`,
},
{
Label: "Single Namespaced Resource With Prefix",
ResourceType: types.ResourceType{
Namespaced: true,
Resource: "pods",
Version: "v1",
},
Prefix: "kube",
Objs: []runtime.Object{
&apiv1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns1",
ResourceVersion: "0",
},
Spec: apiv1.PodSpec{},
Status: apiv1.PodStatus{},
},
},
Expected: `{
"ns1":{
"pod1":{
"apiVersion": "v1",
"kind": "Pod",
"metadata":{
"creationTimestamp":null,
"name":"pod1",
"namespace":"ns1",
"resourceVersion":"0"
},
"spec":{
"containers":null
},
"status":{
}
}
}
}`,
},
{
Label: "Multiple Namespaced Resources With Prefix",
ResourceType: types.ResourceType{
Namespaced: true,
Resource: "pods",
Version: "v1",
},
Prefix: "kube",
Objs: []runtime.Object{
&apiv1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns1",
ResourceVersion: "0",
},
Spec: apiv1.PodSpec{},
Status: apiv1.PodStatus{},
},
&apiv1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: "ns1",
ResourceVersion: "0",
},
Spec: apiv1.PodSpec{},
Status: apiv1.PodStatus{},
},
&apiv1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns2",
ResourceVersion: "0",
},
Spec: apiv1.PodSpec{},
Status: apiv1.PodStatus{},
},
},
Expected: `{
"ns1":{
"pod1":{
"apiVersion": "v1",
"kind": "Pod",
"metadata":{
"creationTimestamp":null,
"name":"pod1",
"namespace":"ns1",
"resourceVersion":"0"
},
"spec":{
"containers":null
},
"status":{
}
},
"pod2":{
"apiVersion": "v1",
"kind": "Pod",
"metadata":{
"creationTimestamp":null,
"name":"pod2",
"namespace":"ns1",
"resourceVersion":"0"
},
"spec":{
"containers":null
},
"status":{
}
}
},
"ns2":{
"pod1": {
"apiVersion": "v1",
"kind": "Pod",
"metadata":{
"creationTimestamp":null,
"name":"pod1",
"namespace":"ns2",
"resourceVersion":"0"
},
"spec":{
"containers":null
},
"status":{
}
}
}
}`,
},
}
for _, tc := range testCases {
tc := tc // We will be running the tests in parallel, so avoid issues with loop var
expected := expect.MustMarshal(t, expect.MustUnmarshal(t, []byte(tc.Expected)))
t.Run(fmt.Sprintf("%s - Must Generate Sync Payload", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testGenerateSyncPayload(t, expected)
})
t.Run(fmt.Sprintf("%s - Must Load Existing Resources", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testLoad(t, expected)
})
t.Run(fmt.Sprintf("%s - Must Add New Resources", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testAdd(t)
})
t.Run(fmt.Sprintf("%s - Must Remove Resources", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testDelete(t)
})
t.Run(fmt.Sprintf("%s - Must Update Resources", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testUpdate(t)
})
t.Run(fmt.Sprintf("%s - Must Retry Load On Error", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testRetryLoad(t, expected)
})
t.Run(fmt.Sprintf("%s - Must Retry Add On Error", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testRetryAdd(t)
})
t.Run(fmt.Sprintf("%s - Must Retry Update On Error", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testRetryUpdate(t)
})
t.Run(fmt.Sprintf("%s - Must Retry Delete On Error", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testRetryDelete(t)
})
}
}
func TestEventQueue(t *testing.T) {
t.Parallel()
tc := testCase{
Label: "Single Cluster Resource",
ResourceType: types.ResourceType{
Namespaced: false,
Resource: "nodes",
Version: "v1",
},
Prefix: "",
Objs: []runtime.Object{
&apiv1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
ResourceVersion: "0",
},
Spec: apiv1.NodeSpec{},
Status: apiv1.NodeStatus{},
},
},
Expected: `{
"node1":{
"apiVersion": "v1",
"kind": "Node",
"metadata":{
"creationTimestamp":null,
"name":"node1",
"resourceVersion":"0"
},
"spec":{
},
"status":{
"daemonEndpoints":{
"kubeletEndpoint":{
"Port":0
}
},
"nodeInfo":{
"architecture":"",
"bootID":"",
"containerRuntimeVersion":"",
"kernelVersion":"",
"kubeProxyVersion":"",
"kubeletVersion":"",
"machineID":"",
"operatingSystem":"",
"osImage":"",
"systemUUID":""
}
}
}
}`,
}
t.Run(fmt.Sprintf("%s - Must Update On Different ResourceVersion", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testUpdateDifferentVersion(t)
})
t.Run(fmt.Sprintf("%s - Must Skip On Same ResourceVersion", tc.Label), func(t *testing.T) {
t.Parallel()
tc.testUpdateSameVersion(t)
})
}
func (tc *testCase) testGenerateSyncPayload(t *testing.T, expected []byte) {
data := make([]interface{}, 0, len(tc.Objs))
for _, obj := range tc.Objs {
data = append(data, obj)
}
patches, err := generateSyncPayload(data, tc.ResourceType.Namespaced)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
result := expect.MustRoundTrip(t, patches)
expect.MustEqual(t, result, expected)
}
func (tc *testCase) Play(t *testing.T, client dynamicClient, play expect.Script) *expect.Client {
t.Helper()
return expect.Play(t, play, func(ctx context.Context, mockClient *expect.Client) {
data := mockClient.Prefix(tc.Prefix)
sync := NewFromInterface(
client,
data,
tc.ResourceType,
WithBackoff(0, 5*time.Second, 0),
)
sync.RunContext(ctx)
})
}
func (tc *testCase) testLoad(t *testing.T, expected []byte) {
client := newFakeDynamicClient(t, tc.Objs...)
play := expect.Script{
expect.PutData("/", expected).End(),
}
data := tc.Play(t, client, play)
expect.MustEqual(t, data.PrefixList, []string{tc.Prefix, tc.ResourceType.Resource})
}
func (tc *testCase) testAdd(t *testing.T) {
client, obj := newFakeDynamicClient(t), tc.Objs[0]
play := expect.Script{
expect.PutData("/", []byte("{}")).Do(client.MustCreate(t, tc.ResourceType, obj)),
expect.PutData(expect.MustKey(t, obj), expect.MustRoundTrip(t, obj)).End(),
}
tc.Play(t, client, play)
}
func (tc *testCase) testDelete(t *testing.T) {
client, obj := newFakeDynamicClient(t, tc.Objs...), tc.Objs[0]
play := expect.Script{
expect.PutData("/").Do(client.MustRemove(t, tc.ResourceType, obj)),
expect.PatchData(expect.MustKey(t, obj), "remove").End(),
}
tc.Play(t, client, play)
}
func (tc *testCase) testUpdate(t *testing.T) {
change := mustUnstructure(t, tc.Objs[0])
change.SetLabels(map[string]string{"test": "update"})
change.SetResourceVersion("1")
client := newFakeDynamicClient(t, tc.Objs...)
play := expect.Script{
expect.PutData("/").Do(client.MustUpdate(t, tc.ResourceType, change)),
expect.PutData(expect.MustKey(t, change), expect.MustRoundTrip(t, change.Object)).End(),
}
tc.Play(t, client, play)
}
func (tc *testCase) testRetryLoad(t *testing.T, expected []byte) {
client := newFakeDynamicClient(t, tc.Objs...)
play := expect.Script{
expect.PutData("/").DoError(errors.New("test fail update")),
expect.PutData("/", expected).End(),
}
tc.Pla
gitextract_9hv5t40p/
├── .dockerignore
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── build.yaml
│ ├── cache.yaml
│ └── release.yaml
├── .gitignore
├── .ko.yaml
├── LICENSE
├── README.md
├── charts/
│ └── opa-kube-mgmt/
│ ├── Chart.yaml
│ ├── README.md
│ ├── templates/
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── ingressroute.yaml
│ │ ├── mgmt-token-secret.yaml
│ │ ├── poddisruptionbudget.yaml
│ │ ├── rbac-mgmt-replicate.yaml
│ │ ├── rbac-mgmt.yaml
│ │ ├── rbac-sar.yaml
│ │ ├── secret-opa-config.yaml
│ │ ├── service.yaml
│ │ ├── serviceaccount.yaml
│ │ ├── servicemonitor.yaml
│ │ └── webhookconfiguration.yaml
│ ├── values.schema.json
│ └── values.yaml
├── cmd/
│ └── kube-mgmt/
│ ├── flag.go
│ ├── flag_test.go
│ └── main.go
├── devbox.json
├── devspace.yaml
├── docs/
│ ├── admission-control-1.7.md
│ ├── admission-control-crd.md
│ ├── admission-control-secure.md
│ └── tls-1.7.md
├── examples/
│ └── service_validation/
│ ├── README.md
│ ├── admission_controller.yaml
│ └── install.sh
├── go.mod
├── go.sum
├── internal/
│ └── expect/
│ ├── client.go
│ ├── request.go
│ └── script.go
├── justfile
├── pkg/
│ ├── configmap/
│ │ └── configmap.go
│ ├── data/
│ │ ├── generic.go
│ │ ├── generic_test.go
│ │ └── types.go
│ ├── dynamicdata/
│ │ ├── dynamicdata.go
│ │ └── dynamicdata_test.go
│ ├── opa/
│ │ ├── opa.go
│ │ └── opa_test.go
│ ├── types/
│ │ └── types.go
│ └── version/
│ └── version.go
└── test/
├── e2e/
│ ├── custom_config/
│ │ ├── 1_bundle_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── custom_mgmt_token/
│ │ ├── 1_policy_loaded.hurl
│ │ ├── 2_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── default/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policy_loaded.hurl
│ │ ├── 3_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── fixture-labels.yaml
│ ├── fixture-multi.yaml
│ ├── fixture-replication.yaml
│ ├── fixture.yaml
│ ├── labels/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policy_loaded.hurl
│ │ ├── 3_data_loaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── multi/
│ │ ├── 1_initial_state.hurl
│ │ ├── 2_policies_loaded.hurl
│ │ ├── 3_policy_unloaded.hurl
│ │ ├── 4_policies_reloaded.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ ├── replicate/
│ │ ├── 1_replication.hurl
│ │ ├── chainsaw-test.yaml
│ │ └── values.yaml
│ └── replicate_auto/
│ ├── .gitignore
│ ├── 1_replication.hurl
│ ├── bundle/
│ │ ├── .manifest
│ │ └── main.rego
│ ├── chainsaw-test.yaml
│ └── values.yaml
├── lint/
│ ├── images.yaml
│ ├── sa.yaml
│ ├── service.yaml
│ └── tsc.yaml
└── unit/
├── health.yaml
├── kube-mgmt_args.yaml
├── rbac_cm.yaml
├── rbac_replicate.yaml
├── sa.yaml
├── service.yaml
└── tsc.yaml
SYMBOL INDEX (171 symbols across 15 files)
FILE: cmd/kube-mgmt/flag.go
type groupVersionKind (line 13) | type groupVersionKind struct
method String (line 21) | func (gvk groupVersionKind) String() string {
method Parse (line 28) | func (gvk *groupVersionKind) Parse(value string) error {
type gvkFlag (line 50) | type gvkFlag
method String (line 52) | func (f *gvkFlag) String() string {
method Set (line 56) | func (f *gvkFlag) Set(value string) error {
method Type (line 65) | func (f *gvkFlag) Type() string {
FILE: cmd/kube-mgmt/flag_test.go
function TestFlagParsing (line 12) | func TestFlagParsing(t *testing.T) {
function TestFlagString (line 48) | func TestFlagString(t *testing.T) {
function TestPolicyFlags (line 58) | func TestPolicyFlags(t *testing.T) {
FILE: cmd/kube-mgmt/main.go
type params (line 34) | type params struct
function main (line 59) | func main() {
function run (line 131) | func run(params *params) {
function loadRESTConfig (line 275) | func loadRESTConfig(path string) (*rest.Config, error) {
function getResourceType (line 282) | func getResourceType(gvk groupVersionKind, namespaced bool) types.Resour...
FILE: internal/expect/client.go
type Client (line 11) | type Client struct
method Prefix (line 18) | func (f *Client) Prefix(path string) opa_client.Data {
method PatchData (line 24) | func (f *Client) PatchData(path string, op string, value *interface{})...
method PutData (line 38) | func (f *Client) PutData(path string, value interface{}) (err error) {
method PostData (line 49) | func (*Client) PostData(string, interface{}) (json.RawMessage, error) {
method InsertPolicy (line 54) | func (f *Client) InsertPolicy(path string, value []byte) (err error) {
method DeletePolicy (line 63) | func (f *Client) DeletePolicy(path string) (err error) {
FILE: internal/expect/request.go
type request (line 9) | type request
constant patchRequest (line 12) | patchRequest request = "PatchData"
constant putRequest (line 13) | putRequest request = "PutData"
constant insertPolicyRequest (line 14) | insertPolicyRequest request = "InsertPolicy"
constant deletePolicyRequest (line 15) | deletePolicyRequest request = "DeletePolicy"
constant noRequest (line 16) | noRequest request = "Nothing"
type Request (line 20) | type Request struct
method Equals (line 29) | func (expected Request) Equals(actual Request) bool {
method String (line 37) | func (r Request) String() string {
method Do (line 110) | func (req Request) Do(action Action) Step {
method DoError (line 118) | func (req Request) DoError(err error) Step {
method End (line 128) | func (req Request) End() Step {
function optional (line 44) | func optional(expected ...[]byte) []byte {
function PutData (line 53) | func PutData(path string, expected ...[]byte) Request {
function PatchData (line 63) | func PatchData(path string, op string, expected ...[]byte) Request {
function InsertPolicy (line 74) | func InsertPolicy(path string, expected ...[]byte) Request {
function DeletePolicy (line 84) | func DeletePolicy(path string) Request {
function Nothing (line 92) | func Nothing(duration time.Duration) Request {
type Action (line 101) | type Action
type Step (line 104) | type Step struct
FILE: internal/expect/script.go
type Script (line 18) | type Script
method String (line 21) | func (s Script) String() string {
method strings (line 156) | func (s Script) strings(sep string) string {
function Play (line 28) | func Play(t *testing.T, script Script, show func(ctx context.Context, cl...
function MustMarshal (line 100) | func MustMarshal(t *testing.T, obj interface{}) []byte {
function MustUnmarshal (line 110) | func MustUnmarshal(t *testing.T, data []byte) interface{} {
function MustRoundTrip (line 131) | func MustRoundTrip(t *testing.T, obj interface{}) []byte {
function MustEqual (line 136) | func MustEqual(t *testing.T, result, expected interface{}) {
function MustKey (line 145) | func MustKey(t *testing.T, obj runtime.Object) string {
FILE: pkg/configmap/configmap.go
constant defaultRetries (line 34) | defaultRetries = 2
constant statusAnnotationKey (line 35) | statusAnnotationKey = "openpolicyagent.org/kube-mgmt-status"
constant retriesAnnotationKey (line 36) | retriesAnnotationKey = "openpolicyagent.org/kube-mgmt-retries"
constant resyncPeriod (line 40) | resyncPeriod = time.Second * 60
constant syncResetBackoffMin (line 41) | syncResetBackoffMin = time.Second
constant syncResetBackoffMax (line 42) | syncResetBackoffMax = time.Second * 30
function CustomLabel (line 46) | func CustomLabel(key, value string) error {
function DefaultConfigMapMatcher (line 58) | func DefaultConfigMapMatcher(namespaces []string, enablePolicies, enable...
function matchesLabel (line 77) | func matchesLabel(cm *v1.ConfigMap, labelKey, labelValue string) bool {
function matchesNamespace (line 81) | func matchesNamespace(cm *v1.ConfigMap, namespaces []string) bool {
type Sync (line 91) | type Sync struct
method Run (line 127) | func (s *Sync) Run(namespaces []string) (chan struct{}, error) {
method add (line 163) | func (s *Sync) add(obj interface{}) {
method update (line 171) | func (s *Sync) update(oldObj, obj interface{}) {
method delete (line 192) | func (s *Sync) delete(obj interface{}) {
method syncAdd (line 203) | func (s *Sync) syncAdd(cm *v1.ConfigMap, isPolicy bool) {
method syncRemove (line 263) | func (s *Sync) syncRemove(cm *v1.ConfigMap, isPolicy bool) {
method setAnnotations (line 281) | func (s *Sync) setAnnotations(cm *v1.ConfigMap, st status, retries int) {
method syncReset (line 306) | func (s *Sync) syncReset(id string) {
function New (line 99) | func New(kubeconfig *rest.Config, opa opa.Client, matcher func(*v1.Confi...
function fingerprint (line 324) | func fingerprint(cm *v1.ConfigMap) uint64 {
type errList (line 333) | type errList
method MarshalJSON (line 347) | func (m errList) MarshalJSON() ([]byte, error) {
method Error (line 367) | func (m errList) Error() string {
type status (line 341) | type status struct
FILE: pkg/data/generic.go
constant backoffMax (line 33) | backoffMax = time.Second * 30
constant backoffMin (line 34) | backoffMin = time.Second
constant jitterFactor (line 35) | jitterFactor = 1.2
constant FieldMeta (line 36) | FieldMeta = "metadata.namespace!="
type GenericSync (line 40) | type GenericSync struct
method Run (line 99) | func (s *GenericSync) Run() (chan struct{}, error) {
method RunContext (line 118) | func (s *GenericSync) RunContext(ctx context.Context) error {
method Ready (line 133) | func (s *GenericSync) Ready() bool {
method setup (line 140) | func (s *GenericSync) setup(ctx context.Context) (cache.Store, workque...
method ignoreNs (line 173) | func (s *GenericSync) ignoreNs() string {
method loop (line 241) | func (s *GenericSync) loop(store cache.Store, queue workqueue.TypedDel...
method processNext (line 272) | func (s *GenericSync) processNext(store cache.Store, path string, sync...
method syncAll (line 312) | func (s *GenericSync) syncAll(objs []interface{}) error {
function New (line 54) | func New(kubeconfig *rest.Config, opa opa_client.Data, ns types.Resource...
type Option (line 62) | type Option
function NewFromInterface (line 65) | func NewFromInterface(client dynamic.Interface, opa opa_client.Data, ns ...
function WithIgnoreNamespaces (line 82) | func WithIgnoreNamespaces(ignoreNamespaces []string) Option {
function WithBackoff (line 89) | func WithBackoff(min, max time.Duration, jitterFactor float64) Option {
type resourceEventQueue (line 188) | type resourceEventQueue struct
method OnAdd (line 193) | func (q resourceEventQueue) OnAdd(obj interface{}, isInInitialList boo...
method resourceVersionMatch (line 202) | func (q resourceEventQueue) resourceVersionMatch(oldObj, newObj interf...
method OnUpdate (line 220) | func (q resourceEventQueue) OnUpdate(oldObj, newObj interface{}) {
method OnDelete (line 227) | func (q resourceEventQueue) OnDelete(obj interface{}) {
constant initPath (line 236) | initPath = ""
function generateSyncPayload (line 323) | func generateSyncPayload(objs []interface{}, namespaced bool) (map[strin...
FILE: pkg/data/generic_test.go
type testCase (line 22) | type testCase struct
method testGenerateSyncPayload (line 603) | func (tc *testCase) testGenerateSyncPayload(t *testing.T, expected []b...
method Play (line 619) | func (tc *testCase) Play(t *testing.T, client dynamicClient, play expe...
method testLoad (line 634) | func (tc *testCase) testLoad(t *testing.T, expected []byte) {
method testAdd (line 645) | func (tc *testCase) testAdd(t *testing.T) {
method testDelete (line 656) | func (tc *testCase) testDelete(t *testing.T) {
method testUpdate (line 667) | func (tc *testCase) testUpdate(t *testing.T) {
method testRetryLoad (line 682) | func (tc *testCase) testRetryLoad(t *testing.T, expected []byte) {
method testRetryAdd (line 693) | func (tc *testCase) testRetryAdd(t *testing.T) {
method testRetryUpdate (line 705) | func (tc *testCase) testRetryUpdate(t *testing.T) {
method testRetryDelete (line 723) | func (tc *testCase) testRetryDelete(t *testing.T) {
method testUpdateSameVersion (line 737) | func (tc *testCase) testUpdateSameVersion(t *testing.T) {
method testUpdateDifferentVersion (line 751) | func (tc *testCase) testUpdateDifferentVersion(t *testing.T) {
function newFakeDynamicClient (line 31) | func newFakeDynamicClient(t *testing.T, objs ...runtime.Object) dynamicC...
function TestGenericSync (line 39) | func TestGenericSync(t *testing.T) {
function TestEventQueue (line 533) | func TestEventQueue(t *testing.T) {
method MustCreate (line 767) | func (f dynamicClient) MustCreate(t *testing.T, resourceType types.Resou...
method MustRemove (line 779) | func (f dynamicClient) MustRemove(t *testing.T, resourceType types.Resou...
method MustUpdate (line 791) | func (f dynamicClient) MustUpdate(t *testing.T, resourceType types.Resou...
function mustUnstructure (line 803) | func mustUnstructure(t *testing.T, obj runtime.Object) *unstructured.Uns...
function mustAccess (line 813) | func mustAccess(t *testing.T, obj runtime.Object) metav1.Object {
function TestGenericSync_ignoreNs (line 821) | func TestGenericSync_ignoreNs(t *testing.T) {
FILE: pkg/data/types.go
type resourceInterface (line 11) | type resourceInterface interface
type dynamicClient (line 16) | type dynamicClient struct
method ResourceFor (line 21) | func (f dynamicClient) ResourceFor(resourceType types.ResourceType, na...
FILE: pkg/dynamicdata/dynamicdata.go
type Sync (line 43) | type Sync struct
method Run (line 78) | func (s *Sync) Run(ctx context.Context) error {
method Ready (line 103) | func (s *Sync) Ready() bool {
method loop (line 119) | func (s *Sync) loop(ctx context.Context, a *analyzer, rts map[string]t...
method processAnalysisResult (line 132) | func (s *Sync) processAnalysisResult(ctx context.Context, result analy...
function New (line 56) | func New(configFile string, analysisEntrypoint string, opaURL, opaAuth s...
function resolveResourceTypes (line 172) | func resolveResourceTypes(config *rest.Config) (map[string]types.Resourc...
type cancellableSync (line 211) | type cancellableSync struct
type analyzer (line 216) | type analyzer struct
method Stop (line 272) | func (a *analyzer) Stop(ctx context.Context) error {
method trigger (line 278) | func (a *analyzer) trigger(_ context.Context, txn storage.Transaction,...
method loop (line 287) | func (a *analyzer) loop(ctx context.Context) {
type analysisResult (line 225) | type analysisResult struct
function newAnalyzer (line 229) | func newAnalyzer(ctx context.Context, bs []byte, replicatePath, analysis...
type ref (line 268) | type ref struct
function analyzeRefs (line 308) | func analyzeRefs(c *ast.Compiler, entrypoints []ast.Ref, prefix ast.Ref,...
FILE: pkg/dynamicdata/dynamicdata_test.go
function TestAnalyzer (line 14) | func TestAnalyzer(t *testing.T) {
function TestAnalyzerNoDeps (line 57) | func TestAnalyzerNoDeps(t *testing.T) {
FILE: pkg/opa/opa.go
type Error (line 17) | type Error struct
method Error (line 23) | func (err *Error) Error() string {
type Undefined (line 28) | type Undefined struct
method Error (line 30) | func (Undefined) Error() string {
function IsUndefinedErr (line 36) | func IsUndefinedErr(err error) bool {
type Client (line 42) | type Client interface
type Policies (line 48) | type Policies interface
type Data (line 54) | type Data interface
function New (line 62) | func New(url string, auth string) Client {
type httpClient (line 66) | type httpClient struct
method Prefix (line 72) | func (c *httpClient) Prefix(path string) Data {
method PatchData (line 78) | func (c *httpClient) PatchData(path string, op string, value *interfac...
method PutData (line 90) | func (c *httpClient) PutData(path string, value interface{}) error {
method PostData (line 103) | func (c *httpClient) PostData(path string, value interface{}) (json.Ra...
method InsertPolicy (line 133) | func (c *httpClient) InsertPolicy(id string, bs []byte) error {
method DeletePolicy (line 143) | func (c *httpClient) DeletePolicy(id string) error {
method makePatch (line 152) | func (c *httpClient) makePatch(path, op string, value *interface{}) (i...
method handleErrors (line 171) | func (c *httpClient) handleErrors(resp *http.Response) error {
method do (line 183) | func (c *httpClient) do(verb, path string, body io.Reader) (*http.Resp...
function slashPath (line 197) | func slashPath(paths ...string) string {
function makePath (line 201) | func makePath(join string, paths ...string) string {
function joinPaths (line 205) | func joinPaths(join string, paths ...string) string {
FILE: pkg/opa/opa_test.go
function TestHTTPClientMakePatch (line 14) | func TestHTTPClientMakePatch(t *testing.T) {
function mustMakePatch (line 93) | func mustMakePatch(client *httpClient, path, op string, value *interface...
function mustUnmarshalJSON (line 103) | func mustUnmarshalJSON(r io.Reader) interface{} {
FILE: pkg/types/types.go
type ResourceType (line 11) | type ResourceType struct
method String (line 19) | func (t ResourceType) String() string {
Condensed preview — 101 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (280K chars).
[
{
"path": ".dockerignore",
"chars": 109,
"preview": "# exclude everything, then re-include only what the Go build needs\n*\n\n!go.mod\n!go.sum\n!cmd/\n!pkg/\n!internal/\n"
},
{
"path": ".editorconfig",
"chars": 346,
"preview": "root = true\n\n[*.{sh,yaml,md}]\nend_of_line = lf\ncharset = utf-8\ninsert_final_newline = true\ntrim_trailing_whitespace = tr"
},
{
"path": ".github/workflows/build.yaml",
"chars": 2521,
"preview": "name: Build\non:\n workflow_dispatch:\n push:\n paths-ignore:\n - \"docs/**\"\n - \"logo/**\"\n - \"examples/**\""
},
{
"path": ".github/workflows/cache.yaml",
"chars": 694,
"preview": "name: Update cache\n\non:\n push:\n branches:\n - master\n workflow_dispatch:\n schedule:\n - cron: '0 6 */6 * *'\n"
},
{
"path": ".github/workflows/release.yaml",
"chars": 1533,
"preview": "name: Release\n\npermissions:\n packages: write\n contents: write\n\non:\n workflow_dispatch: {}\n push:\n tags:\n - '"
},
{
"path": ".gitignore",
"chars": 72,
"preview": "./kube-mgmt\nbin\n.go\n*.tgz\n.idea\n.vscode/settings.json\n.devspace/\nbuild/\n"
},
{
"path": ".ko.yaml",
"chars": 272,
"preview": "defaultBaseImage: alpine:3.23.4\nbuilds:\n - id: kube-mgmt\n main: ./cmd/kube-mgmt\n ldflags:\n - -X github.com/o"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 9096,
"preview": "#  kube-mgmt\n\n`kube-mgmt` manages policies / data of [Open Policy Agent](https://github.com/open"
},
{
"path": "charts/opa-kube-mgmt/Chart.yaml",
"chars": 583,
"preview": "apiVersion: v1\nappVersion: 0.0.0 # managed by git tag\nversion: 0.0.0 # managed by git tag\ndescription: Manage OPA in Kub"
},
{
"path": "charts/opa-kube-mgmt/README.md",
"chars": 2882,
"preview": "# Manage OPA in Kubernetes with kube-mgmt sidecar.\n\n[OPA](https://www.openpolicyagent.org) is an open-source general-pur"
},
{
"path": "charts/opa-kube-mgmt/templates/_helpers.tpl",
"chars": 3418,
"preview": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"opa.name\" -}}\n{{- default .Cha"
},
{
"path": "charts/opa-kube-mgmt/templates/deployment.yaml",
"chars": 10269,
"preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ template \"opa.fullname\" . }}\n labels:\n{{ include \"opa.labels."
},
{
"path": "charts/opa-kube-mgmt/templates/ingressroute.yaml",
"chars": 714,
"preview": "{{- if .Values.e2e }}\napiVersion: traefik.io/v1alpha1\nkind: IngressRouteTCP\nmetadata:\n name: {{ include \"opa.fullname\" "
},
{
"path": "charts/opa-kube-mgmt/templates/mgmt-token-secret.yaml",
"chars": 175,
"preview": "{{- if .Values.e2eMgmtTokenSecret -}}\napiVersion: v1\nkind: Secret\nmetadata:\n name: mgmt-token-secret\ntype: Opaque\nstrin"
},
{
"path": "charts/opa-kube-mgmt/templates/poddisruptionbudget.yaml",
"chars": 670,
"preview": "{{- if .Values.podDisruptionBudget.enabled }}\n{{- if .Capabilities.APIVersions.Has \"policy/v1/PodDisruptionBudget\" }}\nap"
},
{
"path": "charts/opa-kube-mgmt/templates/rbac-mgmt-replicate.yaml",
"chars": 1094,
"preview": "{{- if and .Values.rbac.create .Values.mgmt.enabled -}}\n{{- $arr := concat .Values.mgmt.replicate.cluster .Values.mgmt.r"
},
{
"path": "charts/opa-kube-mgmt/templates/rbac-mgmt.yaml",
"chars": 2919,
"preview": "{{- define \"opa.rbac.cm.rules\" -}}\nrules:\n - apiGroups: [\"\"]\n resources: [\"configmaps\"]\n verbs: [\"get\", \"list\", \""
},
{
"path": "charts/opa-kube-mgmt/templates/rbac-sar.yaml",
"chars": 1016,
"preview": "{{- if (and .Values.rbac.create .Values.sar.enabled) -}}\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\n"
},
{
"path": "charts/opa-kube-mgmt/templates/secret-opa-config.yaml",
"chars": 246,
"preview": "{{- if .Values.opa -}}\napiVersion: v1\nkind: Secret\nmetadata:\n name: {{ template \"opa.fullname\" . }}-config\n labels:\n{{"
},
{
"path": "charts/opa-kube-mgmt/templates/service.yaml",
"chars": 695,
"preview": "kind: Service\napiVersion: v1\nmetadata:\n name: {{ template \"opa.fullname\" . }}\n labels:\n{{ include \"opa.labels.standard"
},
{
"path": "charts/opa-kube-mgmt/templates/serviceaccount.yaml",
"chars": 407,
"preview": "{{- if .Values.serviceAccount.create }}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: {{ template \"opa.serviceAc"
},
{
"path": "charts/opa-kube-mgmt/templates/servicemonitor.yaml",
"chars": 1041,
"preview": "{{- if and (.Capabilities.APIVersions.Has \"monitoring.coreos.com/v1\") .Values.prometheus.enabled .Values.serviceMonitor."
},
{
"path": "charts/opa-kube-mgmt/templates/webhookconfiguration.yaml",
"chars": 4981,
"preview": "{{- $cn := printf \"%s.%s.svc\" ( include \"opa.fullname\" . ) .Release.Namespace }}\n{{- $ca := genCA \"opa-admission-ca\" 365"
},
{
"path": "charts/opa-kube-mgmt/values.schema.json",
"chars": 3333,
"preview": "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"$id\": \"https://github.com/open-policy-agent/kube-mgmt\",\n \""
},
{
"path": "charts/opa-kube-mgmt/values.yaml",
"chars": 9209,
"preview": "# Default values for opa.\n# -----------------------\n#\n# OPA configuration file. See https://www.openpolicyagent.org/docs"
},
{
"path": "cmd/kube-mgmt/flag.go",
"chars": 1311,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "cmd/kube-mgmt/flag_test.go",
"chars": 3574,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/open-policy-agent/kube-mgmt/pkg/configmap\"\n\t\"github"
},
{
"path": "cmd/kube-mgmt/main.go",
"chars": 10275,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "devbox.json",
"chars": 691,
"preview": "{\n \"$schema\": \"https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json\",\n \"packages\": [\n"
},
{
"path": "devspace.yaml",
"chars": 2355,
"preview": "version: v2beta1\nname: opa-kube-mgmt\n\nvars:\n DEVSPACE_FLAGS: \"-n default --no-warn\"\n KO_PLATFORMS: \"linux/amd64\"\n KO_"
},
{
"path": "docs/admission-control-1.7.md",
"chars": 4156,
"preview": "# Admission Control (1.7 and 1.8)\n\n**Note: Admission Control has undergone changes in Kubernetes 1.7 through 1.9. If you"
},
{
"path": "docs/admission-control-crd.md",
"chars": 4449,
"preview": "# Admission Control For Custom Resources\n\nIn the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kube"
},
{
"path": "docs/admission-control-secure.md",
"chars": 7036,
"preview": "# Admission Control Secure\n\nIn the [Kubernetes Admission Control](http://www.openpolicyagent.org/docs/kubernetes-admissi"
},
{
"path": "docs/tls-1.7.md",
"chars": 2492,
"preview": "# Generating TLS Certificates (1.7)\n\nExternal Admission Controllers must be secured with TLS. At a minimum you must:\n\n- "
},
{
"path": "examples/service_validation/README.md",
"chars": 3846,
"preview": "# Kubernetes Admission Control for preventing open AWS LoadBalancers\n\nKubernetes Service objects of type [LoadBalancer]("
},
{
"path": "examples/service_validation/admission_controller.yaml",
"chars": 2864,
"preview": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: opa-sa\n---\nkind: ClusterRoleBinding\napiVersion: rbac.authorization"
},
{
"path": "examples/service_validation/install.sh",
"chars": 1356,
"preview": "#!/bin/bash\n\nset -ex\n\nOUT_DIR=/tmp/opa\nrm -rf ${OUT_DIR}; mkdir -p ${OUT_DIR}\n\nopenssl genrsa -out ${OUT_DIR}/ca.key 204"
},
{
"path": "go.mod",
"chars": 4149,
"preview": "module github.com/open-policy-agent/kube-mgmt\n\ngo 1.24.11\n\nrequire (\n\tgithub.com/open-policy-agent/opa v1.5.1\n\tgithub.co"
},
{
"path": "go.sum",
"chars": 26811,
"preview": "github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\n"
},
{
"path": "internal/expect/client.go",
"chars": 1513,
"preview": "package expect\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\topa_client \"github.com/open-policy-agent/kube-mgmt/pkg/opa\"\n)\n\n// "
},
{
"path": "internal/expect/request.go",
"chars": 3089,
"preview": "package expect\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype request string\n\nconst (\n\tpatchRequest request = \"Patch"
},
{
"path": "internal/expect/script.go",
"chars": 4383,
"preview": "package expect\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"k8s.io/apimachin"
},
{
"path": "justfile",
"chars": 3710,
"preview": "K3D := \"kube-mgmt\"\nTEST_RESULTS := 'build/test-results'\n\n@_default:\n @just --list\n\n# golang linter\n[group('code quality"
},
{
"path": "pkg/configmap/configmap.go",
"chars": 11051,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "pkg/data/generic.go",
"chars": 9648,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "pkg/data/generic_test.go",
"chars": 20826,
"preview": "package data\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/open-policy-agent/kube-mgmt/internal"
},
{
"path": "pkg/data/types.go",
"chars": 891,
"preview": "package data\n\nimport (\n\t\"github.com/open-policy-agent/kube-mgmt/pkg/types\"\n\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\""
},
{
"path": "pkg/dynamicdata/dynamicdata.go",
"chars": 9547,
"preview": "package dynamicdata\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/open-policy-agent/kube-mgmt"
},
{
"path": "pkg/dynamicdata/dynamicdata_test.go",
"chars": 2107,
"preview": "package dynamicdata\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t//lint:ignore SA1019 using OPA v0.x to ensure backwards com"
},
{
"path": "pkg/opa/opa.go",
"chars": 4970,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "pkg/opa/opa_test.go",
"chars": 1930,
"preview": "// Copyright 2018 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "pkg/types/types.go",
"chars": 746,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "pkg/version/version.go",
"chars": 259,
"preview": "// Copyright 2017 The OPA Authors. All rights reserved.\n// Use of this source code is governed by an Apache2\n// license"
},
{
"path": "test/e2e/custom_config/1_bundle_loaded.hurl",
"chars": 152,
"preview": "GET https://localhost:8443/v1/data\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result.test_helm_kube"
},
{
"path": "test/e2e/custom_config/chainsaw-test.yaml",
"chars": 311,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: custom-config\nspec:\n namespace: default\n steps:\n"
},
{
"path": "test/e2e/custom_config/values.yaml",
"chars": 255,
"preview": "opa:\n services:\n controller:\n url: 'https://www.openpolicyagent.org'\n bundles:\n quickstart:\n service: "
},
{
"path": "test/e2e/custom_mgmt_token/1_policy_loaded.hurl",
"chars": 310,
"preview": "GET https://localhost:8443/v1/policies\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result[?(@.id == "
},
{
"path": "test/e2e/custom_mgmt_token/2_data_loaded.hurl",
"chars": 332,
"preview": "GET https://localhost:8443/v1/data/default\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result.*\" cou"
},
{
"path": "test/e2e/custom_mgmt_token/chainsaw-test.yaml",
"chars": 1301,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: custom-mgmt-token\nspec:\n namespace: default\n ste"
},
{
"path": "test/e2e/custom_mgmt_token/values.yaml",
"chars": 113,
"preview": "e2eMgmtTokenSecret: true\nopa:\n replicas: 2\nauthz:\n enabled: true\n mgmtToken:\n secretName: mgmt-token-secret"
},
{
"path": "test/e2e/default/1_initial_state.hurl",
"chars": 120,
"preview": "GET https://localhost:8443/v1/data\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result.*\" count == 0\n"
},
{
"path": "test/e2e/default/2_policy_loaded.hurl",
"chars": 310,
"preview": "GET https://localhost:8443/v1/policies\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result[?(@.id == "
},
{
"path": "test/e2e/default/3_data_loaded.hurl",
"chars": 332,
"preview": "GET https://localhost:8443/v1/data/default\nAuthorization: Bearer {{token}}\n\nHTTP 200\n[Asserts]\njsonpath \"$.result.*\" cou"
},
{
"path": "test/e2e/default/chainsaw-test.yaml",
"chars": 1468,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: default\nspec:\n namespace: default\n steps:\n - "
},
{
"path": "test/e2e/default/values.yaml",
"chars": 0,
"preview": ""
},
{
"path": "test/e2e/fixture-labels.yaml",
"chars": 1099,
"preview": "---\nkind: ConfigMap\nmetadata:\n name: policy-include\n labels:\n kube-mgmt/e2e: \"true\"\n qweqwe/policy: \"111\"\napiVer"
},
{
"path": "test/e2e/fixture-multi.yaml",
"chars": 741,
"preview": "---\nkind: ConfigMap\nmetadata:\n name: multi-file-policy\n labels:\n kube-mgmt/e2e: \"true\"\n openpolicyagent.org/poli"
},
{
"path": "test/e2e/fixture-replication.yaml",
"chars": 479,
"preview": "---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: ignore-me\n---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: dont"
},
{
"path": "test/e2e/fixture.yaml",
"chars": 1127,
"preview": "---\nkind: ConfigMap\nmetadata:\n name: policy-include\n labels:\n kube-mgmt/e2e: \"true\"\n openpolicyagent.org/policy:"
},
{
"path": "test/e2e/labels/1_initial_state.hurl",
"chars": 86,
"preview": "GET http://localhost:8080/v1/data\nHTTP 200\n[Asserts]\njsonpath \"$.result.*\" count == 0\n"
},
{
"path": "test/e2e/labels/2_policy_loaded.hurl",
"chars": 242,
"preview": "GET http://localhost:8080/v1/policies\nHTTP 200\n[Asserts]\njsonpath \"$.result[?(@.id == 'default/policy-include/include.re"
},
{
"path": "test/e2e/labels/3_data_loaded.hurl",
"chars": 264,
"preview": "GET http://localhost:8080/v1/data/default\nHTTP 200\n[Asserts]\njsonpath \"$.result.*\" count == 1\njsonpath \"$.result.data-in"
},
{
"path": "test/e2e/labels/chainsaw-test.yaml",
"chars": 1341,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: labels\nspec:\n namespace: default\n steps:\n - n"
},
{
"path": "test/e2e/labels/values.yaml",
"chars": 268,
"preview": "useHttps: false\n\nopa: null\n\nauthz:\n enabled: false\n\nmgmt:\n startupProbe:\n httpGet:\n scheme: HTTP\n extraArgs:\n"
},
{
"path": "test/e2e/multi/1_initial_state.hurl",
"chars": 99,
"preview": "GET http://localhost:8080/v1/data/my_pkg/my_rule\nHTTP 200\n[Asserts]\njsonpath \"$.result\" not exists\n"
},
{
"path": "test/e2e/multi/2_policies_loaded.hurl",
"chars": 569,
"preview": "GET http://localhost:8080/v1/policies\nHTTP 200\n[Asserts]\njsonpath \"$.result\" count == 2\njsonpath \"$.result[?(@.id == 'de"
},
{
"path": "test/e2e/multi/3_policy_unloaded.hurl",
"chars": 99,
"preview": "GET http://localhost:8080/v1/data/my_pkg/my_rule\nHTTP 200\n[Asserts]\njsonpath \"$.result\" not exists\n"
},
{
"path": "test/e2e/multi/4_policies_reloaded.hurl",
"chars": 569,
"preview": "GET http://localhost:8080/v1/policies\nHTTP 200\n[Asserts]\njsonpath \"$.result\" count == 2\njsonpath \"$.result[?(@.id == 'de"
},
{
"path": "test/e2e/multi/chainsaw-test.yaml",
"chars": 2055,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: multi\nspec:\n namespace: default\n steps:\n - na"
},
{
"path": "test/e2e/multi/values.yaml",
"chars": 146,
"preview": "useHttps: false\n\nopa: null\n\nauthz:\n enabled: false\n\nmgmt:\n startupProbe:\n httpGet:\n scheme: HTTP\n extraArgs:\n"
},
{
"path": "test/e2e/replicate/1_replication.hurl",
"chars": 393,
"preview": "GET http://localhost:8080/v1/data/kubernetes/services/ignore-me\nHTTP 200\n[Asserts]\njsonpath \"$.result\" not exists\n\nGET h"
},
{
"path": "test/e2e/replicate/chainsaw-test.yaml",
"chars": 411,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: replicate\nspec:\n steps:\n - name: apply fixture"
},
{
"path": "test/e2e/replicate/values.yaml",
"chars": 378,
"preview": "useHttps: false\n\nopa: null\n\nauthz:\n enabled: false\n\nmgmt:\n data:\n enabled: false\n policies:\n enabled: false\n s"
},
{
"path": "test/e2e/replicate_auto/.gitignore",
"chars": 13,
"preview": "bundle.tar.gz"
},
{
"path": "test/e2e/replicate_auto/1_replication.hurl",
"chars": 393,
"preview": "GET http://localhost:8080/v1/data/kubernetes/services/ignore-me\nHTTP 200\n[Asserts]\njsonpath \"$.result\" not exists\n\nGET h"
},
{
"path": "test/e2e/replicate_auto/bundle/.manifest",
"chars": 25,
"preview": "{\n \"roots\": [\"main\"]\n}"
},
{
"path": "test/e2e/replicate_auto/bundle/main.rego",
"chars": 123,
"preview": "package main\n\nimport rego.v1\n\nmain if {\n some ns, name\n data.kubernetes.services[ns][name].metadata.labels == \"foo"
},
{
"path": "test/e2e/replicate_auto/chainsaw-test.yaml",
"chars": 416,
"preview": "apiVersion: chainsaw.kyverno.io/v1alpha1\nkind: Test\nmetadata:\n name: replicate-auto\nspec:\n steps:\n - name: apply fi"
},
{
"path": "test/e2e/replicate_auto/values.yaml",
"chars": 570,
"preview": "useHttps: false\n\nopa: {\n bundles: {\n test: {\n resource: file:///bundle/bundle.tar.gz\n }\n }\n}\n\nauthz:\n enab"
},
{
"path": "test/lint/images.yaml",
"chars": 1960,
"preview": "suite: lint image and mgmt.image\ntemplates:\n - fake.yaml\ntests:\n - it: image is null\n set:\n image: null\n as"
},
{
"path": "test/lint/sa.yaml",
"chars": 246,
"preview": "suite: lint serviceaccount\ntemplates:\n - fake.yaml\ntests:\n - it: annotations not string\n set:\n serviceAccount:"
},
{
"path": "test/lint/service.yaml",
"chars": 1101,
"preview": "suite: lint service\ntemplates:\n - service.yaml\ntests:\n - it: fails when service annotation is boolean\n set:\n s"
},
{
"path": "test/lint/tsc.yaml",
"chars": 2068,
"preview": "suite: lint topologySpreadConstraints\ntemplates:\n - deployment.yaml\ntests:\n - it: fails when maxSkew is missing\n se"
},
{
"path": "test/unit/health.yaml",
"chars": 1996,
"preview": "suite: test health probes\ntemplates:\n - deployment.yaml\ntests:\n - it: should have only liveness and readiness for OPA\n"
},
{
"path": "test/unit/kube-mgmt_args.yaml",
"chars": 1886,
"preview": "suite: test kube-mgmt container args\ntemplates:\n - deployment.yaml\ntests:\n - it: should have default args\n asserts:"
},
{
"path": "test/unit/rbac_cm.yaml",
"chars": 2401,
"preview": "suite: test configmap rbac\ntemplates:\n - rbac-mgmt.yaml\ntests:\n - it: should create current namespace role by default\n"
},
{
"path": "test/unit/rbac_replicate.yaml",
"chars": 1453,
"preview": "suite: test replicate rbac\ntemplates:\n - rbac-mgmt-replicate.yaml\ntests:\n - it: should not create cluster role by defa"
},
{
"path": "test/unit/sa.yaml",
"chars": 485,
"preview": "suite: test serviceaccount annotations\ntemplates:\n - serviceaccount.yaml\ntests:\n - it: should omit serviceaccount anno"
},
{
"path": "test/unit/service.yaml",
"chars": 765,
"preview": "suite: test service definition\ntemplates:\n - service.yaml\ntests:\n - it: should omit service annotations when null\n "
},
{
"path": "test/unit/tsc.yaml",
"chars": 5250,
"preview": "suite: test topologySpreadConstraints\ntemplates:\n - deployment.yaml\ntests:\n - it: renders with DoNotSchedule policy\n "
}
]
About this extraction
This page contains the full source code of the open-policy-agent/kube-mgmt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 101 files (250.2 KB), approximately 78.1k tokens, and a symbol index with 171 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.