diff --git a/.github/workflows/make-self-upgrade.yaml b/.github/workflows/make-self-upgrade.yaml
index 6ba55ab1..caeb5689 100644
--- a/.github/workflows/make-self-upgrade.yaml
+++ b/.github/workflows/make-self-upgrade.yaml
@@ -38,7 +38,7 @@ jobs:
scope: 'jetstack/jetstack-secure'
identity: make-self-upgrade
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Adding `fetch-depth: 0` makes sure tags are also fetched. We need
# the tags so `git describe` returns a valid version.
# see https://github.com/actions/checkout/issues/701 for extra info about this option
@@ -50,7 +50,7 @@ jobs:
run: |
make print-go-version >> "$GITHUB_OUTPUT"
- - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ steps.go-version.outputs.result }}
@@ -81,7 +81,7 @@ jobs:
git push -f origin "$SELF_UPGRADE_BRANCH"
- if: ${{ steps.is-up-to-date.outputs.result != 'true' }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.octo-sts.outputs.token }}
script: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b05e2ae0..c5d1702f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,7 +37,7 @@ jobs:
go-version: ${{ steps.go-version.outputs.result }}
- id: release
- run: make release ark-release
+ run: make release ark-release ngts-release
outputs:
RELEASE_OCI_PREFLIGHT_IMAGE: ${{ steps.release.outputs.RELEASE_OCI_PREFLIGHT_IMAGE }}
@@ -50,6 +50,12 @@ jobs:
ARK_CHART: ${{ steps.release.outputs.ARK_CHART }}
ARK_CHART_TAG: ${{ steps.release.outputs.ARK_CHART_TAG }}
ARK_CHART_DIGEST: ${{ steps.release.outputs.ARK_CHART_DIGEST }}
+ NGTS_IMAGE: ${{ steps.release.outputs.NGTS_IMAGE }}
+ NGTS_IMAGE_TAG: ${{ steps.release.outputs.NGTS_IMAGE_TAG }}
+ NGTS_IMAGE_DIGEST: ${{ steps.release.outputs.NGTS_IMAGE_DIGEST }}
+ NGTS_CHART: ${{ steps.release.outputs.NGTS_CHART }}
+ NGTS_CHART_TAG: ${{ steps.release.outputs.NGTS_CHART_TAG }}
+ NGTS_CHART_DIGEST: ${{ steps.release.outputs.NGTS_CHART_DIGEST }}
github_release:
runs-on: ubuntu-latest
@@ -73,6 +79,12 @@ jobs:
echo "ARK_CHART: ${{ needs.build_and_push.outputs.ARK_CHART }}" >> .notes-file
echo "ARK_CHART_TAG: ${{ needs.build_and_push.outputs.ARK_CHART_TAG }}" >> .notes-file
echo "ARK_CHART_DIGEST: ${{ needs.build_and_push.outputs.ARK_CHART_DIGEST }}" >> .notes-file
+ echo "NGTS_IMAGE: ${{ needs.build_and_push.outputs.NGTS_IMAGE }}" >> .notes-file
+ echo "NGTS_IMAGE_TAG: ${{ needs.build_and_push.outputs.NGTS_IMAGE_TAG }}" >> .notes-file
+ echo "NGTS_IMAGE_DIGEST: ${{ needs.build_and_push.outputs.NGTS_IMAGE_DIGEST }}" >> .notes-file
+ echo "NGTS_CHART: ${{ needs.build_and_push.outputs.NGTS_CHART }}" >> .notes-file
+ echo "NGTS_CHART_TAG: ${{ needs.build_and_push.outputs.NGTS_CHART_TAG }}" >> .notes-file
+ echo "NGTS_CHART_DIGEST: ${{ needs.build_and_push.outputs.NGTS_CHART_DIGEST }}" >> .notes-file
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 8f64a20f..f4952b96 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -66,7 +66,89 @@ jobs:
path: _bin/downloaded
key: downloaded-${{ runner.os }}-${{ hashFiles('klone.yaml') }}-test-unit
- - run: make -j test-unit test-helm
+ # NB: helm unit tests will be run by "make verify", so we don't run it here
+ - run: make -j test-unit
+ env:
+ # These environment variables are required to run the CyberArk client integration tests
+ ARK_DISCOVERY_API: https://platform-discovery.integration-cyberark.cloud/
+ ARK_SUBDOMAIN: ${{ secrets.ARK_SUBDOMAIN }}
+ ARK_USERNAME: ${{ secrets.ARK_USERNAME }}
+ ARK_SECRET: ${{ secrets.ARK_SECRET }}
+
+ ark-test-e2e:
+ # TEMPORARY: require an explicit label to test disco-agent until the test environment fixes a recurring issue
+ # where the e2e fails with a 400 error relating to "conflicting tagging values"
+ # The test is flaky, not broken and re-running eventually makes it pass - but that delays progress on
+ # other unrelated work.
+ if: contains(github.event.pull_request.labels.*.name, 'test-ark')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ # Adding `fetch-depth: 0` makes sure tags are also fetched. We need
+ # the tags so `git describe` returns a valid version.
+ # see https://github.com/actions/checkout/issues/701 for extra info about this option
+ with: { fetch-depth: 0 }
+
+ - uses: ./.github/actions/repo_access
+ with:
+ DEPLOY_KEY_READ_VENAFI_CONNECTION_LIB: ${{ secrets.DEPLOY_KEY_READ_VENAFI_CONNECTION_LIB }}
+
+ - id: go-version
+ run: |
+ make print-go-version >> "$GITHUB_OUTPUT"
+
+ - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
+ with:
+ go-version: ${{ steps.go-version.outputs.result }}
+
+ - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: _bin/downloaded
+ key: downloaded-${{ runner.os }}-${{ hashFiles('klone.yaml') }}-test-unit
+
+ - run: make -j ark-test-e2e
+ env:
+ OCI_BASE: ${{ secrets.ARK_OCI_BASE }}
+ # These environment variables are required to connect to CyberArk Disco APIs
+ ARK_DISCOVERY_API: https://platform-discovery.integration-cyberark.cloud/
+ ARK_SUBDOMAIN: ${{ secrets.ARK_SUBDOMAIN }}
+ ARK_USERNAME: ${{ secrets.ARK_USERNAME }}
+ ARK_SECRET: ${{ secrets.ARK_SECRET }}
+
+ ngts-test-e2e:
+ # TEMPORARY: require an explicit label to test NGTS until we have a stable test environment
+ if: contains(github.event.pull_request.labels.*.name, 'test-ngts')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ # Adding `fetch-depth: 0` makes sure tags are also fetched. We need
+ # the tags so `git describe` returns a valid version.
+ # see https://github.com/actions/checkout/issues/701 for extra info about this option
+ with: { fetch-depth: 0 }
+
+ - uses: ./.github/actions/repo_access
+ with:
+ DEPLOY_KEY_READ_VENAFI_CONNECTION_LIB: ${{ secrets.DEPLOY_KEY_READ_VENAFI_CONNECTION_LIB }}
+
+ - id: go-version
+ run: |
+ make print-go-version >> "$GITHUB_OUTPUT"
+
+ - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
+ with:
+ go-version: ${{ steps.go-version.outputs.result }}
+
+ - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: _bin/downloaded
+ key: downloaded-${{ runner.os }}-${{ hashFiles('klone.yaml') }}-test-unit
+
+ - run: make -j ngts-test-e2e
+ env:
+ OCI_BASE: ${{ secrets.NGTS_OCI_BASE }}
+ NGTS_CLIENT_ID: e3c8bde7-5f13-11f1-99f4-5e067e231041
+ NGTS_PRIVATE_KEY: ${{ secrets.NGTS_PRIVATE_KEY }}
+ NGTS_TSG_URL: https://1806660206.ngts.qa.venafi.io
test-e2e:
if: contains(github.event.pull_request.labels.*.name, 'test-e2e')
@@ -108,7 +190,7 @@ jobs:
id: timestamp # Give the step an ID to reference its output
run: |
# Generate a timestamp in the format YYMMDD-HHMMSS.
- # Extracting from PR name would require sanitization due to GKE cluster naming constraints
+ # Extracting from PR name would require sanitization due to GKE cluster naming constraints
TIMESTAMP=$(date +'%y%m%d-%H%M%S')
CLUSTER_NAME="test-secretless-${TIMESTAMP}"
echo "Generated cluster name: ${CLUSTER_NAME}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..55c84d2c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,243 @@
+# Contributing to Discovery Agent
+
+Thank you for your interest in contributing! This document provides guidelines and instructions for contributing.
+
+Note that this repository holds two separate components:
+
+- disco-agent: For CyberArk DisCo
+- venafi-kubernetes-agent: For TLSPK / Certificate Manager SaaS
+
+## Table of Contents
+
+- [Getting Started](#getting-started)
+- [Development Environment](#development-environment)
+- [Making Changes](#making-changes)
+- [Testing](#testing)
+- [Submitting a Pull Request](#submitting-a-pull-request)
+- [Code Review Process](#code-review-process)
+- [Additional Resources](#additional-resources)
+
+### Prerequisites
+
+Before you begin, ensure you have the following installed:
+
+- [Go](https://golang.org/doc/install) (version specified in `go.mod`)
+- [Make](https://www.gnu.org/software/make/)
+- [Git](https://git-scm.com/)
+- [Docker](https://docs.docker.com/get-docker/) (for building container images)
+
+To check which Go version will be used:
+
+```bash
+make which-go
+```
+
+It's also possible to use a vendored version of Go, via `make vendor-go`.
+
+### Repository Tooling
+
+Most of the setup logic for provisioning tooling and for handling builds and testing
+is defined in Makefile logic.
+
+Specifically, `the make/_shared` directory contains shared Makefile logic derived from
+the cert-manager [makefile-modules](https://github.com/cert-manager/makefile-modules/) project.
+
+### Setting Up Your Development Environment
+
+1. **Fork the repository** on GitHub
+
+2. **Clone your fork:**
+
+ ```bash
+ git clone git@github.com:YOUR-USERNAME/jetstack-secure.git
+ cd jetstack-secure
+ ```
+
+3. **Add the upstream remote:**
+
+ ```bash
+ git remote add upstream git@github.com:jetstack/jetstack-secure.git
+ ```
+
+4. **Run initial verification:**
+
+ ```bash
+ make verify
+ ```
+
+ This ensures your environment is set up correctly.
+
+## Development Environment
+
+### Local Execution
+
+To build and run the agent locally:
+
+```bash
+go run main.go agent --agent-config-file ./path/to/agent/config/file.yaml -p 0h1m0s
+```
+
+Example configuration files are available:
+- [agent.yaml](./agent.yaml)
+- [examples/one-shot-secret.yaml](./examples/one-shot-secret.yaml)
+- [examples/cert-manager-agent.yaml](./examples/cert-manager-agent.yaml)
+
+You can also run a local echo server to monitor agent requests:
+
+```bash
+go run main.go echo
+```
+
+### Useful Make Targets
+
+- `make help` - Show all available make targets
+- `make verify` - Run all verification checks (linting, formatting, etc.)
+- `make test-unit` - Run unit tests
+- `make test-helm` - Run Helm chart tests
+- `make generate` - Generate code, documentation, and other artifacts
+- `make oci-build-preflight` - Build container image
+- `make clean` - Clean all temporary files
+
+## Making Changes
+
+### Creating a Branch
+
+Always create a new branch for your changes:
+
+```bash
+git checkout -b feature/your-feature-name
+```
+
+Use descriptive branch names:
+- `feature/` for new features
+- `fix/` for bug fixes
+- `docs/` for documentation changes
+- `refactor/` for refactoring
+
+### Code Style
+
+This project follows standard Go conventions:
+
+- Run `make verify-golangci-lint` to check your code
+- Run `make fix-golangci-lint` to automatically fix some issues
+- Ensure all code is formatted with `gofmt`
+- Follow the [Effective Go](https://golang.org/doc/effective_go) guidelines
+- Most of the conventions are enforced by linters, and violations will prevent code being merged
+
+### Committing Changes
+
+1. **Stage your changes:**
+
+ ```bash
+ git add .
+ ```
+
+2. **Run verification before committing:**
+
+ ```bash
+ make verify
+ ```
+
+3. **Commit with a descriptive message:**
+
+ ```bash
+ git commit -m "Brief description of your changes"
+ ```
+
+ Write clear commit messages:
+ - Use the imperative mood ("Add feature" not "Added feature")
+ - Keep the first line under 72 characters
+ - Add additional context in the body if needed
+
+## Testing
+
+### Running Tests Locally
+
+Before submitting a PR, ensure all tests pass:
+
+```bash
+# Run unit tests
+make test-unit
+
+# Run Helm tests
+make test-helm
+
+# Run all verification checks
+make verify
+```
+
+### End-to-End Tests
+
+E2E tests run automatically in CI when you add specific labels to your PR:
+
+- Add the `test-e2e` label to trigger GKE-based E2E tests
+- Add the `keep-e2e-cluster` label if you need to keep the cluster for debugging (remember to delete it manually afterward to avoid costs)
+
+The E2E test script is located at [hack/e2e/test.sh](./hack/e2e/test.sh).
+
+### Writing Tests
+
+- Add unit tests for all new functionality
+- Place tests in `*_test.go` files alongside the code they test
+- Use the [testify](https://github.com/stretchr/testify) library for assertions
+- Aim for meaningful test coverage, not just high percentages
+
+## Submitting a Pull Request
+
+1. **Push your branch to your fork:**
+
+ ```bash
+ git push origin feature/your-feature-name
+ ```
+
+2. **Create a Pull Request** on GitHub from your fork to the `master` branch of `jetstack/jetstack-secure`
+
+3. **Fill out the PR description** with:
+ - Clear description of the changes
+ - Related issue numbers (if applicable)
+ - Testing instructions
+ - Any breaking changes or special considerations
+
+4. **Ensure CI passes:**
+ - All tests must pass
+ - Code must pass verification / linting checks
+ - No merge conflicts
+
+## Code Review Process
+
+### For All Contributors
+
+- PRs require approval before merging
+- Keep PRs focused and reasonably sized
+- Update your branch if `master` has moved forward:
+
+ ```bash
+ git fetch upstream
+ git rebase upstream/master
+ git push --force-with-lease origin feature/your-feature-name
+ ```
+
+### For CyberArk Contributors
+
+**Contributors from inside CyberArk should reach out to the cert-manager team for reviews for PRs which are passing CI.**
+
+The cert-manager team maintains this project and will provide code reviews and guidance for merging changes.
+
+## Additional Resources
+
+- [Project Documentation](https://docs.cyberark.com/mis-saas/vaas/k8s-components/c-tlspk-agent-overview/)
+- [Issue Tracker](https://github.com/jetstack/jetstack-secure/issues)
+- [Release Process](./RELEASE.md)
+- [cert-manager Community](https://cert-manager.io/docs/contributing/)
+
+## Getting Help
+
+If you need help or have questions:
+
+1. Check existing [issues](https://github.com/jetstack/jetstack-secure/issues) and [documentation](https://docs.cyberark.com/mis-saas/vaas/k8s-components/c-tlspk-agent-overview/)
+2. Open a new issue with the `question` label
+3. For CyberArk contributors, reach out to the cert-manager team
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the license in the LICENSE file in the root directory of this repository.
diff --git a/LICENSES b/LICENSES
index d27cf6dd..34dbafd1 100644
--- a/LICENSES
+++ b/LICENSES
@@ -37,8 +37,6 @@ You can retrieve the actual license text by following these steps:
---
cel.dev/expr,Apache-2.0
-github.com/Khan/genqlient/graphql,MIT
-github.com/Venafi/vcert/v5,Apache-2.0
github.com/antlr4-go/antlr/v4,BSD-3-Clause
github.com/aymerick/douceur,MIT
github.com/beorn7/perks/quantile,MIT
@@ -57,30 +55,40 @@ github.com/go-logr/zapr,Apache-2.0
github.com/go-openapi/jsonpointer,Apache-2.0
github.com/go-openapi/jsonreference,Apache-2.0
github.com/go-openapi/swag,Apache-2.0
+github.com/go-openapi/swag/cmdutils,Apache-2.0
+github.com/go-openapi/swag/conv,Apache-2.0
+github.com/go-openapi/swag/fileutils,Apache-2.0
+github.com/go-openapi/swag/jsonname,Apache-2.0
+github.com/go-openapi/swag/jsonutils,Apache-2.0
+github.com/go-openapi/swag/loading,Apache-2.0
+github.com/go-openapi/swag/mangling,Apache-2.0
+github.com/go-openapi/swag/netutils,Apache-2.0
+github.com/go-openapi/swag/stringutils,Apache-2.0
+github.com/go-openapi/swag/typeutils,Apache-2.0
+github.com/go-openapi/swag/yamlutils,Apache-2.0
github.com/go418/concurrentcache,Apache-2.0
github.com/go418/concurrentcache/logger,Apache-2.0
-github.com/gogo/protobuf,BSD-3-Clause
github.com/golang-jwt/jwt/v4,MIT
github.com/golang-jwt/jwt/v5,MIT
-github.com/google/btree,Apache-2.0
github.com/google/cel-go,Apache-2.0
github.com/google/cel-go,BSD-3-Clause
github.com/google/gnostic-models,Apache-2.0
github.com/google/uuid,BSD-3-Clause
github.com/gorilla/css/scanner,BSD-3-Clause
-github.com/gorilla/websocket,BSD-2-Clause
github.com/hashicorp/errwrap,MPL-2.0
github.com/hashicorp/go-multierror,MPL-2.0
-github.com/josharian/intern,MIT
github.com/json-iterator/go,MIT
-github.com/mailru/easyjson,MIT
+github.com/lestrrat-go/blackmagic,MIT
+github.com/lestrrat-go/httpcc,MIT
+github.com/lestrrat-go/httprc/v3,MIT
+github.com/lestrrat-go/jwx/v3,MIT
+github.com/lestrrat-go/option/v2,MIT
github.com/mattn/go-colorable,MIT
github.com/mattn/go-isatty,MIT
github.com/microcosm-cc/bluemonday,BSD-3-Clause
github.com/modern-go/concurrent,Apache-2.0
github.com/modern-go/reflect2,Apache-2.0
github.com/munnerz/goautoneg,BSD-3-Clause
-github.com/pkg/errors,BSD-2-Clause
github.com/pmezard/go-difflib/difflib,BSD-3-Clause
github.com/pmylund/go-cache,MIT
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil,BSD-3-Clause
@@ -91,23 +99,23 @@ github.com/prometheus/procfs,Apache-2.0
github.com/sosodev/duration,MIT
github.com/spf13/cobra,Apache-2.0
github.com/spf13/pflag,BSD-3-Clause
-github.com/stoewer/go-strcase,MIT
github.com/stretchr/testify,MIT
-github.com/vektah/gqlparser/v2,MIT
+github.com/valyala/fastjson,MIT
github.com/x448/float16,MIT
-github.com/youmark/pkcs8,MIT
go.opentelemetry.io/otel,Apache-2.0
+go.opentelemetry.io/otel,BSD-3-Clause
go.opentelemetry.io/otel/trace,Apache-2.0
+go.opentelemetry.io/otel/trace,BSD-3-Clause
go.uber.org/multierr,MIT
go.uber.org/zap,MIT
go.yaml.in/yaml/v2,Apache-2.0
go.yaml.in/yaml/v3,MIT
-golang.org/x/crypto,BSD-3-Clause
-golang.org/x/exp,BSD-3-Clause
+golang.org/x/crypto/pbkdf2,BSD-3-Clause
+golang.org/x/exp/slices,BSD-3-Clause
golang.org/x/net,BSD-3-Clause
golang.org/x/oauth2,BSD-3-Clause
golang.org/x/sync,BSD-3-Clause
-golang.org/x/sys,BSD-3-Clause
+golang.org/x/sys/unix,BSD-3-Clause
golang.org/x/term,BSD-3-Clause
golang.org/x/text,BSD-3-Clause
golang.org/x/time/rate,BSD-3-Clause
@@ -117,8 +125,6 @@ google.golang.org/genproto/googleapis/rpc/status,Apache-2.0
google.golang.org/protobuf,BSD-3-Clause
gopkg.in/evanphx/json-patch.v4,BSD-3-Clause
gopkg.in/inf.v0,BSD-3-Clause
-gopkg.in/ini.v1,Apache-2.0
-gopkg.in/yaml.v2,Apache-2.0
gopkg.in/yaml.v3,MIT
k8s.io/api,Apache-2.0
k8s.io/apiextensions-apiserver/pkg,Apache-2.0
@@ -137,6 +143,7 @@ k8s.io/kube-openapi/pkg/validation/strfmt,Apache-2.0
k8s.io/kube-openapi/pkg/validation/validate,Apache-2.0
k8s.io/utils,Apache-2.0
k8s.io/utils/internal/third_party/forked/golang,BSD-3-Clause
+k8s.io/utils/third_party/forked/golang/btree,Apache-2.0
sigs.k8s.io/controller-runtime/pkg,Apache-2.0
sigs.k8s.io/json,Apache-2.0
sigs.k8s.io/json,BSD-3-Clause
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 672704c9..6de8798c 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -12,3 +12,4 @@ aliases:
- inteon
- thatsmrtalbot
- erikgb
+ - hjoshi123
diff --git a/RELEASE.md b/RELEASE.md
index d386a0a7..0f1f5939 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,7 +1,8 @@
# Release Process
> [!NOTE]
-> Before starting, let Michael McLoughlin know that a release is about to be created so that documentation can be prepared in advance.
+> Before starting a release let the docs team know that a release is about to be created so that documentation can be prepared in advance.
+> This is not necessary for pre-releases.
The release process is semi-automated.
@@ -15,34 +16,32 @@ The release process is semi-automated.
> - Build and publish the Helm chart: `oci://quay.io/jetstack/charts/venafi-kubernetes-agent`,
> - Build and publish the container image: `quay.io/jetstack/disco-agent`,
> - Build and publish the Helm chart: `oci://quay.io/jetstack/charts/disco-agent`,
+> - Build and publish the container image: `quay.io/jetstack/discovery-agent`,
+> - Build and publish the Helm chart: `oci://quay.io/jetstack/charts/discovery-agent`,
> - Create a draft GitHub release,
-1. Upgrade the Go dependencies.
-
- You will need to install `go-mod-upgrade`:
+1. Run govulncheck; it's the best indicator that a dependency needs to be upgraded.
```bash
- go install github.com/oligot/go-mod-upgrade@latest
+ make verify-govulncheck
```
- Then, run the following:
+ Any failures should be treated extremely seriously and patched before release unless you can be absolutely
+ confident it's a false positive.
+
+2. Consider upgrading Go dependencies using `go-mod-upgrade`:
```bash
+ go install github.com/oligot/go-mod-upgrade@latest
go-mod-upgrade
make generate
```
- Finally, create a PR with the changes and merge it.
+ Once complete, you'll need to create a PR to merge the changes.
-2. Open the [tests GitHub Actions workflow][tests-workflow]
+3. Open the [tests GitHub Actions workflow][tests-workflow]
and verify that it succeeds on the master branch.
-3. Run govulncheck:
-
- ```bash
- make verify-govulncheck
- ```
-
4. Create a tag for the new release:
```sh
@@ -51,9 +50,11 @@ The release process is semi-automated.
git push origin "${VERSION}"
```
-5. Wait until the GitHub Actions finishes.
+ This triggers a [release action](https://github.com/jetstack/jetstack-secure/actions/workflows/release.yml).
-6. Navigate to the GitHub Releases page and select the draft release to edit.
+5. Wait until the release action finishes.
+
+6. Navigate to the [GitHub Releases](https://github.com/jetstack/jetstack-secure/releases) page and select the draft release to edit.
1. Click on “Generate release notes” to automatically compile the changelog.
2. Review and refine the generated notes to ensure they’re clear and useful
@@ -63,11 +64,10 @@ The release process is semi-automated.
7. Publish the release.
-8. Inform the `#venctl` channel that a new version of Discovery Agent has been
- released. Make sure to share any breaking change that may affect `venctl connect`
- or `venctl generate`.
+8. Inform the `#venafi-kubernetes-agent` channel on Slack that a new version of the Discovery Agent has been released!
+ Consider also messaging the DisCo team at CyberArk (ask in the cert-manager team Slack channel if you don't know who to message)
-9. Inform Michael McLoughlin of the new release so he can update the
+9. Inform the docs team of the new release so they can update the
documentation at .
[tests-workflow]: https://github.com/jetstack/jetstack-secure/actions/workflows/tests.yaml?query=branch%3Amaster
@@ -76,13 +76,16 @@ The release process is semi-automated.
For context, the new tag will create the following images:
-| Image | Automation |
-| --------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
-| `quay.io/jetstack/venafi-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
-| `quay.io/jetstack/disco-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
-| `registry.venafi.cloud/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
-| `private-registry.venafi.cloud/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
-| `private-registry.venafi.eu/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
+| Image | Automation |
+| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| `quay.io/jetstack/venafi-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
+| `quay.io/jetstack/disco-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
+| `quay.io/jetstack/discovery-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
+| `registry.venafi.cloud/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
+| `private-registry.venafi.cloud/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
+| `private-registry.venafi.eu/venafi-agent/venafi-agent` | Automatically mirrored by Harbor Replication rule |
+| `registry.ngts.paloaltonetworks.com/disco-agent/disco-agent` | Automatically mirrored by Harbor Replication rule |
+| `registry.ngts.paloaltonetworks.com/discovery-agent/discovery-agent` | Automatically mirrored by Harbor Replication rule |
and the following OCI Helm charts:
@@ -90,28 +93,33 @@ and the following OCI Helm charts:
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `oci://quay.io/jetstack/charts/venafi-kubernetes-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
| `oci://quay.io/jetstack/charts/disco-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
+| `oci://quay.io/jetstack/charts/discovery-agent` | Automatically built by the [release action](.github/workflows/release.yml) on Git tag pushes |
| `oci://registry.venafi.cloud/charts/venafi-kubernetes-agent` | Automatically mirrored by Harbor Replication rule |
| `oci://private-registry.venafi.cloud/charts/venafi-kubernetes-agent` | Automatically mirrored by Harbor Replication rule |
| `oci://private-registry.venafi.eu/charts/venafi-kubernetes-agent` | Automatically mirrored by Harbor Replication rule |
+| `oci://registry.ngts.paloaltonetworks.com/charts/disco-agent` | Automatically mirrored by Harbor Replication rule |
+| `oci://registry.ngts.paloaltonetworks.com/charts/discovery-agent` | Automatically mirrored by Harbor Replication rule |
-Here is replication flow for OCI Helm charts:
+### Replication Flows
+
+TODO: These flows are helpful illustrations but describe a process whose source of truth is defined elsewhere. Instead, we should document the replication process where it's defined, in enterprise-builds.
+
+Replication flow for the venafi-kubernetes-agent Helm chart:
```text
v1.1.0 (Git tag in the jetstack-secure repo)
└── oci://quay.io/jetstack/charts/venafi-kubernetes-agent --version 1.1.0 (GitHub Actions in the jetstack-secure repo)
- ├── oci://us.gcr.io/jetstack-secure-enterprise/charts/venafi-kubernetes-agent (Enterprise Builds's GitHub Actions)
└── oci://eu.gcr.io/jetstack-secure-enterprise/charts/venafi-kubernetes-agent (Enterprise Builds's GitHub Actions)
├── oci://registry.venafi.cloud/charts/venafi-kubernetes-agent --version 1.1.0 (Harbor Replication)
└── oci://private-registry.venafi.cloud/charts/venafi-kubernetes-agent --version 1.1.0 (Harbor Replication)
└── oci://private-registry.venafi.eu/charts/venafi-kubernetes-agent --version 1.1.0 (Harbor Replication)
```
-And the replication flow for Docker images:
+Replication flow for the venafi-kubernetes-agent container image:
```text
v1.1.0 (Git tag in the jetstack-secure repo)
└── quay.io/jetstack/venafi-agent:v1.1.0 (GitHub Actions in the jetstack-secure repo)
- ├── us.gcr.io/jetstack-secure-enterprise/venafi-agent:v1.1.0 (Enterprise Builds's GitHub Actions)
└── eu.gcr.io/jetstack-secure-enterprise/venafi-agent:v1.1.0 (Enterprise Builds's GitHub Actions)
├── registry.venafi.cloud/venafi-agent/venafi-agent:v1.1.0 (Harbor Replication)
├── private-registry.venafi.cloud/venafi-agent/venafi-agent:v1.1.0 (Harbor Replication)
@@ -122,10 +130,6 @@ v1.1.0 (Git tag in the jetstack-secure repo)
[private-img-and-chart-replication.tf]: https://gitlab.com/venafi/vaas/delivery/harbor/-/blob/3d114f54092eb44a1deb0edc7c4e8a2d4f855aa2/private-registry/module/subsystems/tlspk/replication.tf
[release_enterprise_builds.yaml]: https://github.com/jetstack/enterprise-builds/actions/workflows/release_enterprise_builds.yaml
-### Step 2: Test the Helm chart "venafi-kubernetes-agent" with venctl connect
-
-NOTE(mael): TBD
-
-### Step 3: Test the Helm chart "disco-agent"
+## Step 2: Testing
-NOTE(wallrj): TBD
+When a release is complete, consider installing it into a cluster and testing it. TODO: provide guidance on doing those tests.
diff --git a/api/datareading.go b/api/datareading.go
index 0556dd63..3ea95b3f 100644
--- a/api/datareading.go
+++ b/api/datareading.go
@@ -64,6 +64,7 @@ func (o *DataReading) UnmarshalJSON(data []byte) error {
target any
assign func(any)
}{
+ {&OIDCDiscoveryData{}, func(v any) { o.Data = v.(*OIDCDiscoveryData) }},
{&DiscoveryData{}, func(v any) { o.Data = v.(*DiscoveryData) }},
{&DynamicData{}, func(v any) { o.Data = v.(*DynamicData) }},
}
@@ -130,14 +131,14 @@ func (v *GatheredResource) UnmarshalJSON(data []byte) error {
return nil
}
-// DynamicData is the DataReading.Data returned by the k8s.DataGathererDynamic
+// DynamicData is the DataReading.Data returned by the k8sdynamic.DataGathererDynamic
// gatherer
type DynamicData struct {
// Items is a list of GatheredResource
Items []*GatheredResource `json:"items"`
}
-// DiscoveryData is the DataReading.Data returned by the k8s.ConfigDiscovery
+// DiscoveryData is the DataReading.Data returned by the k8sdiscovery.DataGathererDiscovery
// gatherer
type DiscoveryData struct {
// ClusterID is the unique ID of the Kubernetes cluster which this snapshot was taken from.
@@ -149,3 +150,18 @@ type DiscoveryData struct {
// See https://godoc.org/k8s.io/apimachinery/pkg/version#Info
ServerVersion *version.Info `json:"server_version"`
}
+
+// OIDCDiscoveryData is the DataReading.Data returned by the oidc.OIDCDiscovery
+// gatherer
+type OIDCDiscoveryData struct {
+ // OIDCConfig contains OIDC configuration data from the API server's
+ // `/.well-known/openid-configuration` endpoint
+ OIDCConfig map[string]any `json:"openid_configuration,omitempty"`
+ // OIDCConfigError contains any error encountered while fetching the OIDC configuration
+ OIDCConfigError string `json:"openid_configuration_error,omitempty"`
+
+ // JWKS contains JWKS data from the API server's `/openid/v1/jwks` endpoint
+ JWKS map[string]any `json:"jwks,omitempty"`
+ // JWKSError contains any error encountered while fetching the JWKS
+ JWKSError string `json:"jwks_error,omitempty"`
+}
diff --git a/api/datareading_test.go b/api/datareading_test.go
index 9fa90b01..087877d8 100644
--- a/api/datareading_test.go
+++ b/api/datareading_test.go
@@ -75,6 +75,20 @@ func TestDataReading_UnmarshalJSON(t *testing.T) {
}`,
wantDataType: &DynamicData{},
},
+ {
+ name: "OIDCDiscoveryData type",
+ input: `{
+ "cluster_id": "11111111-2222-3333-4444-555555555555",
+ "data-gatherer": "oidc",
+ "timestamp": "2024-06-01T12:00:00Z",
+ "data": {
+ "openid_configuration": {"issuer": "https://example.com"},
+ "jwks": {"keys": []}
+ },
+ "schema_version": "v1"
+ }`,
+ wantDataType: &OIDCDiscoveryData{},
+ },
{
name: "Invalid JSON",
input: `not a json`,
diff --git a/cmd/agent_test.go b/cmd/agent_test.go
index b3591468..488d5a21 100644
--- a/cmd/agent_test.go
+++ b/cmd/agent_test.go
@@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -32,7 +33,14 @@ func TestOutputModes(t *testing.T) {
})
t.Run("machinehub", func(t *testing.T) {
+ if strings.ToLower(os.Getenv("ARK_LIVE_TEST")) != "true" {
+ t.Skip("set ARK_LIVE_TEST=true to run this test against the live service")
+ return
+ }
arktesting.SkipIfNoEnv(t)
+
+ t.Log("This test runs against a live service and has been known to flake. If you see timeout issues it's possible that the test is flaking and it could be unrelated to your changes.")
+
runSubprocess(t, repoRoot, []string{
"--agent-config-file", filepath.Join(repoRoot, "examples/machinehub/config.yaml"),
"--input-path", filepath.Join(repoRoot, "examples/machinehub/input.json"),
diff --git a/deploy/charts/disco-agent/README.md b/deploy/charts/disco-agent/README.md
index 2b4a69a6..32107f78 100644
--- a/deploy/charts/disco-agent/README.md
+++ b/deploy/charts/disco-agent/README.md
@@ -91,11 +91,61 @@ kubectl logs deployments/disco-agent --namespace "${NAMESPACE}" --follow
> ```
This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
+#### **acceptTerms** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+Must be set to indicate that you have read and accepted the CyberArk Terms of Service. If false, the helm chart will fail to install and will print a message with instructions on how to accept the TOS.
+#### **imageRegistry** ~ `string`
+> Default value:
+> ```yaml
+> quay.io
+> ```
+
+The container registry used for disco-agent images by default. This can include path prefixes (e.g. "artifactory.example.com/docker").
+
+#### **imageNamespace** ~ `string`
+> Default value:
+> ```yaml
+> jetstack
+> ```
+
+The repository namespace used for disco-agent images by default.
+Examples:
+- jetstack
+- custom-namespace
+
+#### **image.registry** ~ `string`
+
+Deprecated: per-component registry prefix.
+
+If set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from
+`imageRegistry` + `imageNamespace` + `image.name`.
+
+This can produce "double registry" style references such as
+`legacy.example.io/quay.io/jetstack/...`. Prefer using the global
+`imageRegistry`/`imageNamespace` values.
+
#### **image.repository** ~ `string`
> Default value:
> ```yaml
> ""
> ```
+
+Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`).
+Example: quay.io/jetstack/disco-agent
+
+#### **image.name** ~ `string`
+> Default value:
+> ```yaml
+> disco-agent
+> ```
+
+The image name for the Discovery Agent.
+This is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.
+
#### **image.pullPolicy** ~ `string`
> Default value:
> ```yaml
@@ -109,14 +159,14 @@ This sets the pull policy for images.
> ""
> ```
-Overrides the image tag whose default is the chart appVersion.
+Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.
#### **image.digest** ~ `string`
> Default value:
> ```yaml
> ""
> ```
-The image digest
+Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.
#### **imagePullSecrets** ~ `array`
> Default value:
> ```yaml
@@ -295,6 +345,13 @@ This cluster name will be associated with the data that the agent uploads to the
A short description of the cluster where the agent is deployed (optional).
This description will be associated with the data that the agent uploads to the Discovery and Context service. The description may include contact information such as the email address of the cluster administrator, so that any problems and risks identified by the Discovery and Context service can be communicated to the people responsible for the affected secrets.
+#### **config.sendSecretValues** ~ `bool`
+> Default value:
+> ```yaml
+> true
+> ```
+
+Enable sending of Secret values to CyberArk in addition to metadata. Metadata is always sent, but the actual values of Secrets are not sent by default. When enabled, Secret data is encrypted using envelope encryption using a key managed by CyberArk, fetched from the Discovery and Context service.
#### **authentication.secretName** ~ `string`
> Default value:
> ```yaml
@@ -399,3 +456,5 @@ endpointAdditionalProperties:
targetLabel: instance
```
+
+
diff --git a/deploy/charts/disco-agent/templates/NOTES.txt b/deploy/charts/disco-agent/templates/NOTES.txt
index 2aea6a74..407a30a7 100644
--- a/deploy/charts/disco-agent/templates/NOTES.txt
+++ b/deploy/charts/disco-agent/templates/NOTES.txt
@@ -7,3 +7,7 @@ APP VERSION: {{ .Chart.AppVersion }}
- Check the application logs for successful connection to the platform:
> kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
+
+{{ if .Values.config.sendSecretValues }}
+NB: sendSecretValues is set to "true". Encrypted secret data will be sent to the CyberArk Discovery and Context service
+{{ end }}
diff --git a/deploy/charts/disco-agent/templates/_helpers.tpl b/deploy/charts/disco-agent/templates/_helpers.tpl
index 73107c5f..4cd42b1e 100644
--- a/deploy/charts/disco-agent/templates/_helpers.tpl
+++ b/deploy/charts/disco-agent/templates/_helpers.tpl
@@ -60,3 +60,59 @@ Create the name of the service account to use
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+
+{{/*
+Util function for generating an image reference based on the provided options.
+This function is derived from similar functions used in the cert-manager GitHub organization
+*/}}
+{{- define "disco-agent.image" -}}
+{{- /*
+Calling convention:
+- (tuple )
+We intentionally pass imageRegistry/imageNamespace as explicit arguments rather than reading
+from `.Values` inside this helper, because `helm-tool lint` does not reliably track `.Values.*`
+usage through tuple/variable indirection.
+*/ -}}
+{{- if ne (len .) 4 -}}
+ {{- fail (printf "ERROR: template \"disco-agent.image\" expects (tuple ), got %d arguments" (len .)) -}}
+{{- end -}}
+{{- $image := index . 0 -}}
+{{- $imageRegistry := index . 1 | default "" -}}
+{{- $imageNamespace := index . 2 | default "" -}}
+{{- $defaultReference := index . 3 -}}
+{{- $repository := "" -}}
+{{- if $image.repository -}}
+ {{- $repository = $image.repository -}}
+ {{- /*
+ Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry.
+ */ -}}
+ {{- if $image.registry -}}
+ {{- $repository = printf "%s/%s" $image.registry $repository -}}
+ {{- end -}}
+{{- else -}}
+ {{- $name := required "ERROR: image.name must be set when image.repository is empty" $image.name -}}
+ {{- $repository = $name -}}
+ {{- if $imageNamespace -}}
+ {{- $repository = printf "%s/%s" $imageNamespace $repository -}}
+ {{- end -}}
+ {{- if $imageRegistry -}}
+ {{- $repository = printf "%s/%s" $imageRegistry $repository -}}
+ {{- end -}}
+ {{- /*
+ Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry.
+ */ -}}
+ {{- if $image.registry -}}
+ {{- $repository = printf "%s/%s" $image.registry $repository -}}
+ {{- end -}}
+{{- end -}}
+{{- $repository -}}
+{{- if and $image.tag $image.digest -}}
+ {{- printf ":%s@%s" $image.tag $image.digest -}}
+{{- else if $image.tag -}}
+ {{- printf ":%s" $image.tag -}}
+{{- else if $image.digest -}}
+ {{- printf "@%s" $image.digest -}}
+{{- else -}}
+ {{- printf "%s" $defaultReference -}}
+{{- end -}}
+{{- end }}
diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml
index 231a26cd..e88ae910 100644
--- a/deploy/charts/disco-agent/templates/configmap.yaml
+++ b/deploy/charts/disco-agent/templates/configmap.yaml
@@ -19,6 +19,8 @@ data:
{{- . | toYaml | nindent 6 }}
{{- end }}
data-gatherers:
+ - kind: oidc
+ name: ark/oidc
- kind: k8s-discovery
name: ark/discovery
- kind: k8s-dynamic
@@ -107,3 +109,39 @@ data:
resource-type:
version: v1
resource: pods
+ - kind: k8s-dynamic
+ name: ark/configmaps
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+ - kind: k8s-dynamic
+ name: ark/esoexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+ - kind: k8s-dynamic
+ name: ark/esosecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+ - kind: k8s-dynamic
+ name: ark/esoclusterexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+ - kind: k8s-dynamic
+ name: ark/esoclustersecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
diff --git a/deploy/charts/disco-agent/templates/deployment.yaml b/deploy/charts/disco-agent/templates/deployment.yaml
index 74390e81..0c98b9a5 100644
--- a/deploy/charts/disco-agent/templates/deployment.yaml
+++ b/deploy/charts/disco-agent/templates/deployment.yaml
@@ -1,3 +1,6 @@
+{{- if not .Values.acceptTerms }}
+ {{- fail "\n\n=================================================================\n Terms & Conditions Notice\n=================================================================\n\nBefore installing this application, you must review and accept\nthe terms and conditions available at:\nhttps://www.cyberark.com/contract-terms/\n\nTo proceed with installation, you must indicate acceptance by\nsetting:\n\n - In your values file: acceptTerms: true\n or\n - Via the Helm flag: --set acceptTerms=true\n\nBy continuing with the next command, you confirm that you have\nreviewed and accepted these terms and conditions.\n\n=================================================================\n" }}
+{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -36,7 +39,7 @@ spec:
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}{{- with .Values.image.digest }}@{{ . }}{{- end }}"
+ image: "{{ template "disco-agent.image" (tuple .Values.image .Values.imageRegistry .Values.imageNamespace (printf ":%s" .Chart.AppVersion)) }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: POD_NAMESPACE
@@ -76,6 +79,8 @@ spec:
name: {{ .Values.authentication.secretName }}
key: ARK_DISCOVERY_API
optional: true
+ - name: ARK_SEND_SECRET_VALUES
+ value: {{ .Values.config.sendSecretValues | default "false" | quote }}
{{- with .Values.http_proxy }}
- name: HTTP_PROXY
value: {{ . }}
diff --git a/deploy/charts/disco-agent/templates/rbac.yaml b/deploy/charts/disco-agent/templates/rbac.yaml
index f3fae414..92bd1349 100644
--- a/deploy/charts/disco-agent/templates/rbac.yaml
+++ b/deploy/charts/disco-agent/templates/rbac.yaml
@@ -95,4 +95,48 @@ subjects:
- kind: ServiceAccount
name: {{ include "disco-agent.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
-
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "disco-agent.fullname" . }}-oidc-discovery
+ labels:
+ {{- include "disco-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: system:service-account-issuer-discovery
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "disco-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "disco-agent.fullname" . }}-eso-reader
+ labels:
+ {{- include "disco-agent.labels" . | nindent 4 }}
+rules:
+ - apiGroups: ["external-secrets.io"]
+ resources:
+ - externalsecrets
+ - clusterexternalsecrets
+ - secretstores
+ - clustersecretstores
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "disco-agent.fullname" . }}-eso-reader
+ labels:
+ {{- include "disco-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: {{ include "disco-agent.fullname" . }}-eso-reader
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "disco-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap
index 2c70df00..3ee3c884 100644
--- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap
+++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap
@@ -7,6 +7,8 @@ custom-cluster-description:
cluster_description: "A cloud hosted Kubernetes cluster hosting production workloads.\n\nteam: team-1\nemail: team-1@example.com\npurpose: Production workloads\n"
period: "12h0m0s"
data-gatherers:
+ - kind: oidc
+ name: ark/oidc
- kind: k8s-discovery
name: ark/discovery
- kind: k8s-dynamic
@@ -95,6 +97,42 @@ custom-cluster-description:
resource-type:
version: v1
resource: pods
+ - kind: k8s-dynamic
+ name: ark/configmaps
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+ - kind: k8s-dynamic
+ name: ark/esoexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+ - kind: k8s-dynamic
+ name: ark/esosecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+ - kind: k8s-dynamic
+ name: ark/esoclusterexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+ - kind: k8s-dynamic
+ name: ark/esoclustersecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
kind: ConfigMap
metadata:
labels:
@@ -114,6 +152,8 @@ custom-cluster-name:
cluster_description: ""
period: "12h0m0s"
data-gatherers:
+ - kind: oidc
+ name: ark/oidc
- kind: k8s-discovery
name: ark/discovery
- kind: k8s-dynamic
@@ -202,6 +242,42 @@ custom-cluster-name:
resource-type:
version: v1
resource: pods
+ - kind: k8s-dynamic
+ name: ark/configmaps
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+ - kind: k8s-dynamic
+ name: ark/esoexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+ - kind: k8s-dynamic
+ name: ark/esosecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+ - kind: k8s-dynamic
+ name: ark/esoclusterexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+ - kind: k8s-dynamic
+ name: ark/esoclustersecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
kind: ConfigMap
metadata:
labels:
@@ -221,6 +297,8 @@ custom-period:
cluster_description: ""
period: "1m"
data-gatherers:
+ - kind: oidc
+ name: ark/oidc
- kind: k8s-discovery
name: ark/discovery
- kind: k8s-dynamic
@@ -309,6 +387,42 @@ custom-period:
resource-type:
version: v1
resource: pods
+ - kind: k8s-dynamic
+ name: ark/configmaps
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+ - kind: k8s-dynamic
+ name: ark/esoexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+ - kind: k8s-dynamic
+ name: ark/esosecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+ - kind: k8s-dynamic
+ name: ark/esoclusterexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+ - kind: k8s-dynamic
+ name: ark/esoclustersecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
kind: ConfigMap
metadata:
labels:
@@ -328,6 +442,8 @@ defaults:
cluster_description: ""
period: "12h0m0s"
data-gatherers:
+ - kind: oidc
+ name: ark/oidc
- kind: k8s-discovery
name: ark/discovery
- kind: k8s-dynamic
@@ -416,6 +532,42 @@ defaults:
resource-type:
version: v1
resource: pods
+ - kind: k8s-dynamic
+ name: ark/configmaps
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+ - kind: k8s-dynamic
+ name: ark/esoexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+ - kind: k8s-dynamic
+ name: ark/esosecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+ - kind: k8s-dynamic
+ name: ark/esoclusterexternalsecrets
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+ - kind: k8s-dynamic
+ name: ark/esoclustersecretstores
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
kind: ConfigMap
metadata:
labels:
diff --git a/deploy/charts/disco-agent/values.schema.json b/deploy/charts/disco-agent/values.schema.json
index 35fa2d40..401b11a1 100644
--- a/deploy/charts/disco-agent/values.schema.json
+++ b/deploy/charts/disco-agent/values.schema.json
@@ -3,6 +3,9 @@
"helm-values": {
"additionalProperties": false,
"properties": {
+ "acceptTerms": {
+ "$ref": "#/$defs/helm-values.acceptTerms"
+ },
"affinity": {
"$ref": "#/$defs/helm-values.affinity"
},
@@ -30,9 +33,15 @@
"image": {
"$ref": "#/$defs/helm-values.image"
},
+ "imageNamespace": {
+ "$ref": "#/$defs/helm-values.imageNamespace"
+ },
"imagePullSecrets": {
"$ref": "#/$defs/helm-values.imagePullSecrets"
},
+ "imageRegistry": {
+ "$ref": "#/$defs/helm-values.imageRegistry"
+ },
"metrics": {
"$ref": "#/$defs/helm-values.metrics"
},
@@ -84,6 +93,11 @@
},
"type": "object"
},
+ "helm-values.acceptTerms": {
+ "default": false,
+ "description": "Must be set to indicate that you have read and accepted the CyberArk Terms of Service. If false, the helm chart will fail to install and will print a message with instructions on how to accept the TOS.",
+ "type": "boolean"
+ },
"helm-values.affinity": {
"default": {},
"type": "object"
@@ -118,6 +132,9 @@
},
"period": {
"$ref": "#/$defs/helm-values.config.period"
+ },
+ "sendSecretValues": {
+ "$ref": "#/$defs/helm-values.config.sendSecretValues"
}
},
"type": "object"
@@ -148,6 +165,11 @@
"description": "Push data every 12 hours unless changed.",
"type": "string"
},
+ "helm-values.config.sendSecretValues": {
+ "default": true,
+ "description": "Enable sending of Secret values to CyberArk in addition to metadata. Metadata is always sent, but the actual values of Secrets are not sent by default. When enabled, Secret data is encrypted using envelope encryption using a key managed by CyberArk, fetched from the Discovery and Context service.",
+ "type": "boolean"
+ },
"helm-values.extraArgs": {
"default": [],
"description": "extraArgs:\n- --logging-format=json\n- --log-level=6 # To enable HTTP request logging",
@@ -175,9 +197,15 @@
"digest": {
"$ref": "#/$defs/helm-values.image.digest"
},
+ "name": {
+ "$ref": "#/$defs/helm-values.image.name"
+ },
"pullPolicy": {
"$ref": "#/$defs/helm-values.image.pullPolicy"
},
+ "registry": {
+ "$ref": "#/$defs/helm-values.image.registry"
+ },
"repository": {
"$ref": "#/$defs/helm-values.image.repository"
},
@@ -189,7 +217,12 @@
},
"helm-values.image.digest": {
"default": "",
- "description": "The image digest",
+ "description": "Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.",
+ "type": "string"
+ },
+ "helm-values.image.name": {
+ "default": "disco-agent",
+ "description": "The image name for the Discovery Agent.\nThis is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.",
"type": "string"
},
"helm-values.image.pullPolicy": {
@@ -197,13 +230,23 @@
"description": "This sets the pull policy for images.",
"type": "string"
},
+ "helm-values.image.registry": {
+ "description": "Deprecated: per-component registry prefix.\n\nIf set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from\n`imageRegistry` + `imageNamespace` + `image.name`.\n\nThis can produce \"double registry\" style references such as\n`legacy.example.io/quay.io/jetstack/...`. Prefer using the global\n`imageRegistry`/`imageNamespace` values.",
+ "type": "string"
+ },
"helm-values.image.repository": {
"default": "",
+ "description": "Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`).\nExample: quay.io/jetstack/disco-agent",
"type": "string"
},
"helm-values.image.tag": {
"default": "",
- "description": "Overrides the image tag whose default is the chart appVersion.",
+ "description": "Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.",
+ "type": "string"
+ },
+ "helm-values.imageNamespace": {
+ "default": "jetstack",
+ "description": "The repository namespace used for disco-agent images by default.\nExamples:\n- jetstack\n- custom-namespace",
"type": "string"
},
"helm-values.imagePullSecrets": {
@@ -212,6 +255,11 @@
"items": {},
"type": "array"
},
+ "helm-values.imageRegistry": {
+ "default": "quay.io",
+ "description": "The container registry used for disco-agent images by default. This can include path prefixes (e.g. \"artifactory.example.com/docker\").",
+ "type": "string"
+ },
"helm-values.metrics": {
"additionalProperties": false,
"properties": {
diff --git a/deploy/charts/disco-agent/values.yaml b/deploy/charts/disco-agent/values.yaml
index be8c71b8..7f362328 100644
--- a/deploy/charts/disco-agent/values.yaml
+++ b/deploy/charts/disco-agent/values.yaml
@@ -5,14 +5,56 @@
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 1
+# Must be set to indicate that you have read and accepted the CyberArk Terms of Service. If false, the helm chart will fail to install and will print a message with instructions on how to accept the TOS.
+acceptTerms: false
+
+# The container registry used for disco-agent images by default.
+# This can include path prefixes (e.g. "artifactory.example.com/docker").
+# +docs:property
+imageRegistry: "quay.io"
+
+# The repository namespace used for disco-agent images by default.
+# Examples:
+# - jetstack
+# - custom-namespace
+# +docs:property
+imageNamespace: "jetstack"
+
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image:
+ # Deprecated: per-component registry prefix.
+ #
+ # If set, this value is *prepended* to the image repository that the chart would otherwise render.
+ # This applies both when `image.repository` is set and when the repository is computed from
+ # `imageRegistry` + `imageNamespace` + `image.name`.
+ #
+ # This can produce "double registry" style references such as
+ # `legacy.example.io/quay.io/jetstack/...`. Prefer using the global
+ # `imageRegistry`/`imageNamespace` values.
+ # +docs:property
+ # registry: quay.io
+
+ # Full repository override (takes precedence over `imageRegistry`, `imageNamespace`,
+ # and `image.name`).
+ # Example: quay.io/jetstack/disco-agent
+ # +docs:property
repository: ""
+
+ # The image name for the Discovery Agent.
+ # This is used (together with `imageRegistry` and `imageNamespace`) to construct the full
+ # image reference.
+ # +docs:property
+ name: disco-agent
+
# This sets the pull policy for images.
pullPolicy: IfNotPresent
- # Overrides the image tag whose default is the chart appVersion.
+
+ # Override the image tag to deploy by setting this variable.
+ # If no value is set, the chart's appVersion is used.
tag: ""
- # The image digest
+
+ # Override the image digest to deploy by setting this variable.
+ # If set together with `image.tag`, the rendered image will include both tag and digest.
digest: ""
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
@@ -154,6 +196,12 @@ config:
# be communicated to the people responsible for the affected secrets.
clusterDescription: ""
+ # Enable sending of Secret values to CyberArk in addition to metadata.
+ # Metadata is always sent, but the actual values of Secrets are not sent by default.
+ # When enabled, Secret data is encrypted using envelope encryption using
+ # a key managed by CyberArk, fetched from the Discovery and Context service.
+ sendSecretValues: true
+
authentication:
secretName: agent-credentials
diff --git a/deploy/charts/discovery-agent/.helmignore b/deploy/charts/discovery-agent/.helmignore
new file mode 100644
index 00000000..0e8a0eb3
--- /dev/null
+++ b/deploy/charts/discovery-agent/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/deploy/charts/discovery-agent/Chart.yaml b/deploy/charts/discovery-agent/Chart.yaml
new file mode 100644
index 00000000..376b8ec8
--- /dev/null
+++ b/deploy/charts/discovery-agent/Chart.yaml
@@ -0,0 +1,17 @@
+apiVersion: v2
+name: discovery-agent
+description: |-
+ The discovery-agent connects your Kubernetes or Openshift cluster to NGTS for discovery and monitoring.
+
+maintainers:
+ - name: Palo Alto Networks
+ url: https://www.paloaltonetworks.com
+
+sources:
+ - https://github.com/jetstack/jetstack-secure
+
+# These versions are meant to be overridden by `make helm-chart`. No `v` prefix
+# for the `version` because Helm doesn't support auto-determining the latest
+# version for OCI Helm charts that use a `v` prefix.
+version: 0.0.0
+appVersion: "v0.0.0"
diff --git a/deploy/charts/discovery-agent/README.md b/deploy/charts/discovery-agent/README.md
new file mode 100644
index 00000000..111e8409
--- /dev/null
+++ b/deploy/charts/discovery-agent/README.md
@@ -0,0 +1,427 @@
+# discovery-agent
+
+The Discovery Agent connects your Kubernetes or OpenShift cluster to Palo Alto NGTS.
+
+## Values
+
+
+
+### Venafi Connection
+
+#### **venafiConnection.include** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+When set to false, the rendered output does not contain the VenafiConnection CRDs and RBAC. This is useful for when the Venafi Connection resoures are already installed separately.
+#### **venafiConnection.serviceAccountNamespace** ~ `string`
+
+The namespace in which the 'venafi-connection' service account lives. This is the service account that is used to create JWT tokens for SAs or read credential secrets. (defaults to the namespace in which the controller is running)
+
+### Discovery Agent
+
+#### **config.tsgID** ~ `number,string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+The TSG (Tenant Service Group) ID to use when connecting to SCM. The production SCM server URL is derived from this value. Required unless config.serverURL is set. Mutually exclusive with config.serverURL. Must not be set when config.venafiConnection.enabled is true (the TSG ID is taken from the VenafiConnection's `spec.ngts` instead).
+
+
+#### **config.clusterName** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+Required: A human readable name for the cluster into which the agent is being deployed.
+
+This cluster name will be associated with the data that the agent uploads to the backend.
+
+#### **config.clusterDescription** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+A short description of the cluster where the agent is deployed (optional).
+
+This description will be associated with the data that the agent uploads to the backend.
+
+#### **config.claimableCerts** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+Whether discovered certs can be claimed by other tenants (optional). true = certs are left unassigned, available for any tenant to claim. false (default) = certs are owned by this cluster's tenant.
+#### **config.period** ~ `string`
+> Default value:
+> ```yaml
+> 0h1m0s
+> ```
+
+How often to push data to the remote server
+
+#### **config.excludeAnnotationKeysRegex** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+
+You can configure the agent to exclude some annotations or labels from being pushed. All Kubernetes objects are affected. The objects are still pushed, but the specified annotations and labels are removed before being pushed.
+
+Dots is the only character that needs to be escaped in the regex. Use either double quotes with escaped single quotes or unquoted strings for the regex to avoid YAML parsing issues with `\.`.
+
+Example: excludeAnnotationKeysRegex: ['^kapp\.k14s\.io/original.*']
+#### **config.excludeLabelKeysRegex** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+#### **config.clientID** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+Deprecated: Client ID for the configured service account. The client ID should be provided in the "clientID" field of the authentication secret (see config.secretName). This field is provided for compatibility for users migrating from the "venafi-kubernetes-agent" chart. Must not be set when config.venafiConnection.enabled is true.
+
+#### **config.secretName** ~ `string`
+> Default value:
+> ```yaml
+> discovery-agent-credentials
+> ```
+
+The name of the Secret containing the NGTS built-in service account credentials.
+The Secret must contain the following key:
+- privatekey.pem: PEM-encoded private key for the service account
+The Secret should also contain the following key:
+- clientID: Service account client ID (config.clientID must be set if not present)
+Must not be set when config.venafiConnection.enabled is true (the credentials Secret is not mounted in that mode).
+
+#### **config.venafiConnection.enabled** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+When set to true, config.tsgID, config.serverURL, config.clientID and config.clientId must not be set (the chart will fail to render otherwise), and the Secret named by config.secretName will _not_ be mounted into the Discovery Agent Pod.
+#### **config.venafiConnection.name** ~ `string`
+> Default value:
+> ```yaml
+> venafi-components
+> ```
+
+The name of a VenafiConnection resource which contains the configuration for authenticating to the upload backend.
+#### **config.venafiConnection.namespace** ~ `string`
+> Default value:
+> ```yaml
+> venafi
+> ```
+
+The namespace of a VenafiConnection resource which contains the configuration for authenticating to the upload backend.
+#### **replicaCount** ~ `number`
+> Default value:
+> ```yaml
+> 1
+> ```
+
+This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
+#### **imageRegistry** ~ `string`
+> Default value:
+> ```yaml
+> quay.io
+> ```
+
+The container registry used for discovery-agent images by default. This can include path prefixes (e.g. "artifactory.example.com/docker").
+
+#### **imageNamespace** ~ `string`
+> Default value:
+> ```yaml
+> jetstack
+> ```
+
+The repository namespace used for discovery-agent images by default.
+Examples:
+- jetstack
+- custom-namespace
+
+#### **image.repository** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`).
+Example: quay.io/jetstack/discovery-agent
+
+#### **image.name** ~ `string`
+> Default value:
+> ```yaml
+> discovery-agent
+> ```
+
+The image name for the Discovery Agent.
+This is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.
+
+#### **image.pullPolicy** ~ `string`
+> Default value:
+> ```yaml
+> IfNotPresent
+> ```
+
+This sets the pull policy for images.
+#### **image.tag** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.
+#### **image.digest** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.
+#### **imagePullSecrets** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+
+This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+#### **nameOverride** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+This is to override the chart name.
+#### **fullnameOverride** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+#### **serviceAccount.create** ~ `bool`
+> Default value:
+> ```yaml
+> true
+> ```
+
+Specifies whether a service account should be created
+#### **serviceAccount.automount** ~ `bool`
+> Default value:
+> ```yaml
+> true
+> ```
+
+Automatically mount a ServiceAccount's API credentials?
+#### **serviceAccount.annotations** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+Annotations to add to the service account
+#### **serviceAccount.name** ~ `string`
+> Default value:
+> ```yaml
+> ""
+> ```
+
+The name of the service account to use.
+If not set and create is true, a name is generated using the fullname template
+#### **podAnnotations** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+This is for setting Kubernetes Annotations to a Pod. For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+#### **podLabels** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+This is for setting Kubernetes Labels to a Pod.
+For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+#### **podSecurityContext** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+#### **securityContext** ~ `object`
+> Default value:
+> ```yaml
+> allowPrivilegeEscalation: false
+> capabilities:
+> drop:
+> - ALL
+> readOnlyRootFilesystem: true
+> runAsNonRoot: true
+> seccompProfile:
+> type: RuntimeDefault
+> ```
+
+Add Container specific SecurityContext settings to the container. Takes precedence over `podSecurityContext` when set. See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container
+
+#### **resources** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+#### **volumes** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+
+Additional volumes on the output Deployment definition.
+#### **volumeMounts** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+
+Additional volumeMounts on the output Deployment definition.
+#### **nodeSelector** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+#### **tolerations** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+#### **affinity** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+#### **http_proxy** ~ `string`
+
+Configures the HTTP_PROXY environment variable where a HTTP proxy is required.
+
+#### **https_proxy** ~ `string`
+
+Configures the HTTPS_PROXY environment variable where a HTTP proxy is required.
+
+#### **no_proxy** ~ `string`
+
+Configures the NO_PROXY environment variable where a HTTP proxy is required, but certain domains should be excluded.
+
+#### **podDisruptionBudget** ~ `object`
+> Default value:
+> ```yaml
+> enabled: false
+> ```
+
+Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple replicas, consider setting podDisruptionBudget.enabled to true.
+
+#### **extraArgs** ~ `array`
+> Default value:
+> ```yaml
+> []
+> ```
+
+```yaml
+extraArgs:
+- --logging-format=json
+- --log-level=6 # To enable HTTP request logging
+```
+#### **pprof.enabled** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+Enable profiling with the pprof endpoint
+#### **metrics.enabled** ~ `bool`
+> Default value:
+> ```yaml
+> true
+> ```
+
+Enable the metrics server.
+If false, the metrics server will be disabled and the other metrics fields below will be ignored.
+#### **metrics.podmonitor.enabled** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator. See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor
+#### **metrics.podmonitor.namespace** ~ `string`
+
+The namespace that the pod monitor should live in.
+Defaults to the discovery-agent namespace.
+
+#### **metrics.podmonitor.prometheusInstance** ~ `string`
+> Default value:
+> ```yaml
+> default
+> ```
+
+Specifies the `prometheus` label on the created PodMonitor. This is used when different Prometheus instances have label selectors matching different PodMonitors.
+#### **metrics.podmonitor.interval** ~ `string`
+> Default value:
+> ```yaml
+> 60s
+> ```
+
+The interval to scrape metrics.
+#### **metrics.podmonitor.scrapeTimeout** ~ `string`
+> Default value:
+> ```yaml
+> 30s
+> ```
+
+The timeout before a metrics scrape fails.
+#### **metrics.podmonitor.labels** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+Additional labels to add to the PodMonitor.
+#### **metrics.podmonitor.annotations** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+Additional annotations to add to the PodMonitor.
+#### **metrics.podmonitor.honorLabels** ~ `bool`
+> Default value:
+> ```yaml
+> false
+> ```
+
+Keep labels from scraped data, overriding server-side labels.
+#### **metrics.podmonitor.endpointAdditionalProperties** ~ `object`
+> Default value:
+> ```yaml
+> {}
+> ```
+
+EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc.
+
+For example:
+
+```yaml
+endpointAdditionalProperties:
+ relabelings:
+ - action: replace
+ sourceLabels:
+ - __meta_kubernetes_pod_node_name
+ targetLabel: instance
+```
+
+
diff --git a/deploy/charts/discovery-agent/crd_bases/crd.footer.yaml b/deploy/charts/discovery-agent/crd_bases/crd.footer.yaml
new file mode 100644
index 00000000..e7b91194
--- /dev/null
+++ b/deploy/charts/discovery-agent/crd_bases/crd.footer.yaml
@@ -0,0 +1,2 @@
+{{ end }}
+{{ end }}
diff --git a/deploy/charts/discovery-agent/crd_bases/crd.header-without-validations.yaml b/deploy/charts/discovery-agent/crd_bases/crd.header-without-validations.yaml
new file mode 100644
index 00000000..ce1d8a36
--- /dev/null
+++ b/deploy/charts/discovery-agent/crd_bases/crd.header-without-validations.yaml
@@ -0,0 +1,13 @@
+{{/* DO NOT EDIT. Use 'make generate-crds-venconn' to regenerate. */}}
+{{- if .Values.venafiConnection.include }}
+{{- if (semverCompare "<1.25" .Capabilities.KubeVersion.GitVersion) }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: "venaficonnections.jetstack.io"
+ annotations:
+ # This annotation prevents the CRD from being pruned by Helm when this chart
+ # is deleted.
+ helm.sh/resource-policy: keep
+ labels:
+ {{- include "venafi-connection.labels" . | nindent 4 }}
diff --git a/deploy/charts/discovery-agent/crd_bases/crd.header.yaml b/deploy/charts/discovery-agent/crd_bases/crd.header.yaml
new file mode 100644
index 00000000..22207680
--- /dev/null
+++ b/deploy/charts/discovery-agent/crd_bases/crd.header.yaml
@@ -0,0 +1,13 @@
+{{/* DO NOT EDIT. Use 'make generate-crds-venconn' to regenerate. */}}
+{{- if .Values.venafiConnection.include }}
+{{- if not (semverCompare "<1.25" .Capabilities.KubeVersion.GitVersion) }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: "venaficonnections.jetstack.io"
+ annotations:
+ # This annotation prevents the CRD from being pruned by Helm when this chart
+ # is deleted.
+ helm.sh/resource-policy: keep
+ labels:
+ {{- include "venafi-connection.labels" . | nindent 4 }}
diff --git a/deploy/charts/discovery-agent/crd_bases/jetstack.io_venaficonnections.yaml b/deploy/charts/discovery-agent/crd_bases/jetstack.io_venaficonnections.yaml
new file mode 100644
index 00000000..9389601d
--- /dev/null
+++ b/deploy/charts/discovery-agent/crd_bases/jetstack.io_venaficonnections.yaml
@@ -0,0 +1,1944 @@
+# DO NOT EDIT: Use 'make generate-crds-venconn' to regenerate.
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.21.0
+ name: venaficonnections.jetstack.io
+spec:
+ group: jetstack.io
+ names:
+ kind: VenafiConnection
+ listKind: VenafiConnectionList
+ plural: venaficonnections
+ shortNames:
+ - vc
+ singular: venaficonnection
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: VenafiConnection is the Schema for the VenafiConnection API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ allowReferencesFrom:
+ description: |-
+ A namespace selector that specifies what namespaces this VenafiConnection
+ is allowed to be used from.
+ If not set/ null, the VenafiConnection can only be used within its namespace.
+ An empty selector ({}) matches all namespaces.
+ If set to a non-empty selector, the VenafiConnection can only be used from
+ namespaces that match the selector. This possibly excludes the namespace
+ the VenafiConnection is in.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ distributedIssuer:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be
+ used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tsgID url] must be set
+ rule: '[has(self.tsgID),has(self.url)].filter(x,x==true).size()
+ == 1'
+ tpp:
+ properties:
+ accessToken:
+ description: The list of steps to retrieve a TPP access token.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out by
+ venafi-connection-lib.
+ type: string
+ required:
+ - accessToken
+ - url
+ type: object
+ vaas:
+ description: 'Deprecated: The ''vaas'' field is deprecated use the
+ field called ''vcp'' instead.'
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [apiKey accessToken] must
+ be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size()
+ == 1'
+ vcp:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [apiKey accessToken] must
+ be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size()
+ == 1'
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tpp ngts vcp vaas distributedIssuer
+ firefly] must be set
+ rule: '[has(self.tpp),has(self.ngts),has(self.vcp),has(self.vaas),has(self.distributedIssuer),has(self.firefly)].filter(x,x==true).size()
+ == 1'
+ status:
+ properties:
+ conditions:
+ description: List of status conditions to indicate the status of a
+ VenafiConnection.
+ items:
+ description: ConnectionCondition contains condition information
+ for a VenafiConnection.
+ properties:
+ lastTransitionTime:
+ description: |-
+ LastTransitionTime is the timestamp corresponding to the last status
+ change of this condition.
+ format: date-time
+ type: string
+ lastUpdateTime:
+ description: lastUpdateTime is the time of the last update to
+ this condition
+ format: date-time
+ type: string
+ message:
+ description: |-
+ Message is a human readable description of the details of the last
+ transition, complementing reason.
+ type: string
+ observedGeneration:
+ description: |-
+ If set, this represents the .metadata.generation that the condition was
+ set based upon.
+ For instance, if .metadata.generation is currently 12, but the
+ .status.condition[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the Issuer.
+ format: int64
+ type: integer
+ reason:
+ description: |-
+ Reason is a brief machine readable explanation for the condition's last
+ transition.
+ type: string
+ status:
+ description: Status of the condition, one of (`True`, `False`,
+ `Unknown`).
+ type: string
+ tokenValidUntil:
+ description: |-
+ The ValidUntil time of the token used to authenticate with the
+ Certificate Manager, SaaS.
+ format: date-time
+ type: string
+ type:
+ description: |-
+ Type of the condition, should be a combination of the unique name of the
+ operator and the type of condition.
+ eg. `VenafiEnhancedIssuerReady`
+ type: string
+ required:
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - metadata
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/deploy/charts/discovery-agent/templates/NOTES.txt b/deploy/charts/discovery-agent/templates/NOTES.txt
new file mode 100644
index 00000000..92cce232
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/NOTES.txt
@@ -0,0 +1,9 @@
+CHART NAME: {{ .Chart.Name }}
+CHART VERSION: {{ .Chart.Version }}
+APP VERSION: {{ .Chart.AppVersion }}
+
+- Check the application is running:
+> kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
+
+- Check the application logs for successful connection to NGTS:
+> kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
diff --git a/deploy/charts/discovery-agent/templates/_helpers.tpl b/deploy/charts/discovery-agent/templates/_helpers.tpl
new file mode 100644
index 00000000..df3112a7
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/_helpers.tpl
@@ -0,0 +1,125 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "discovery-agent.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 "discovery-agent.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 }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "discovery-agent.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "discovery-agent.labels" -}}
+helm.sh/chart: {{ include "discovery-agent.chart" . }}
+{{ include "discovery-agent.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "discovery-agent.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "discovery-agent.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "discovery-agent.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "discovery-agent.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Util function for generating an image reference based on the provided options.
+This function is derived from similar functions used in the cert-manager GitHub organization
+*/}}
+{{- define "discovery-agent.image" -}}
+{{- /*
+Calling convention:
+- (tuple )
+We intentionally pass imageRegistry/imageNamespace as explicit arguments rather than reading
+from `.Values` inside this helper, because `helm-tool lint` does not reliably track `.Values.*`
+usage through tuple/variable indirection.
+*/ -}}
+{{- if ne (len .) 4 -}}
+ {{- fail (printf "ERROR: template \"discovery-agent.image\" expects (tuple ), got %d arguments" (len .)) -}}
+{{- end -}}
+{{- $image := index . 0 -}}
+{{- $imageRegistry := index . 1 | default "" -}}
+{{- $imageNamespace := index . 2 | default "" -}}
+{{- $defaultReference := index . 3 -}}
+{{- $repository := "" -}}
+{{- if $image.repository -}}
+ {{- $repository = $image.repository -}}
+{{- else -}}
+ {{- $name := required "ERROR: image.name must be set when image.repository is empty" $image.name -}}
+ {{- $repository = $name -}}
+ {{- if $imageNamespace -}}
+ {{- $repository = printf "%s/%s" $imageNamespace $repository -}}
+ {{- end -}}
+ {{- if $imageRegistry -}}
+ {{- $repository = printf "%s/%s" $imageRegistry $repository -}}
+ {{- end -}}
+{{- end -}}
+{{- $repository -}}
+{{- if and $image.tag $image.digest -}}
+ {{- printf ":%s@%s" $image.tag $image.digest -}}
+{{- else if $image.tag -}}
+ {{- printf ":%s" $image.tag -}}
+{{- else if $image.digest -}}
+ {{- printf "@%s" $image.digest -}}
+{{- else -}}
+ {{- printf "%s" $defaultReference -}}
+{{- end -}}
+{{- end }}
+
+{{/*
+Because of Helm bug (https://github.com/helm/helm/issues/3001), Helm converts
+int value to float64 implictly, like 2748336 becomes 2.748336e+06.
+This breaks the output even when using quote to render.
+
+Use this function when you want to get the string value only.
+It handles the case when the value is string itself as well.
+Parameters: is string/number
+
+Usage: {{ include "discovery-agent.stringOrNumber" .Values.config.tsgID }}
+*/}}
+{{- define "discovery-agent.stringOrNumber" -}}
+{{- if kindIs "string" . }}
+ {{- print . -}}
+{{- else }}
+ {{- int64 . | toString -}}
+{{- end -}}
+{{- end -}}
diff --git a/deploy/charts/discovery-agent/templates/_venafi-connection.tpl b/deploy/charts/discovery-agent/templates/_venafi-connection.tpl
new file mode 100644
index 00000000..07e7fe1c
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/_venafi-connection.tpl
@@ -0,0 +1,26 @@
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "venafi-connection.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "venafi-connection.labels" -}}
+helm.sh/chart: {{ include "venafi-connection.chart" . }}
+{{ include "venafi-connection.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "venafi-connection.selectorLabels" -}}
+app.kubernetes.io/name: "venafi-connection"
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/templates/configmap.yaml b/deploy/charts/discovery-agent/templates/configmap.yaml
new file mode 100644
index 00000000..648a77d2
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/configmap.yaml
@@ -0,0 +1,273 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-config
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+data:
+ config.yaml: |-
+ cluster_name: {{ required "config.clusterName is required" .Values.config.clusterName | quote }}
+ cluster_description: {{ .Values.config.clusterDescription | quote }}
+ {{- if .Values.config.claimableCerts }}
+ claimable_certs: true
+ {{- end }}
+ period: {{ .Values.config.period | quote }}
+ {{- with .Values.config.excludeAnnotationKeysRegex }}
+ exclude-annotation-keys-regex:
+ {{- . | toYaml | nindent 6 }}
+ {{- end }}
+ {{- with .Values.config.excludeLabelKeysRegex }}
+ exclude-label-keys-regex:
+ {{- . | toYaml | nindent 6 }}
+ {{- end }}
+ data-gatherers:
+ - kind: k8s-discovery
+ name: k8s-discovery
+ - kind: k8s-dynamic
+ name: k8s/secrets
+ config:
+ resource-type:
+ version: v1
+ resource: secrets
+ field-selectors:
+ - type!=kubernetes.io/dockercfg
+ - type!=kubernetes.io/dockerconfigjson
+ - type!=bootstrap.kubernetes.io/token
+ - type!=helm.sh/release.v1
+ - kind: k8s-dynamic
+ name: k8s/jobs
+ config:
+ resource-type:
+ version: v1
+ group: batch
+ resource: jobs
+ - kind: k8s-dynamic
+ name: k8s/cronjobs
+ config:
+ resource-type:
+ version: v1
+ group: batch
+ resource: cronjobs
+ - kind: k8s-dynamic
+ name: k8s/deployments
+ config:
+ resource-type:
+ version: v1
+ group: apps
+ resource: deployments
+ - kind: k8s-dynamic
+ name: k8s/statefulsets
+ config:
+ resource-type:
+ version: v1
+ group: apps
+ resource: statefulsets
+ - kind: k8s-dynamic
+ name: k8s/daemonsets
+ config:
+ resource-type:
+ version: v1
+ group: apps
+ resource: daemonsets
+ - kind: k8s-dynamic
+ name: k8s/pods
+ config:
+ resource-type:
+ version: v1
+ resource: pods
+ - kind: "k8s-dynamic"
+ name: "k8s/namespaces"
+ config:
+ resource-type:
+ resource: namespaces
+ version: v1
+ # gather services for pod readiness probe rules
+ - kind: "k8s-dynamic"
+ name: "k8s/services"
+ config:
+ resource-type:
+ resource: services
+ version: v1
+ - kind: "k8s-dynamic"
+ name: "k8s/ingresses"
+ config:
+ resource-type:
+ group: networking.k8s.io
+ version: v1
+ resource: ingresses
+ - kind: "k8s-dynamic"
+ name: "k8s/certificates"
+ config:
+ resource-type:
+ group: cert-manager.io
+ version: v1
+ resource: certificates
+ - kind: "k8s-dynamic"
+ name: "k8s/certificaterequests"
+ config:
+ resource-type:
+ group: cert-manager.io
+ version: v1
+ resource: certificaterequests
+ - kind: "k8s-dynamic"
+ name: "k8s/issuers"
+ config:
+ resource-type:
+ group: cert-manager.io
+ version: v1
+ resource: issuers
+ - kind: "k8s-dynamic"
+ name: "k8s/clusterissuers"
+ config:
+ resource-type:
+ group: cert-manager.io
+ version: v1
+ resource: clusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/googlecasissuers"
+ config:
+ resource-type:
+ group: cas-issuer.jetstack.io
+ version: v1beta1
+ resource: googlecasissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/googlecasclusterissuers"
+ config:
+ resource-type:
+ group: cas-issuer.jetstack.io
+ version: v1beta1
+ resource: googlecasclusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/awspcaissuer"
+ config:
+ resource-type:
+ group: awspca.cert-manager.io
+ version: v1beta1
+ resource: awspcaissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/awspcaclusterissuers"
+ config:
+ resource-type:
+ group: awspca.cert-manager.io
+ version: v1beta1
+ resource: awspcaclusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/mutatingwebhookconfigurations"
+ config:
+ resource-type:
+ group: admissionregistration.k8s.io
+ version: v1
+ resource: mutatingwebhookconfigurations
+ - kind: "k8s-dynamic"
+ name: "k8s/validatingwebhookconfigurations"
+ config:
+ resource-type:
+ group: admissionregistration.k8s.io
+ version: v1
+ resource: validatingwebhookconfigurations
+ - kind: "k8s-dynamic"
+ name: "k8s/gateways"
+ config:
+ resource-type:
+ group: networking.istio.io
+ version: v1alpha3
+ resource: gateways
+ - kind: "k8s-dynamic"
+ name: "k8s/virtualservices"
+ config:
+ resource-type:
+ group: networking.istio.io
+ version: v1alpha3
+ resource: virtualservices
+ - kind: "k8s-dynamic"
+ name: "k8s/routes"
+ config:
+ resource-type:
+ version: v1
+ group: route.openshift.io
+ resource: routes
+ - kind: "k8s-dynamic"
+ name: "k8s/venaficonnections"
+ config:
+ resource-type:
+ group: jetstack.io
+ version: v1alpha1
+ resource: venaficonnections
+ - kind: "k8s-dynamic"
+ name: "k8s/venaficlusterissuers"
+ config:
+ resource-type:
+ group: jetstack.io
+ version: v1alpha1
+ resource: venaficlusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/venafiissuers"
+ config:
+ resource-type:
+ group: jetstack.io
+ version: v1alpha1
+ resource: venafiissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/fireflyissuers"
+ config:
+ resource-type:
+ group: firefly.venafi.com
+ version: v1
+ resource: issuers
+ - kind: "k8s-dynamic"
+ name: "k8s/stepissuers"
+ config:
+ resource-type:
+ group: certmanager.step.sm
+ version: v1beta1
+ resource: stepissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/stepclusterissuers"
+ config:
+ resource-type:
+ group: certmanager.step.sm
+ version: v1beta1
+ resource: stepclusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/originissuers"
+ config:
+ resource-type:
+ group: cert-manager.k8s.cloudflare.com
+ version: v1
+ resource: originissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/clusteroriginissuers"
+ config:
+ resource-type:
+ group: cert-manager.k8s.cloudflare.com
+ version: v1
+ resource: clusteroriginissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/freeipaissuers"
+ config:
+ resource-type:
+ group: certmanager.freeipa.org
+ version: v1beta1
+ resource: issuers
+ - kind: "k8s-dynamic"
+ name: "k8s/freeipaclusterissuers"
+ config:
+ resource-type:
+ group: certmanager.freeipa.org
+ version: v1beta1
+ resource: clusterissuers
+ - kind: "k8s-dynamic"
+ name: "k8s/ejbcaissuers"
+ config:
+ resource-type:
+ group: ejbca-issuer.keyfactor.com
+ version: v1alpha1
+ resource: issuers
+ - kind: "k8s-dynamic"
+ name: "k8s/ejbcaclusterissuers"
+ config:
+ resource-type:
+ group: ejbca-issuer.keyfactor.com
+ version: v1alpha1
+ resource: clusterissuers
diff --git a/deploy/charts/discovery-agent/templates/deployment.yaml b/deploy/charts/discovery-agent/templates/deployment.yaml
new file mode 100644
index 00000000..18ec6c51
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/deployment.yaml
@@ -0,0 +1,170 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ {{- include "discovery-agent.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ {{- with .Values.podAnnotations }}
+ annotations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "discovery-agent.serviceAccountName" . }}
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: agent
+ {{- with .Values.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ image: "{{ template "discovery-agent.image" (tuple .Values.image .Values.imageRegistry .Values.imageNamespace (printf ":%s" .Chart.AppVersion)) }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ env:
+ - name: POD_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: POD_UID
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.uid
+ - name: POD_NODE
+ valueFrom:
+ fieldRef:
+ fieldPath: spec.nodeName
+ {{- with .Values.http_proxy }}
+ - name: HTTP_PROXY
+ value: {{ . }}
+ {{- end }}
+ {{- with .Values.https_proxy }}
+ - name: HTTPS_PROXY
+ value: {{ . }}
+ {{- end }}
+ {{- with .Values.no_proxy }}
+ - name: NO_PROXY
+ value: {{ . }}
+ {{- end }}
+ args:
+ - "agent"
+ - "-c"
+ - "/etc/discovery-agent/config.yaml"
+ {{- if .Values.config.venafiConnection.enabled }}
+ {{- if .Values.config.tsgID }}
+ {{- fail "config.tsgID must not be set when config.venafiConnection.enabled is true; the TSG ID is read from the VenafiConnection's spec.ngts" }}
+ {{- end }}
+ {{- if .Values.config.serverURL }}
+ {{- fail "config.serverURL must not be set when config.venafiConnection.enabled is true; the server URL is read from the VenafiConnection's spec" }}
+ {{- end }}
+ {{- if .Values.config.clientID }}
+ {{- fail "config.clientID must not be set when config.venafiConnection.enabled is true; authentication is performed via the VenafiConnection resource" }}
+ {{- end }}
+ {{- if .Values.config.clientId }}
+ {{- fail "config.clientId must not be set when config.venafiConnection.enabled is true; authentication is performed via the VenafiConnection resource" }}
+ {{- end }}
+ {{- if ne .Values.config.secretName "discovery-agent-credentials" }}
+ {{- fail "config.secretName must not be set when config.venafiConnection.enabled is true; the credentials Secret is not mounted in this mode (authentication is performed via the VenafiConnection resource)" }}
+ {{- end }}
+ - --venafi-connection
+ - {{ .Values.config.venafiConnection.name | quote }}
+ - --venafi-connection-namespace
+ - {{ .Values.config.venafiConnection.namespace | quote }}
+ {{- with .Values.venafiConnection.serviceAccountNamespace }}
+ - --install-namespace
+ - {{ . | quote }}
+ {{- end }}
+ {{- else }}
+ - --ngts
+ {{- if and .Values.config.tsgID .Values.config.serverURL }}
+ {{- fail "config.tsgID and config.serverURL are mutually exclusive; set exactly one" }}
+ {{- else if .Values.config.serverURL }}
+ - --ngts-server-url
+ - {{ .Values.config.serverURL | quote }}
+ {{- else }}
+ - --tsg-id
+ - {{ required "config.tsgID is required when config.serverURL is not set" .Values.config.tsgID | include "discovery-agent.stringOrNumber" | quote }}
+ {{- end }}
+ {{- if or .Values.config.clientID .Values.config.clientId }}
+ - --client-id
+ - {{ .Values.config.clientID | default .Values.config.clientId }}
+ {{- end }}
+ - --private-key-path
+ - /etc/discovery-agent/credentials/privatekey.pem
+ {{- end }}
+ - --logging-format=json
+ {{- if .Values.metrics.enabled }}
+ - --enable-metrics
+ {{- end }}
+ {{- if .Values.pprof.enabled }}
+ - --enable-pprof
+ {{- end }}
+ {{- range .Values.extraArgs }}
+ - {{ . | quote }}
+ {{- end }}
+ {{- with .Values.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ volumeMounts:
+ - name: config
+ mountPath: "/etc/discovery-agent"
+ readOnly: true
+ {{- if not .Values.config.venafiConnection.enabled }}
+ - name: credentials
+ mountPath: "/etc/discovery-agent/credentials"
+ readOnly: true
+ {{- end }}
+ {{- with .Values.volumeMounts }}
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ ports:
+ - name: agent-api
+ containerPort: 8081
+ volumes:
+ - name: config
+ configMap:
+ name: {{ include "discovery-agent.fullname" . }}-config
+ optional: false
+ {{- if not .Values.config.venafiConnection.enabled }}
+ - name: credentials
+ secret:
+ secretName: {{ .Values.config.secretName }}
+ optional: false
+ {{- end }}
+ {{- with .Values.volumes }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
diff --git a/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml b/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml
new file mode 100644
index 00000000..0ccdc945
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/poddisruptionbudget.yaml
@@ -0,0 +1,23 @@
+{{- if .Values.podDisruptionBudget.enabled }}
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+spec:
+ selector:
+ matchLabels:
+ {{- include "discovery-agent.selectorLabels" . | nindent 6 }}
+
+ {{- if not (or (hasKey .Values.podDisruptionBudget "minAvailable") (hasKey .Values.podDisruptionBudget "maxUnavailable")) }}
+ minAvailable: 1 # Default value because minAvailable and maxUnavailable are not set
+ {{- end }}
+ {{- if hasKey .Values.podDisruptionBudget "minAvailable" }}
+ minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
+ {{- end }}
+ {{- if hasKey .Values.podDisruptionBudget "maxUnavailable" }}
+ maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/templates/podmonitor.yaml b/deploy/charts/discovery-agent/templates/podmonitor.yaml
new file mode 100644
index 00000000..d48da1fd
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/podmonitor.yaml
@@ -0,0 +1,40 @@
+{{- if and .Values.metrics.enabled .Values.metrics.podmonitor.enabled }}
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}
+{{- if .Values.metrics.podmonitor.namespace }}
+ namespace: {{ .Values.metrics.podmonitor.namespace }}
+{{- else }}
+ namespace: {{ .Release.Namespace | quote }}
+{{- end }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+ prometheus: {{ .Values.metrics.podmonitor.prometheusInstance }}
+ {{- with .Values.metrics.podmonitor.labels }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- with .Values.metrics.podmonitor.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+{{- end }}
+spec:
+ jobLabel: {{ include "discovery-agent.fullname" . }}
+ selector:
+ matchLabels:
+ {{- include "discovery-agent.selectorLabels" . | nindent 6 }}
+{{- if .Values.metrics.podmonitor.namespace }}
+ namespaceSelector:
+ matchNames:
+ - {{ .Release.Namespace | quote }}
+{{- end }}
+ podMetricsEndpoints:
+ - port: agent-api
+ path: /metrics
+ interval: {{ .Values.metrics.podmonitor.interval }}
+ scrapeTimeout: {{ .Values.metrics.podmonitor.scrapeTimeout }}
+ honorLabels: {{ .Values.metrics.podmonitor.honorLabels }}
+ {{- with .Values.metrics.podmonitor.endpointAdditionalProperties }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/templates/rbac.yaml b/deploy/charts/discovery-agent/templates/rbac.yaml
new file mode 100644
index 00000000..c6add95e
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/rbac.yaml
@@ -0,0 +1,150 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-event-emitted
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+rules:
+ - apiGroups: [""]
+ resources: ["events"]
+ verbs: ["create"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-event-emitted
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ include "discovery-agent.fullname" . }}-event-emitted
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-cluster-viewer
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: view
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-secret-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+rules:
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-secret-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: {{ include "discovery-agent.fullname" . }}-secret-reader
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-rbac-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+rules:
+ - apiGroups: ["rbac.authorization.k8s.io"]
+ resources:
+ - roles
+ - clusterroles
+ - rolebindings
+ - clusterrolebindings
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-rbac-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: {{ include "discovery-agent.fullname" . }}-rbac-reader
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-oidc-discovery
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: system:service-account-issuer-discovery
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-crd-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+rules:
+ - apiGroups:
+ - cert-manager.io
+ - cas-issuer.jetstack.io
+ - awspca.cert-manager.io
+ - jetstack.io
+ - firefly.venafi.com
+ - certmanager.step.sm
+ - cert-manager.k8s.cloudflare.com
+ - certmanager.freeipa.org
+ - ejbca-issuer.keyfactor.com
+ - networking.istio.io
+ - route.openshift.io
+ - admissionregistration.k8s.io
+ resources: ["*"]
+ verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-crd-reader
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ kind: ClusterRole
+ name: {{ include "discovery-agent.fullname" . }}-crd-reader
+ apiGroup: rbac.authorization.k8s.io
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
diff --git a/deploy/charts/discovery-agent/templates/serviceaccount.yaml b/deploy/charts/discovery-agent/templates/serviceaccount.yaml
new file mode 100644
index 00000000..9281d7fc
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/serviceaccount.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/templates/venafi-connection-crd.without-validations.yaml b/deploy/charts/discovery-agent/templates/venafi-connection-crd.without-validations.yaml
new file mode 100644
index 00000000..3f28e932
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/venafi-connection-crd.without-validations.yaml
@@ -0,0 +1,1788 @@
+{{/* DO NOT EDIT. Use 'make generate-crds-venconn' to regenerate. */}}
+{{- if .Values.venafiConnection.include }}
+{{- if (semverCompare "<1.25" .Capabilities.KubeVersion.GitVersion) }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: "venaficonnections.jetstack.io"
+ annotations:
+ # This annotation prevents the CRD from being pruned by Helm when this chart
+ # is deleted.
+ helm.sh/resource-policy: keep
+ labels:
+ {{- include "venafi-connection.labels" . | nindent 4 }}
+spec:
+ group: jetstack.io
+ names:
+ kind: VenafiConnection
+ listKind: VenafiConnectionList
+ plural: venaficonnections
+ shortNames:
+ - vc
+ singular: venaficonnection
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: VenafiConnection is the Schema for the VenafiConnection API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ allowReferencesFrom:
+ description: |-
+ A namespace selector that specifies what namespaces this VenafiConnection
+ is allowed to be used from.
+ If not set/ null, the VenafiConnection can only be used within its namespace.
+ An empty selector ({}) matches all namespaces.
+ If set to a non-empty selector, the VenafiConnection can only be used from
+ namespaces that match the selector. This possibly excludes the namespace
+ the VenafiConnection is in.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ distributedIssuer:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
+ tpp:
+ properties:
+ accessToken:
+ description: The list of steps to retrieve a TPP access token.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out by
+ venafi-connection-lib.
+ type: string
+ required:
+ - accessToken
+ - url
+ type: object
+ vaas:
+ description: 'Deprecated: The ''vaas'' field is deprecated use the field called ''vcp'' instead.'
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ vcp:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ type: object
+ status:
+ properties:
+ conditions:
+ description: List of status conditions to indicate the status of a VenafiConnection.
+ items:
+ description: ConnectionCondition contains condition information for a VenafiConnection.
+ properties:
+ lastTransitionTime:
+ description: |-
+ LastTransitionTime is the timestamp corresponding to the last status
+ change of this condition.
+ format: date-time
+ type: string
+ lastUpdateTime:
+ description: lastUpdateTime is the time of the last update to this condition
+ format: date-time
+ type: string
+ message:
+ description: |-
+ Message is a human readable description of the details of the last
+ transition, complementing reason.
+ type: string
+ observedGeneration:
+ description: |-
+ If set, this represents the .metadata.generation that the condition was
+ set based upon.
+ For instance, if .metadata.generation is currently 12, but the
+ .status.condition[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the Issuer.
+ format: int64
+ type: integer
+ reason:
+ description: |-
+ Reason is a brief machine readable explanation for the condition's last
+ transition.
+ type: string
+ status:
+ description: Status of the condition, one of (`True`, `False`, `Unknown`).
+ type: string
+ tokenValidUntil:
+ description: |-
+ The ValidUntil time of the token used to authenticate with the
+ Certificate Manager, SaaS.
+ format: date-time
+ type: string
+ type:
+ description: |-
+ Type of the condition, should be a combination of the unique name of the
+ operator and the type of condition.
+ eg. `VenafiEnhancedIssuerReady`
+ type: string
+ required:
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - metadata
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+{{ end }}
+{{ end }}
diff --git a/deploy/charts/discovery-agent/templates/venafi-connection-crd.yaml b/deploy/charts/discovery-agent/templates/venafi-connection-crd.yaml
new file mode 100644
index 00000000..9110a291
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/venafi-connection-crd.yaml
@@ -0,0 +1,1848 @@
+{{/* DO NOT EDIT. Use 'make generate-crds-venconn' to regenerate. */}}
+{{- if .Values.venafiConnection.include }}
+{{- if not (semverCompare "<1.25" .Capabilities.KubeVersion.GitVersion) }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: "venaficonnections.jetstack.io"
+ annotations:
+ # This annotation prevents the CRD from being pruned by Helm when this chart
+ # is deleted.
+ helm.sh/resource-policy: keep
+ labels:
+ {{- include "venafi-connection.labels" . | nindent 4 }}
+spec:
+ group: jetstack.io
+ names:
+ kind: VenafiConnection
+ listKind: VenafiConnectionList
+ plural: venaficonnections
+ shortNames:
+ - vc
+ singular: venaficonnection
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: VenafiConnection is the Schema for the VenafiConnection API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ allowReferencesFrom:
+ description: |-
+ A namespace selector that specifies what namespaces this VenafiConnection
+ is allowed to be used from.
+ If not set/ null, the VenafiConnection can only be used within its namespace.
+ An empty selector ({}) matches all namespaces.
+ If set to a non-empty selector, the VenafiConnection can only be used from
+ namespaces that match the selector. This possibly excludes the namespace
+ the VenafiConnection is in.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ distributedIssuer:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tsgID url] must be set
+ rule: '[has(self.tsgID),has(self.url)].filter(x,x==true).size() == 1'
+ tpp:
+ properties:
+ accessToken:
+ description: The list of steps to retrieve a TPP access token.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out by
+ venafi-connection-lib.
+ type: string
+ required:
+ - accessToken
+ - url
+ type: object
+ vaas:
+ description: 'Deprecated: The ''vaas'' field is deprecated use the field called ''vcp'' instead.'
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [apiKey accessToken] must be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size() == 1'
+ vcp:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ apiKey:
+ description: |-
+ The list of steps to retrieve the API key that will be used to connect to
+ Certificate Manager, SaaS.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, SaaS instance. If not set, the default
+ value https://api.venafi.cloud is used.
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [apiKey accessToken] must be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size() == 1'
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tpp ngts vcp vaas distributedIssuer firefly] must be set
+ rule: '[has(self.tpp),has(self.ngts),has(self.vcp),has(self.vaas),has(self.distributedIssuer),has(self.firefly)].filter(x,x==true).size() == 1'
+ status:
+ properties:
+ conditions:
+ description: List of status conditions to indicate the status of a VenafiConnection.
+ items:
+ description: ConnectionCondition contains condition information for a VenafiConnection.
+ properties:
+ lastTransitionTime:
+ description: |-
+ LastTransitionTime is the timestamp corresponding to the last status
+ change of this condition.
+ format: date-time
+ type: string
+ lastUpdateTime:
+ description: lastUpdateTime is the time of the last update to this condition
+ format: date-time
+ type: string
+ message:
+ description: |-
+ Message is a human readable description of the details of the last
+ transition, complementing reason.
+ type: string
+ observedGeneration:
+ description: |-
+ If set, this represents the .metadata.generation that the condition was
+ set based upon.
+ For instance, if .metadata.generation is currently 12, but the
+ .status.condition[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the Issuer.
+ format: int64
+ type: integer
+ reason:
+ description: |-
+ Reason is a brief machine readable explanation for the condition's last
+ transition.
+ type: string
+ status:
+ description: Status of the condition, one of (`True`, `False`, `Unknown`).
+ type: string
+ tokenValidUntil:
+ description: |-
+ The ValidUntil time of the token used to authenticate with the
+ Certificate Manager, SaaS.
+ format: date-time
+ type: string
+ type:
+ description: |-
+ Type of the condition, should be a combination of the unique name of the
+ operator and the type of condition.
+ eg. `VenafiEnhancedIssuerReady`
+ type: string
+ required:
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - metadata
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+{{ end }}
+{{ end }}
diff --git a/deploy/charts/discovery-agent/templates/venafi-connection-rbac.yaml b/deploy/charts/discovery-agent/templates/venafi-connection-rbac.yaml
new file mode 100644
index 00000000..5327bf37
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/venafi-connection-rbac.yaml
@@ -0,0 +1,47 @@
+{{- if .Values.venafiConnection.include }}
+{{- $saNamespace := .Values.venafiConnection.serviceAccountNamespace | default $.Release.Namespace }}
+# The 'venafi-connection' service account is used by multiple
+# controllers. When configuring which resources a VenafiConnection
+# can access, the RBAC rules you create manually must point to this SA.
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: venafi-connection
+ namespace: {{ $saNamespace | quote }}
+ labels:
+ {{- include "venafi-connection.labels" $ | nindent 4 }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: venafi-connection-role
+ labels:
+ {{- include "venafi-connection.labels" $ | nindent 4 }}
+rules:
+- apiGroups: [ "" ]
+ resources: [ "namespaces" ]
+ verbs: [ "get", "list", "watch" ]
+
+- apiGroups: [ "jetstack.io" ]
+ resources: [ "venaficonnections" ]
+ verbs: [ "get", "list", "watch" ]
+
+- apiGroups: [ "jetstack.io" ]
+ resources: [ "venaficonnections/status" ]
+ verbs: [ "get", "patch" ]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: venafi-connection-rolebinding
+ labels:
+ {{- include "venafi-connection.labels" $ | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: venafi-connection-role
+subjects:
+- kind: ServiceAccount
+ name: venafi-connection
+ namespace: {{ $saNamespace | quote }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/templates/venafi-rbac.yaml b/deploy/charts/discovery-agent/templates/venafi-rbac.yaml
new file mode 100644
index 00000000..35cf8792
--- /dev/null
+++ b/deploy/charts/discovery-agent/templates/venafi-rbac.yaml
@@ -0,0 +1,31 @@
+{{- if .Values.config.venafiConnection.enabled }}
+{{- $saNamespace := .Values.venafiConnection.serviceAccountNamespace | default $.Release.Namespace }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-impersonate-role
+ namespace: {{ $saNamespace | quote }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+rules:
+- apiGroups: [ "" ]
+ resources: [ "serviceaccounts" ]
+ verbs: [ "impersonate" ]
+ resourceNames: [ "venafi-connection" ]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "discovery-agent.fullname" . }}-impersonate-rolebinding
+ namespace: {{ $saNamespace | quote }}
+ labels:
+ {{- include "discovery-agent.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ include "discovery-agent.fullname" . }}-impersonate-role
+subjects:
+- kind: ServiceAccount
+ name: {{ include "discovery-agent.serviceAccountName" . }}
+ namespace: {{ $.Release.Namespace | quote }}
+{{- end }}
diff --git a/deploy/charts/discovery-agent/tests/configmap_test.yaml b/deploy/charts/discovery-agent/tests/configmap_test.yaml
new file mode 100644
index 00000000..ff68cd67
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/configmap_test.yaml
@@ -0,0 +1,123 @@
+suite: test configmap
+templates:
+ - configmap.yaml
+
+tests:
+ # Test basic ConfigMap rendering
+ - it: should create ConfigMap with required values
+ set:
+ config.clusterName: my-test-cluster
+ config.tsgID: "123456"
+ asserts:
+ - isKind:
+ of: ConfigMap
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-config
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'cluster_name: "my-test-cluster"'
+
+ # Test cluster description
+ - it: should include cluster description when set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.clusterDescription: "This is a test cluster"
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'cluster_description: "This is a test cluster"'
+
+ # Test period configuration
+ - it: should set custom period
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.period: "0h5m0s"
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'period: "0h5m0s"'
+
+ # Test exclude annotation keys regex
+ - it: should include excludeAnnotationKeysRegex when set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.excludeAnnotationKeysRegex:
+ - "^kapp\\.k14s\\.io/original.*"
+ - "^kubectl\\.kubernetes\\.io/.*"
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'exclude-annotation-keys-regex:'
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: '\^kapp\\\.k14s\\\.io/original\.\*'
+
+ # Test exclude label keys regex
+ - it: should include excludeLabelKeysRegex when set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.excludeLabelKeysRegex:
+ - "^helm\\.sh/.*"
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'exclude-label-keys-regex:'
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: '\^helm\\\.sh/\.\*'
+
+ # Test claimable certs when enabled
+ - it: "should include 'claimable_certs: true' when claimableCerts is true"
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.claimableCerts: true
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'claimable_certs: true'
+
+ # Test claimable certs when disabled
+ - it: "should not include 'claimable_certs' when claimableCerts is false"
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.claimableCerts: false
+ asserts:
+ - notMatchRegex:
+ path: data["config.yaml"]
+ pattern: 'claimable_certs'
+
+ # Test claimable certs default
+ - it: "should not include 'claimable_certs' when claimableCerts is unset"
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ asserts:
+ - notMatchRegex:
+ path: data["config.yaml"]
+ pattern: 'claimable_certs'
+
+ # Test data-gatherers are present
+ - it: should include all data-gatherers
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'kind: k8s-discovery'
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'name: k8s/secrets'
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'name: k8s/jobs'
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: 'name: k8s/deployments'
diff --git a/deploy/charts/discovery-agent/tests/deployment_test.yaml b/deploy/charts/discovery-agent/tests/deployment_test.yaml
new file mode 100644
index 00000000..21f7969e
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/deployment_test.yaml
@@ -0,0 +1,554 @@
+suite: test deployment
+templates:
+ - deployment.yaml
+
+tests:
+ # Test that tsgID is rendered correctly as a string
+ - it: tsgID is rendered as string in deployment args
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "987654321"
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --tsg-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: "987654321"
+
+ # Test that tsgID preserves leading zeros (only possible with string type)
+ # NB: TSG IDs are defined to start with "1", but this test is defence in depth
+ - it: tsgID preserves leading zeros when provided as string
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "0001234"
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --tsg-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: "0001234"
+
+ # Test basic deployment rendering with all required values
+ - it: deployment templates correctly with required values
+ set:
+ config.clusterName: my-test-cluster
+ config.tsgID: "123456"
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - matchRegex:
+ path: metadata.name
+ pattern: ^.*-discovery-agent$
+
+ # Test replica count
+ - it: should set replica count correctly
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ replicaCount: 3
+ asserts:
+ - equal:
+ path: spec.replicas
+ value: 3
+
+ # Test security contexts
+ - it: should apply pod security context
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podSecurityContext:
+ fsGroup: 2000
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.fsGroup
+ value: 2000
+
+ - it: should apply container security context defaults
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem
+ value: true
+ - equal:
+ path: spec.template.spec.containers[0].securityContext.runAsNonRoot
+ value: true
+ - equal:
+ path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
+ value: false
+
+ # Test resources
+ - it: should set resources when specified
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ resources:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].resources.limits.cpu
+ value: 200m
+ - equal:
+ path: spec.template.spec.containers[0].resources.requests.memory
+ value: 128Mi
+
+ # Test environment variables
+ - it: should set HTTP_PROXY environment variable
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ http_proxy: "http://proxy:8080"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: HTTP_PROXY
+ value: "http://proxy:8080"
+
+ - it: should set HTTPS_PROXY environment variable
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ https_proxy: "https://proxy:8443"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: HTTPS_PROXY
+ value: "https://proxy:8443"
+
+ - it: should set NO_PROXY environment variable
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ no_proxy: "127.0.0.1,localhost"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: NO_PROXY
+ value: "127.0.0.1,localhost"
+
+ # Test command line arguments
+ - it: should include metrics flag when enabled
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --enable-metrics
+
+ - it: should include pprof flag when enabled
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ pprof.enabled: true
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --enable-pprof
+
+ - it: should include custom server URL when set, omitting --tsg-id
+ set:
+ config.clusterName: test-cluster
+ config.serverURL: "https://custom.example.com"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --ngts-server-url
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: "https://custom.example.com"
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --tsg-id
+
+ - it: should fail when both tsgID and serverURL are set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.serverURL: "https://custom.example.com"
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.tsgID and config.serverURL are mutually exclusive; set exactly one"
+
+ - it: should fail when neither tsgID nor serverURL is set
+ set:
+ config.clusterName: test-cluster
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.tsgID is required when config.serverURL is not set"
+
+ - it: should include client ID when set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.clientID: "test-client-id"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: test-client-id
+
+ - it: should include client ID when clientId is set (lowercase d)
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.clientId: "test-client-id-lowercase"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: test-client-id-lowercase
+
+ - it: should prefer clientID over clientId when both are set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.clientID: "uppercase-takes-precedence"
+ config.clientId: "lowercase-ignored"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: uppercase-takes-precedence
+
+ - it: should include extra args
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ extraArgs:
+ - --log-level=6
+ - --custom-flag=value
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: "--log-level=6"
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: "--custom-flag=value"
+
+ # Test volumes and volume mounts
+ - it: should mount config and credentials volumes
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: config
+ mountPath: "/etc/discovery-agent"
+ readOnly: true
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: credentials
+ mountPath: "/etc/discovery-agent/credentials"
+ readOnly: true
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: config
+ configMap:
+ name: RELEASE-NAME-discovery-agent-config
+ optional: false
+
+ - it: should use custom secret name
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ config.secretName: custom-secret
+ asserts:
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: credentials
+ secret:
+ secretName: custom-secret
+ optional: false
+
+ # Test pod annotations and labels
+ - it: should apply pod annotations
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podAnnotations:
+ annotation-key: annotation-value
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations.annotation-key
+ value: annotation-value
+
+ - it: should apply pod labels
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podLabels:
+ custom-label: label-value
+ asserts:
+ - equal:
+ path: spec.template.metadata.labels.custom-label
+ value: label-value
+
+ # Test node selector, tolerations, and affinity
+ - it: should apply node selector
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ nodeSelector:
+ disktype: ssd
+ asserts:
+ - equal:
+ path: spec.template.spec.nodeSelector.disktype
+ value: ssd
+
+ - it: should apply tolerations
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ tolerations:
+ - key: "key1"
+ operator: "Equal"
+ value: "value1"
+ effect: "NoSchedule"
+ asserts:
+ - contains:
+ path: spec.template.spec.tolerations
+ content:
+ key: "key1"
+ operator: "Equal"
+ value: "value1"
+ effect: "NoSchedule"
+
+ - it: should apply affinity
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - node1
+ asserts:
+ - isNotEmpty:
+ path: spec.template.spec.affinity.nodeAffinity
+
+ # Test image pull secrets
+ - it: should apply image pull secrets
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ imagePullSecrets:
+ - name: my-secret
+ asserts:
+ - contains:
+ path: spec.template.spec.imagePullSecrets
+ content:
+ name: my-secret
+
+ # VenafiConnection mode wires the connection name/namespace through as flags
+ # and skips both the --ngts/--tsg-id flags and the keypair Secret mount. The
+ # agent picks the actual upload backend (NGTS or VCP) from the
+ # VenafiConnection resource at runtime.
+ - it: VenafiConnection mode passes the connection flags and omits NGTS/keypair flags
+ set:
+ config.clusterName: test-cluster
+ config.venafiConnection.enabled: true
+ config.venafiConnection.name: my-venconn
+ config.venafiConnection.namespace: my-ns
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-connection
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: my-venconn
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-connection-namespace
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: my-ns
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --ngts
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --tsg-id
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --ngts-server-url
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --private-key-path
+ - notContains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: credentials
+ mountPath: "/etc/discovery-agent/credentials"
+ readOnly: true
+ - notContains:
+ path: spec.template.spec.volumes
+ content:
+ name: credentials
+ secret:
+ secretName: discovery-agent-credentials
+ optional: false
+
+ # VenafiConnection mode does not require config.tsgID, since the agent reads
+ # the TSG ID from the VenafiConnection resource at runtime.
+ - it: VenafiConnection mode does not require config.tsgID
+ set:
+ config.clusterName: test-cluster
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+
+ # Keypair-mode fields must not be set in VenafiConnection mode; the chart
+ # should fail to render rather than silently dropping the values, so users
+ # don't end up with a config that looks wired but isn't.
+ - it: VenafiConnection mode rejects config.tsgID
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "999"
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.tsgID must not be set when config.venafiConnection.enabled is true; the TSG ID is read from the VenafiConnection's spec.ngts"
+
+ - it: VenafiConnection mode rejects config.serverURL
+ set:
+ config.clusterName: test-cluster
+ config.serverURL: "https://should-be-rejected.example.com"
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.serverURL must not be set when config.venafiConnection.enabled is true; the server URL is read from the VenafiConnection's spec"
+
+ - it: VenafiConnection mode rejects config.clientID
+ set:
+ config.clusterName: test-cluster
+ config.clientID: "should-be-rejected"
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.clientID must not be set when config.venafiConnection.enabled is true; authentication is performed via the VenafiConnection resource"
+
+ - it: VenafiConnection mode rejects config.clientId
+ set:
+ config.clusterName: test-cluster
+ config.clientId: "should-be-rejected"
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.clientId must not be set when config.venafiConnection.enabled is true; authentication is performed via the VenafiConnection resource"
+
+ - it: VenafiConnection mode rejects a non-default config.secretName
+ set:
+ config.clusterName: test-cluster
+ config.secretName: custom-credentials-secret
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - failedTemplate:
+ errorMessage: "config.secretName must not be set when config.venafiConnection.enabled is true; the credentials Secret is not mounted in this mode (authentication is performed via the VenafiConnection resource)"
+
+ - it: VenafiConnection mode accepts the default config.secretName
+ set:
+ config.clusterName: test-cluster
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+
+ # When venafiConnection.serviceAccountNamespace is set in VenafiConnection
+ # mode, the chart should pass --install-namespace to the agent so it knows
+ # which namespace holds the 'venafi-connection' service account used for
+ # token issuance / credential reads.
+ - it: VenafiConnection mode passes --install-namespace when serviceAccountNamespace is set
+ set:
+ config.clusterName: test-cluster
+ config.venafiConnection.enabled: true
+ venafiConnection.serviceAccountNamespace: venafi
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --install-namespace
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: venafi
+
+ # When venafiConnection.serviceAccountNamespace is unset (the default), the
+ # --install-namespace flag must NOT be passed; the agent falls back to
+ # POD_NAMESPACE in that case.
+ - it: VenafiConnection mode omits --install-namespace when serviceAccountNamespace is unset
+ set:
+ config.clusterName: test-cluster
+ config.venafiConnection.enabled: true
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --install-namespace
+
+ # --install-namespace is only emitted in VenafiConnection mode. In keypair
+ # (NGTS/TSG) mode it must be omitted even if venafiConnection.serviceAccountNamespace
+ # is set, since that value only applies to the VenafiConnection flow.
+ - it: keypair mode omits --install-namespace even when serviceAccountNamespace is set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ venafiConnection.serviceAccountNamespace: venafi
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --install-namespace
diff --git a/deploy/charts/discovery-agent/tests/poddisruptionbudget_test.yaml b/deploy/charts/discovery-agent/tests/poddisruptionbudget_test.yaml
new file mode 100644
index 00000000..13cb044d
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/poddisruptionbudget_test.yaml
@@ -0,0 +1,102 @@
+suite: test poddisruptionbudget
+templates:
+ - poddisruptionbudget.yaml
+
+tests:
+ # Test PodDisruptionBudget is not created by default
+ - it: should not create PodDisruptionBudget when disabled
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ # Test PodDisruptionBudget is created when enabled
+ - it: should create PodDisruptionBudget when enabled
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ asserts:
+ - isKind:
+ of: PodDisruptionBudget
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent
+
+ # Test default minAvailable when neither minAvailable nor maxUnavailable is set
+ - it: should set default minAvailable when no disruption values are set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ asserts:
+ - equal:
+ path: spec.minAvailable
+ value: 1
+ - isNull:
+ path: spec.maxUnavailable
+
+ # Test custom minAvailable
+ - it: should set custom minAvailable
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ podDisruptionBudget.minAvailable: 2
+ asserts:
+ - equal:
+ path: spec.minAvailable
+ value: 2
+ - isNull:
+ path: spec.maxUnavailable
+
+ # Test minAvailable as percentage
+ - it: should set minAvailable as percentage
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ podDisruptionBudget.minAvailable: "50%"
+ asserts:
+ - equal:
+ path: spec.minAvailable
+ value: "50%"
+
+ # Test custom maxUnavailable
+ - it: should set custom maxUnavailable
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ podDisruptionBudget.maxUnavailable: 1
+ asserts:
+ - equal:
+ path: spec.maxUnavailable
+ value: 1
+ - isNull:
+ path: spec.minAvailable
+
+ # Test maxUnavailable as percentage
+ - it: should set maxUnavailable as percentage
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ podDisruptionBudget.maxUnavailable: "25%"
+ asserts:
+ - equal:
+ path: spec.maxUnavailable
+ value: "25%"
+
+ # Test selector labels
+ - it: should use correct selector labels
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ podDisruptionBudget.enabled: true
+ asserts:
+ - isNotEmpty:
+ path: spec.selector.matchLabels
diff --git a/deploy/charts/discovery-agent/tests/podmonitor_test.yaml b/deploy/charts/discovery-agent/tests/podmonitor_test.yaml
new file mode 100644
index 00000000..ff4afc0d
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/podmonitor_test.yaml
@@ -0,0 +1,162 @@
+suite: test podmonitor
+templates:
+ - podmonitor.yaml
+
+tests:
+ # Test PodMonitor is not created by default
+ - it: should not create PodMonitor when metrics.podmonitor.enabled is false
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ # Test PodMonitor is not created when metrics are disabled
+ - it: should not create PodMonitor when metrics.enabled is false
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: false
+ metrics.podmonitor.enabled: true
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ # Test PodMonitor is created when both metrics and podmonitor are enabled
+ - it: should create PodMonitor when both metrics and podmonitor are enabled
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ asserts:
+ - isKind:
+ of: PodMonitor
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent
+
+ # Test PodMonitor namespace defaults to Release namespace
+ - it: should use Release namespace by default
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ release:
+ namespace: my-namespace
+ asserts:
+ - equal:
+ path: metadata.namespace
+ value: my-namespace
+
+ # Test custom PodMonitor namespace
+ - it: should use custom namespace when specified
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.namespace: monitoring
+ release:
+ namespace: default
+ asserts:
+ - equal:
+ path: metadata.namespace
+ value: monitoring
+ - contains:
+ path: spec.namespaceSelector.matchNames
+ content: default
+
+ # Test prometheus instance label
+ - it: should set prometheus instance label
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.prometheusInstance: custom-prometheus
+ asserts:
+ - equal:
+ path: metadata.labels.prometheus
+ value: custom-prometheus
+
+ # Test custom labels
+ - it: should apply custom labels
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.labels:
+ custom-label: custom-value
+ another-label: another-value
+ asserts:
+ - equal:
+ path: metadata.labels.custom-label
+ value: custom-value
+ - equal:
+ path: metadata.labels.another-label
+ value: another-value
+
+ # Test custom annotations
+ - it: should apply custom annotations
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.annotations:
+ custom-annotation: custom-value
+ asserts:
+ - equal:
+ path: metadata.annotations.custom-annotation
+ value: custom-value
+
+ # Test scrape configuration
+ - it: should configure scrape interval and timeout
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.interval: 30s
+ metrics.podmonitor.scrapeTimeout: 15s
+ asserts:
+ - equal:
+ path: spec.podMetricsEndpoints[0].interval
+ value: 30s
+ - equal:
+ path: spec.podMetricsEndpoints[0].scrapeTimeout
+ value: 15s
+
+ # Test honorLabels setting
+ - it: should set honorLabels correctly
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ metrics.podmonitor.honorLabels: true
+ asserts:
+ - equal:
+ path: spec.podMetricsEndpoints[0].honorLabels
+ value: true
+
+ # Test metrics endpoint configuration
+ - it: should configure metrics endpoint correctly
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ metrics.enabled: true
+ metrics.podmonitor.enabled: true
+ asserts:
+ - equal:
+ path: spec.podMetricsEndpoints[0].port
+ value: agent-api
+ - equal:
+ path: spec.podMetricsEndpoints[0].path
+ value: /metrics
diff --git a/deploy/charts/discovery-agent/tests/rbac_test.yaml b/deploy/charts/discovery-agent/tests/rbac_test.yaml
new file mode 100644
index 00000000..4457a876
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/rbac_test.yaml
@@ -0,0 +1,230 @@
+suite: test rbac
+templates:
+ - rbac.yaml
+
+tests:
+ # Test Role for event emission
+ - it: should create Role for event emission
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 0
+ asserts:
+ - isKind:
+ of: Role
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-event-emitted
+ - contains:
+ path: rules
+ content:
+ apiGroups: [""]
+ resources: ["events"]
+ verbs: ["create"]
+
+ # Test RoleBinding for event emission
+ - it: should create RoleBinding for event emission
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 1
+ asserts:
+ - isKind:
+ of: RoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-event-emitted
+ - equal:
+ path: roleRef.kind
+ value: Role
+ - equal:
+ path: roleRef.name
+ value: RELEASE-NAME-discovery-agent-event-emitted
+ - contains:
+ path: subjects
+ content:
+ kind: ServiceAccount
+ name: RELEASE-NAME-discovery-agent
+ namespace: NAMESPACE
+
+ # Test ClusterRoleBinding for cluster viewer
+ - it: should create ClusterRoleBinding for cluster viewer
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 2
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-cluster-viewer
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: view
+ - contains:
+ path: subjects
+ content:
+ kind: ServiceAccount
+ name: RELEASE-NAME-discovery-agent
+ namespace: NAMESPACE
+
+ # Test ClusterRole for secret reader
+ - it: should create ClusterRole for secret reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 3
+ asserts:
+ - isKind:
+ of: ClusterRole
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-secret-reader
+ - contains:
+ path: rules
+ content:
+ apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "list", "watch"]
+
+ # Test ClusterRoleBinding for secret reader
+ - it: should create ClusterRoleBinding for secret reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 4
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-secret-reader
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: RELEASE-NAME-discovery-agent-secret-reader
+
+ # Test ClusterRole for RBAC reader
+ - it: should create ClusterRole for RBAC reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 5
+ asserts:
+ - isKind:
+ of: ClusterRole
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-rbac-reader
+ - contains:
+ path: rules[0].resources
+ content: roles
+ - contains:
+ path: rules[0].resources
+ content: clusterroles
+ - contains:
+ path: rules[0].resources
+ content: rolebindings
+ - contains:
+ path: rules[0].resources
+ content: clusterrolebindings
+
+ # Test ClusterRoleBinding for RBAC reader
+ - it: should create ClusterRoleBinding for RBAC reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 6
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-rbac-reader
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: RELEASE-NAME-discovery-agent-rbac-reader
+
+ # Test ClusterRoleBinding for OIDC discovery
+ - it: should create ClusterRoleBinding for OIDC discovery
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 7
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-oidc-discovery
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: system:service-account-issuer-discovery
+
+ # Test ClusterRole for CRD reader
+ - it: should create ClusterRole for CRD reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 8
+ asserts:
+ - isKind:
+ of: ClusterRole
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-crd-reader
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - cert-manager.io
+ - cas-issuer.jetstack.io
+ - awspca.cert-manager.io
+ - jetstack.io
+ - firefly.venafi.com
+ - certmanager.step.sm
+ - cert-manager.k8s.cloudflare.com
+ - certmanager.freeipa.org
+ - ejbca-issuer.keyfactor.com
+ - networking.istio.io
+ - route.openshift.io
+ - admissionregistration.k8s.io
+ resources: ["*"]
+ verbs: ["get", "list", "watch"]
+
+ # Test ClusterRoleBinding for CRD reader
+ - it: should create ClusterRoleBinding for CRD reader
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ documentIndex: 9
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent-crd-reader
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: RELEASE-NAME-discovery-agent-crd-reader
+ - contains:
+ path: subjects
+ content:
+ kind: ServiceAccount
+ name: RELEASE-NAME-discovery-agent
+ namespace: NAMESPACE
diff --git a/deploy/charts/discovery-agent/tests/serviceaccount_test.yaml b/deploy/charts/discovery-agent/tests/serviceaccount_test.yaml
new file mode 100644
index 00000000..5547fa2b
--- /dev/null
+++ b/deploy/charts/discovery-agent/tests/serviceaccount_test.yaml
@@ -0,0 +1,78 @@
+suite: test serviceaccount
+templates:
+ - serviceaccount.yaml
+
+tests:
+ # Test ServiceAccount is created by default
+ - it: should create ServiceAccount when serviceAccount.create is true
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: true
+ asserts:
+ - isKind:
+ of: ServiceAccount
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-discovery-agent
+
+ # Test ServiceAccount is not created when disabled
+ - it: should not create ServiceAccount when serviceAccount.create is false
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ # Test custom ServiceAccount name
+ - it: should use custom name when serviceAccount.name is set
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: true
+ serviceAccount.name: custom-sa-name
+ asserts:
+ - equal:
+ path: metadata.name
+ value: custom-sa-name
+
+ # Test automountServiceAccountToken setting
+ - it: should set automountServiceAccountToken correctly
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: true
+ serviceAccount.automount: false
+ asserts:
+ - equal:
+ path: automountServiceAccountToken
+ value: false
+
+ - it: should enable automountServiceAccountToken by default
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: true
+ asserts:
+ - equal:
+ path: automountServiceAccountToken
+ value: true
+
+ # Test ServiceAccount annotations
+ - it: should apply annotations to ServiceAccount
+ set:
+ config.clusterName: test-cluster
+ config.tsgID: "123456"
+ serviceAccount.create: true
+ serviceAccount.annotations:
+ eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-role
+ custom-annotation: custom-value
+ asserts:
+ - equal:
+ path: metadata.annotations["eks.amazonaws.com/role-arn"]
+ value: arn:aws:iam::123456789012:role/my-role
+ - equal:
+ path: metadata.annotations.custom-annotation
+ value: custom-value
diff --git a/deploy/charts/discovery-agent/values.linter.exceptions b/deploy/charts/discovery-agent/values.linter.exceptions
new file mode 100644
index 00000000..e69de29b
diff --git a/deploy/charts/discovery-agent/values.schema.json b/deploy/charts/discovery-agent/values.schema.json
new file mode 100644
index 00000000..eb48d29b
--- /dev/null
+++ b/deploy/charts/discovery-agent/values.schema.json
@@ -0,0 +1,554 @@
+{
+ "$defs": {
+ "helm-values": {
+ "additionalProperties": false,
+ "properties": {
+ "affinity": {
+ "$ref": "#/$defs/helm-values.affinity"
+ },
+ "config": {
+ "$ref": "#/$defs/helm-values.config"
+ },
+ "extraArgs": {
+ "$ref": "#/$defs/helm-values.extraArgs"
+ },
+ "fullnameOverride": {
+ "$ref": "#/$defs/helm-values.fullnameOverride"
+ },
+ "global": {
+ "$ref": "#/$defs/helm-values.global"
+ },
+ "http_proxy": {
+ "$ref": "#/$defs/helm-values.http_proxy"
+ },
+ "https_proxy": {
+ "$ref": "#/$defs/helm-values.https_proxy"
+ },
+ "image": {
+ "$ref": "#/$defs/helm-values.image"
+ },
+ "imageNamespace": {
+ "$ref": "#/$defs/helm-values.imageNamespace"
+ },
+ "imagePullSecrets": {
+ "$ref": "#/$defs/helm-values.imagePullSecrets"
+ },
+ "imageRegistry": {
+ "$ref": "#/$defs/helm-values.imageRegistry"
+ },
+ "metrics": {
+ "$ref": "#/$defs/helm-values.metrics"
+ },
+ "nameOverride": {
+ "$ref": "#/$defs/helm-values.nameOverride"
+ },
+ "no_proxy": {
+ "$ref": "#/$defs/helm-values.no_proxy"
+ },
+ "nodeSelector": {
+ "$ref": "#/$defs/helm-values.nodeSelector"
+ },
+ "podAnnotations": {
+ "$ref": "#/$defs/helm-values.podAnnotations"
+ },
+ "podDisruptionBudget": {
+ "$ref": "#/$defs/helm-values.podDisruptionBudget"
+ },
+ "podLabels": {
+ "$ref": "#/$defs/helm-values.podLabels"
+ },
+ "podSecurityContext": {
+ "$ref": "#/$defs/helm-values.podSecurityContext"
+ },
+ "pprof": {
+ "$ref": "#/$defs/helm-values.pprof"
+ },
+ "replicaCount": {
+ "$ref": "#/$defs/helm-values.replicaCount"
+ },
+ "resources": {
+ "$ref": "#/$defs/helm-values.resources"
+ },
+ "securityContext": {
+ "$ref": "#/$defs/helm-values.securityContext"
+ },
+ "serviceAccount": {
+ "$ref": "#/$defs/helm-values.serviceAccount"
+ },
+ "tolerations": {
+ "$ref": "#/$defs/helm-values.tolerations"
+ },
+ "venafiConnection": {
+ "$ref": "#/$defs/helm-values.venafiConnection"
+ },
+ "volumeMounts": {
+ "$ref": "#/$defs/helm-values.volumeMounts"
+ },
+ "volumes": {
+ "$ref": "#/$defs/helm-values.volumes"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.affinity": {
+ "default": {},
+ "type": "object"
+ },
+ "helm-values.config": {
+ "additionalProperties": false,
+ "properties": {
+ "claimableCerts": {
+ "$ref": "#/$defs/helm-values.config.claimableCerts"
+ },
+ "clientID": {
+ "$ref": "#/$defs/helm-values.config.clientID"
+ },
+ "clientId": {
+ "$ref": "#/$defs/helm-values.config.clientId"
+ },
+ "clusterDescription": {
+ "$ref": "#/$defs/helm-values.config.clusterDescription"
+ },
+ "clusterName": {
+ "$ref": "#/$defs/helm-values.config.clusterName"
+ },
+ "excludeAnnotationKeysRegex": {
+ "$ref": "#/$defs/helm-values.config.excludeAnnotationKeysRegex"
+ },
+ "excludeLabelKeysRegex": {
+ "$ref": "#/$defs/helm-values.config.excludeLabelKeysRegex"
+ },
+ "period": {
+ "$ref": "#/$defs/helm-values.config.period"
+ },
+ "secretName": {
+ "$ref": "#/$defs/helm-values.config.secretName"
+ },
+ "serverURL": {
+ "$ref": "#/$defs/helm-values.config.serverURL"
+ },
+ "tsgID": {
+ "$ref": "#/$defs/helm-values.config.tsgID"
+ },
+ "venafiConnection": {
+ "$ref": "#/$defs/helm-values.config.venafiConnection"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.config.claimableCerts": {
+ "default": false,
+ "description": "Whether discovered certs can be claimed by other tenants (optional). true = certs are left unassigned, available for any tenant to claim. false (default) = certs are owned by this cluster's tenant.",
+ "type": "boolean"
+ },
+ "helm-values.config.clientID": {
+ "default": "",
+ "description": "Deprecated: Client ID for the configured service account. The client ID should be provided in the \"clientID\" field of the authentication secret (see config.secretName). This field is provided for compatibility for users migrating from the \"venafi-kubernetes-agent\" chart. Must not be set when config.venafiConnection.enabled is true.",
+ "type": "string"
+ },
+ "helm-values.config.clientId": {
+ "default": "",
+ "description": "Deprecated: Client ID for the configured service account (alternative to clientID). The client ID should be provided in the \"clientID\" field of the authentication secret (see config.secretName). This field is provided for compatibility for users migrating from the \"venafi-kubernetes-agent\" chart. If both clientID and clientId are set, clientID takes precedence. Must not be set when config.venafiConnection.enabled is true.",
+ "type": "string"
+ },
+ "helm-values.config.clusterDescription": {
+ "default": "",
+ "description": "A short description of the cluster where the agent is deployed (optional).\n\nThis description will be associated with the data that the agent uploads to the backend.",
+ "type": "string"
+ },
+ "helm-values.config.clusterName": {
+ "default": "",
+ "description": "Required: A human readable name for the cluster into which the agent is being deployed.\n\nThis cluster name will be associated with the data that the agent uploads to the backend.",
+ "type": "string"
+ },
+ "helm-values.config.excludeAnnotationKeysRegex": {
+ "default": [],
+ "description": "You can configure the agent to exclude some annotations or labels from being pushed. All Kubernetes objects are affected. The objects are still pushed, but the specified annotations and labels are removed before being pushed.\n\nDots is the only character that needs to be escaped in the regex. Use either double quotes with escaped single quotes or unquoted strings for the regex to avoid YAML parsing issues with `\\.`.\n\nExample: excludeAnnotationKeysRegex: ['^kapp\\.k14s\\.io/original.*']",
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.config.excludeLabelKeysRegex": {
+ "default": [],
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.config.period": {
+ "default": "0h1m0s",
+ "description": "How often to push data to the remote server",
+ "type": "string"
+ },
+ "helm-values.config.secretName": {
+ "default": "discovery-agent-credentials",
+ "description": "The name of the Secret containing the NGTS built-in service account credentials.\nThe Secret must contain the following key:\n- privatekey.pem: PEM-encoded private key for the service account\nThe Secret should also contain the following key:\n- clientID: Service account client ID (config.clientID must be set if not present)\nMust not be set when config.venafiConnection.enabled is true (the credentials Secret is not mounted in that mode).",
+ "type": "string"
+ },
+ "helm-values.config.serverURL": {
+ "default": "",
+ "description": "Explicit SCM server URL (optional).\nIf not set, the production SCM server URL is derived from config.tsgID. This value is intended for development purposes only and should not be set in production.\nMutually exclusive with config.tsgID.\nMust not be set when config.venafiConnection.enabled is true.",
+ "type": "string"
+ },
+ "helm-values.config.tsgID": {
+ "default": "",
+ "description": "The TSG (Tenant Service Group) ID to use when connecting to SCM. The production SCM server URL is derived from this value. Required unless config.serverURL is set. Mutually exclusive with config.serverURL. Must not be set when config.venafiConnection.enabled is true (the TSG ID is taken from the VenafiConnection's `spec.ngts` instead)."
+ },
+ "helm-values.config.venafiConnection": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "$ref": "#/$defs/helm-values.config.venafiConnection.enabled"
+ },
+ "name": {
+ "$ref": "#/$defs/helm-values.config.venafiConnection.name"
+ },
+ "namespace": {
+ "$ref": "#/$defs/helm-values.config.venafiConnection.namespace"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.config.venafiConnection.enabled": {
+ "default": false,
+ "description": "When set to true, config.tsgID, config.serverURL, config.clientID and config.clientId must not be set (the chart will fail to render otherwise), and the Secret named by config.secretName will _not_ be mounted into the Discovery Agent Pod.",
+ "type": "boolean"
+ },
+ "helm-values.config.venafiConnection.name": {
+ "default": "venafi-components",
+ "description": "The name of a VenafiConnection resource which contains the configuration for authenticating to the upload backend.",
+ "type": "string"
+ },
+ "helm-values.config.venafiConnection.namespace": {
+ "default": "venafi",
+ "description": "The namespace of a VenafiConnection resource which contains the configuration for authenticating to the upload backend.",
+ "type": "string"
+ },
+ "helm-values.extraArgs": {
+ "default": [],
+ "description": "extraArgs:\n- --logging-format=json\n- --log-level=6 # To enable HTTP request logging",
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.fullnameOverride": {
+ "default": "",
+ "type": "string"
+ },
+ "helm-values.global": {
+ "description": "Global values shared across all (sub)charts"
+ },
+ "helm-values.http_proxy": {
+ "description": "Configures the HTTP_PROXY environment variable where a HTTP proxy is required.",
+ "type": "string"
+ },
+ "helm-values.https_proxy": {
+ "description": "Configures the HTTPS_PROXY environment variable where a HTTP proxy is required.",
+ "type": "string"
+ },
+ "helm-values.image": {
+ "additionalProperties": false,
+ "properties": {
+ "digest": {
+ "$ref": "#/$defs/helm-values.image.digest"
+ },
+ "name": {
+ "$ref": "#/$defs/helm-values.image.name"
+ },
+ "pullPolicy": {
+ "$ref": "#/$defs/helm-values.image.pullPolicy"
+ },
+ "repository": {
+ "$ref": "#/$defs/helm-values.image.repository"
+ },
+ "tag": {
+ "$ref": "#/$defs/helm-values.image.tag"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.image.digest": {
+ "default": "",
+ "description": "Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.",
+ "type": "string"
+ },
+ "helm-values.image.name": {
+ "default": "discovery-agent",
+ "description": "The image name for the Discovery Agent.\nThis is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.",
+ "type": "string"
+ },
+ "helm-values.image.pullPolicy": {
+ "default": "IfNotPresent",
+ "description": "This sets the pull policy for images.",
+ "type": "string"
+ },
+ "helm-values.image.repository": {
+ "default": "",
+ "description": "Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`).\nExample: quay.io/jetstack/discovery-agent",
+ "type": "string"
+ },
+ "helm-values.image.tag": {
+ "default": "",
+ "description": "Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.",
+ "type": "string"
+ },
+ "helm-values.imageNamespace": {
+ "default": "jetstack",
+ "description": "The repository namespace used for discovery-agent images by default.\nExamples:\n- jetstack\n- custom-namespace",
+ "type": "string"
+ },
+ "helm-values.imagePullSecrets": {
+ "default": [],
+ "description": "This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/",
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.imageRegistry": {
+ "default": "quay.io",
+ "description": "The container registry used for discovery-agent images by default. This can include path prefixes (e.g. \"artifactory.example.com/docker\").",
+ "type": "string"
+ },
+ "helm-values.metrics": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "$ref": "#/$defs/helm-values.metrics.enabled"
+ },
+ "podmonitor": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.metrics.enabled": {
+ "default": true,
+ "description": "Enable the metrics server.\nIf false, the metrics server will be disabled and the other metrics fields below will be ignored.",
+ "type": "boolean"
+ },
+ "helm-values.metrics.podmonitor": {
+ "additionalProperties": false,
+ "properties": {
+ "annotations": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.annotations"
+ },
+ "enabled": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.enabled"
+ },
+ "endpointAdditionalProperties": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.endpointAdditionalProperties"
+ },
+ "honorLabels": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.honorLabels"
+ },
+ "interval": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.interval"
+ },
+ "labels": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.labels"
+ },
+ "namespace": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.namespace"
+ },
+ "prometheusInstance": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.prometheusInstance"
+ },
+ "scrapeTimeout": {
+ "$ref": "#/$defs/helm-values.metrics.podmonitor.scrapeTimeout"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.metrics.podmonitor.annotations": {
+ "default": {},
+ "description": "Additional annotations to add to the PodMonitor.",
+ "type": "object"
+ },
+ "helm-values.metrics.podmonitor.enabled": {
+ "default": false,
+ "description": "Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator. See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor",
+ "type": "boolean"
+ },
+ "helm-values.metrics.podmonitor.endpointAdditionalProperties": {
+ "default": {},
+ "description": "EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc.\n\nFor example:\nendpointAdditionalProperties:\n relabelings:\n - action: replace\n sourceLabels:\n - __meta_kubernetes_pod_node_name\n targetLabel: instance",
+ "type": "object"
+ },
+ "helm-values.metrics.podmonitor.honorLabels": {
+ "default": false,
+ "description": "Keep labels from scraped data, overriding server-side labels.",
+ "type": "boolean"
+ },
+ "helm-values.metrics.podmonitor.interval": {
+ "default": "60s",
+ "description": "The interval to scrape metrics.",
+ "type": "string"
+ },
+ "helm-values.metrics.podmonitor.labels": {
+ "default": {},
+ "description": "Additional labels to add to the PodMonitor.",
+ "type": "object"
+ },
+ "helm-values.metrics.podmonitor.namespace": {
+ "description": "The namespace that the pod monitor should live in.\nDefaults to the discovery-agent namespace.",
+ "type": "string"
+ },
+ "helm-values.metrics.podmonitor.prometheusInstance": {
+ "default": "default",
+ "description": "Specifies the `prometheus` label on the created PodMonitor. This is used when different Prometheus instances have label selectors matching different PodMonitors.",
+ "type": "string"
+ },
+ "helm-values.metrics.podmonitor.scrapeTimeout": {
+ "default": "30s",
+ "description": "The timeout before a metrics scrape fails.",
+ "type": "string"
+ },
+ "helm-values.nameOverride": {
+ "default": "",
+ "description": "This is to override the chart name.",
+ "type": "string"
+ },
+ "helm-values.no_proxy": {
+ "description": "Configures the NO_PROXY environment variable where a HTTP proxy is required, but certain domains should be excluded.",
+ "type": "string"
+ },
+ "helm-values.nodeSelector": {
+ "default": {},
+ "type": "object"
+ },
+ "helm-values.podAnnotations": {
+ "default": {},
+ "description": "This is for setting Kubernetes Annotations to a Pod. For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/",
+ "type": "object"
+ },
+ "helm-values.podDisruptionBudget": {
+ "default": {
+ "enabled": false
+ },
+ "description": "Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple replicas, consider setting podDisruptionBudget.enabled to true.",
+ "type": "object"
+ },
+ "helm-values.podLabels": {
+ "default": {},
+ "description": "This is for setting Kubernetes Labels to a Pod.\nFor more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/",
+ "type": "object"
+ },
+ "helm-values.podSecurityContext": {
+ "default": {},
+ "type": "object"
+ },
+ "helm-values.pprof": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "$ref": "#/$defs/helm-values.pprof.enabled"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.pprof.enabled": {
+ "default": false,
+ "description": "Enable profiling with the pprof endpoint",
+ "type": "boolean"
+ },
+ "helm-values.replicaCount": {
+ "default": 1,
+ "description": "This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/",
+ "type": "number"
+ },
+ "helm-values.resources": {
+ "default": {},
+ "type": "object"
+ },
+ "helm-values.securityContext": {
+ "default": {
+ "allowPrivilegeEscalation": false,
+ "capabilities": {
+ "drop": [
+ "ALL"
+ ]
+ },
+ "readOnlyRootFilesystem": true,
+ "runAsNonRoot": true,
+ "seccompProfile": {
+ "type": "RuntimeDefault"
+ }
+ },
+ "description": "Add Container specific SecurityContext settings to the container. Takes precedence over `podSecurityContext` when set. See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container",
+ "type": "object"
+ },
+ "helm-values.serviceAccount": {
+ "additionalProperties": false,
+ "properties": {
+ "annotations": {
+ "$ref": "#/$defs/helm-values.serviceAccount.annotations"
+ },
+ "automount": {
+ "$ref": "#/$defs/helm-values.serviceAccount.automount"
+ },
+ "create": {
+ "$ref": "#/$defs/helm-values.serviceAccount.create"
+ },
+ "name": {
+ "$ref": "#/$defs/helm-values.serviceAccount.name"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.serviceAccount.annotations": {
+ "default": {},
+ "description": "Annotations to add to the service account",
+ "type": "object"
+ },
+ "helm-values.serviceAccount.automount": {
+ "default": true,
+ "description": "Automatically mount a ServiceAccount's API credentials?",
+ "type": "boolean"
+ },
+ "helm-values.serviceAccount.create": {
+ "default": true,
+ "description": "Specifies whether a service account should be created",
+ "type": "boolean"
+ },
+ "helm-values.serviceAccount.name": {
+ "default": "",
+ "description": "The name of the service account to use.\nIf not set and create is true, a name is generated using the fullname template",
+ "type": "string"
+ },
+ "helm-values.tolerations": {
+ "default": [],
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.venafiConnection": {
+ "additionalProperties": false,
+ "properties": {
+ "include": {
+ "$ref": "#/$defs/helm-values.venafiConnection.include"
+ },
+ "serviceAccountNamespace": {
+ "$ref": "#/$defs/helm-values.venafiConnection.serviceAccountNamespace"
+ }
+ },
+ "type": "object"
+ },
+ "helm-values.venafiConnection.include": {
+ "default": false,
+ "description": "When set to false, the rendered output does not contain the VenafiConnection CRDs and RBAC. This is useful for when the Venafi Connection resoures are already installed separately.",
+ "type": "boolean"
+ },
+ "helm-values.venafiConnection.serviceAccountNamespace": {
+ "description": "The namespace in which the 'venafi-connection' service account lives. This is the service account that is used to create JWT tokens for SAs or read credential secrets. (defaults to the namespace in which the controller is running)",
+ "type": "string"
+ },
+ "helm-values.volumeMounts": {
+ "default": [],
+ "description": "Additional volumeMounts on the output Deployment definition.",
+ "items": {},
+ "type": "array"
+ },
+ "helm-values.volumes": {
+ "default": [],
+ "description": "Additional volumes on the output Deployment definition.",
+ "items": {},
+ "type": "array"
+ }
+ },
+ "$ref": "#/$defs/helm-values",
+ "$schema": "http://json-schema.org/draft-07/schema#"
+}
diff --git a/deploy/charts/discovery-agent/values.yaml b/deploy/charts/discovery-agent/values.yaml
new file mode 100644
index 00000000..a7ef2f0f
--- /dev/null
+++ b/deploy/charts/discovery-agent/values.yaml
@@ -0,0 +1,307 @@
+# +docs:section=Venafi Connection
+
+venafiConnection:
+ # When set to false, the rendered output does not contain the VenafiConnection CRDs
+ # and RBAC. This is useful for when the Venafi Connection resoures are already installed separately.
+ include: false
+
+ # The namespace in which the 'venafi-connection' service account lives. This is the service account
+ # that is used to create JWT tokens for SAs or read credential secrets. (defaults to the namespace
+ # in which the controller is running)
+ # +docs:property
+ # serviceAccountNamespace: venafi
+
+# +docs:section=Discovery Agent
+
+# Configuration for the Discovery Agent
+config:
+ # The TSG (Tenant Service Group) ID to use when connecting to SCM.
+ # The production SCM server URL is derived from this value.
+ # Required unless config.serverURL is set. Mutually exclusive with config.serverURL.
+ # Must not be set when config.venafiConnection.enabled is true (the TSG ID is taken from the VenafiConnection's `spec.ngts` instead).
+ # +docs:property
+ # +docs:type=number,string
+ tsgID: ""
+
+ # Required: A human readable name for the cluster into which the agent is being deployed.
+ #
+ # This cluster name will be associated with the data that the agent uploads to the backend.
+ # +docs:property
+ clusterName: ""
+
+ # A short description of the cluster where the agent is deployed (optional).
+ #
+ # This description will be associated with the data that the agent uploads to the backend.
+ # +docs:property
+ clusterDescription: ""
+
+ # Whether discovered certs can be claimed by other tenants (optional).
+ # true = certs are left unassigned, available for any tenant to claim.
+ # false (default) = certs are owned by this cluster's tenant.
+ claimableCerts: false
+
+ # How often to push data to the remote server
+ # +docs:property
+ period: "0h1m0s"
+
+ # You can configure the agent to exclude some annotations or
+ # labels from being pushed. All Kubernetes objects
+ # are affected. The objects are still pushed, but the specified annotations
+ # and labels are removed before being pushed.
+ #
+ # Dots is the only character that needs to be escaped in the regex. Use either
+ # double quotes with escaped single quotes or unquoted strings for the regex
+ # to avoid YAML parsing issues with `\.`.
+ #
+ # Example: excludeAnnotationKeysRegex: ['^kapp\.k14s\.io/original.*']
+ excludeAnnotationKeysRegex: []
+ excludeLabelKeysRegex: []
+
+ # Deprecated: Client ID for the configured service account.
+ # The client ID should be provided in the "clientID" field of the authentication secret (see config.secretName).
+ # This field is provided for compatibility for users migrating from the "venafi-kubernetes-agent" chart.
+ # Must not be set when config.venafiConnection.enabled is true.
+ # +docs:property
+ clientID: ""
+
+ # Deprecated: Client ID for the configured service account (alternative to clientID).
+ # The client ID should be provided in the "clientID" field of the authentication secret (see config.secretName).
+ # This field is provided for compatibility for users migrating from the "venafi-kubernetes-agent" chart.
+ # If both clientID and clientId are set, clientID takes precedence.
+ # Must not be set when config.venafiConnection.enabled is true.
+ # +docs:hidden
+ clientId: ""
+
+ # The name of the Secret containing the NGTS built-in service account credentials.
+ # The Secret must contain the following key:
+ # - privatekey.pem: PEM-encoded private key for the service account
+ # The Secret should also contain the following key:
+ # - clientID: Service account client ID (config.clientID must be set if not present)
+ # Must not be set when config.venafiConnection.enabled is true (the credentials Secret is not mounted in that mode).
+ # +docs:property
+ secretName: discovery-agent-credentials
+
+ # Explicit SCM server URL (optional).
+ # If not set, the production SCM server URL is derived from config.tsgID.
+ # This value is intended for development purposes only and should not be set in production.
+ # Mutually exclusive with config.tsgID.
+ # Must not be set when config.venafiConnection.enabled is true.
+ # +docs:hidden
+ serverURL: ""
+
+ # When venafiConnection.enabled is true, the Discovery Agent authenticates to
+ # its upload backend using the referenced VenafiConnection resource instead
+ # of the NGTS built-in service account key pair. For the NGTS backend, the
+ # VenafiConnection's `spec.ngts` (with `tsgID` or `url`, and a `jwt` source)
+ # is used.
+ venafiConnection:
+ # When set to true, config.tsgID, config.serverURL, config.clientID and
+ # config.clientId must not be set (the chart will fail to render
+ # otherwise), and the Secret named by config.secretName will _not_ be
+ # mounted into the Discovery Agent Pod.
+ enabled: false
+ # The name of a VenafiConnection resource which contains the configuration
+ # for authenticating to the upload backend.
+ name: venafi-components
+ # The namespace of a VenafiConnection resource which contains the
+ # configuration for authenticating to the upload backend.
+ namespace: venafi
+
+# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
+replicaCount: 1
+
+# The container registry used for discovery-agent images by default.
+# This can include path prefixes (e.g. "artifactory.example.com/docker").
+# +docs:property
+imageRegistry: "quay.io"
+
+# The repository namespace used for discovery-agent images by default.
+# Examples:
+# - jetstack
+# - custom-namespace
+# +docs:property
+imageNamespace: "jetstack"
+
+# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
+image:
+ # Full repository override (takes precedence over `imageRegistry`, `imageNamespace`,
+ # and `image.name`).
+ # Example: quay.io/jetstack/discovery-agent
+ # +docs:property
+ repository: ""
+
+ # The image name for the Discovery Agent.
+ # This is used (together with `imageRegistry` and `imageNamespace`) to construct the full
+ # image reference.
+ # +docs:property
+ name: discovery-agent
+
+ # This sets the pull policy for images.
+ pullPolicy: IfNotPresent
+
+ # Override the image tag to deploy by setting this variable.
+ # If no value is set, the chart's appVersion is used.
+ tag: ""
+
+ # Override the image digest to deploy by setting this variable.
+ # If set together with `image.tag`, the rendered image will include both tag and digest.
+ digest: ""
+
+# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+imagePullSecrets: []
+# This is to override the chart name.
+nameOverride: ""
+fullnameOverride: ""
+
+# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
+serviceAccount:
+ # Specifies whether a service account should be created
+ create: true
+ # Automatically mount a ServiceAccount's API credentials?
+ automount: true
+ # Annotations to add to the service account
+ annotations: {}
+ # The name of the service account to use.
+ # If not set and create is true, a name is generated using the fullname template
+ name: ""
+
+# This is for setting Kubernetes Annotations to a Pod.
+# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+podAnnotations: {}
+# This is for setting Kubernetes Labels to a Pod.
+# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+podLabels: {}
+
+podSecurityContext: {}
+ # fsGroup: 2000
+
+# Add Container specific SecurityContext settings to the container. Takes
+# precedence over `podSecurityContext` when set. See
+# https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container
+# +docs:property
+securityContext:
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ seccompProfile: { type: RuntimeDefault }
+
+resources: {}
+ # We usually recommend not to specify default resources and to leave this as a conscious
+ # choice for the user. This also increases chances charts run on environments with little
+ # resources, such as Minikube. If you do want to specify resources, uncomment the following
+ # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+ # limits:
+ # cpu: 100m
+ # memory: 128Mi
+ # requests:
+ # cpu: 100m
+ # memory: 128Mi
+
+# Additional volumes on the output Deployment definition.
+volumes: []
+# - name: foo
+# secret:
+# secretName: mysecret
+# optional: false
+
+# Additional volumeMounts on the output Deployment definition.
+volumeMounts: []
+# - name: foo
+# mountPath: "/etc/foo"
+# readOnly: true
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
+# Configures the HTTP_PROXY environment variable where a HTTP proxy is required.
+# +docs:property
+# http_proxy: "http://proxy:8080"
+
+# Configures the HTTPS_PROXY environment variable where a HTTP proxy is required.
+# +docs:property
+# https_proxy: "https://proxy:8080"
+
+# Configures the NO_PROXY environment variable where a HTTP proxy is required,
+# but certain domains should be excluded.
+# +docs:property
+# no_proxy: 127.0.0.1,localhost
+
+# Configure a PodDisruptionBudget for the agent's Deployment. If running with multiple
+# replicas, consider setting podDisruptionBudget.enabled to true.
+# +docs:property
+podDisruptionBudget:
+ # Enable or disable the PodDisruptionBudget resource, which helps prevent downtime
+ # during voluntary disruptions such as during a Node upgrade.
+ enabled: false
+
+ # Configure the minimum available pods for disruptions. Can either be set to
+ # an integer (e.g. 1) or a percentage value (e.g. 25%).
+ # Cannot be used if `maxUnavailable` is set.
+ # +docs:property
+ # minAvailable: 1
+
+ # Configure the maximum unavailable pods for disruptions. Can either be set to
+ # an integer (e.g. 1) or a percentage value (e.g. 25%).
+ # Cannot be used if `minAvailable` is set.
+ # +docs:property
+ # maxUnavailable: 1
+
+# extraArgs:
+# - --logging-format=json
+# - --log-level=6 # To enable HTTP request logging
+extraArgs: []
+
+pprof:
+ # Enable profiling with the pprof endpoint
+ enabled: false
+
+metrics:
+ # Enable the metrics server.
+ # If false, the metrics server will be disabled and the other metrics fields below will be ignored.
+ enabled: true
+ podmonitor:
+ # Create a PodMonitor to add the metrics to Prometheus, if you are using Prometheus Operator.
+ # See https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor
+ enabled: false
+
+ # The namespace that the pod monitor should live in.
+ # Defaults to the discovery-agent namespace.
+ # +docs:property
+ # namespace: ngts
+
+ # Specifies the `prometheus` label on the created PodMonitor.
+ # This is used when different Prometheus instances have label selectors
+ # matching different PodMonitors.
+ prometheusInstance: default
+
+ # The interval to scrape metrics.
+ interval: 60s
+
+ # The timeout before a metrics scrape fails.
+ scrapeTimeout: 30s
+
+ # Additional labels to add to the PodMonitor.
+ labels: {}
+
+ # Additional annotations to add to the PodMonitor.
+ annotations: {}
+
+ # Keep labels from scraped data, overriding server-side labels.
+ honorLabels: false
+
+ # EndpointAdditionalProperties allows setting additional properties on the endpoint such as relabelings, metricRelabelings etc.
+ #
+ # For example:
+ # endpointAdditionalProperties:
+ # relabelings:
+ # - action: replace
+ # sourceLabels:
+ # - __meta_kubernetes_pod_node_name
+ # targetLabel: instance
+ endpointAdditionalProperties: {}
diff --git a/deploy/charts/venafi-kubernetes-agent/README.md b/deploy/charts/venafi-kubernetes-agent/README.md
index b99f89be..e88e396a 100644
--- a/deploy/charts/venafi-kubernetes-agent/README.md
+++ b/deploy/charts/venafi-kubernetes-agent/README.md
@@ -99,13 +99,53 @@ endpointAdditionalProperties:
> ```
default replicas, do not scale up
+#### **imageRegistry** ~ `string`
+> Default value:
+> ```yaml
+> registry.venafi.cloud
+> ```
+
+The container registry used for venafi-kubernetes-agent images by default. This can include path prefixes (e.g. "artifactory.example.com/docker").
+
+#### **imageNamespace** ~ `string`
+> Default value:
+> ```yaml
+> venafi-agent
+> ```
+
+The repository namespace used for venafi-kubernetes-agent images by default.
+Examples:
+- venafi-agent
+- custom-namespace
+
+#### **image.registry** ~ `string`
+
+Deprecated: per-component registry prefix.
+
+If set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from
+`imageRegistry` + `imageNamespace` + `image.name`.
+
+This can produce "double registry" style references such as
+`legacy.example.io/registry.venafi.cloud/venafi-agent/...`. Prefer using the global
+`imageRegistry`/`imageNamespace` values.
+
#### **image.repository** ~ `string`
> Default value:
> ```yaml
-> registry.venafi.cloud/venafi-agent/venafi-agent
+> ""
> ```
-The container image for the Discovery Agent.
+Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`). Example: registry.venafi.cloud/venafi-agent/venafi-agent
+
+#### **image.name** ~ `string`
+> Default value:
+> ```yaml
+> venafi-agent
+> ```
+
+The image name for the Discovery Agent.
+This is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.
+
#### **image.pullPolicy** ~ `string`
> Default value:
> ```yaml
@@ -116,10 +156,17 @@ Kubernetes imagePullPolicy on Deployment.
#### **image.tag** ~ `string`
> Default value:
> ```yaml
-> v0.0.0
+> ""
+> ```
+
+Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.
+#### **image.digest** ~ `string`
+> Default value:
+> ```yaml
+> ""
> ```
-Overrides the image tag whose default is the chart appVersion.
+Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.
#### **imagePullSecrets** ~ `array`
> Default value:
> ```yaml
@@ -343,7 +390,10 @@ Configure VenafiConnection authentication
> false
> ```
-When set to true, the Discovery Agent will authenticate to CyberArk Certificate Manager using the configuration in a VenafiConnection resource. Use `venafiConnection.enabled=true` for [secretless authentication](https://docs.cyberark.com/mis-saas/vaas/k8s-components/t-install-tlspk-agent/). When set to true, the `authentication.secret` values will be ignored and the. Secret with `authentication.secretName` will _not_ be mounted into the
+When set to true, the Discovery Agent will authenticate to its upload backend using the configuration in a VenafiConnection resource. The backend is determined by the VenafiConnection's spec: use `spec.vcp` for CyberArk Certificate Manager (CMSaaS), or `spec.ngts` (with `tsgID` or
+`url`, and a `jwt` source) for NGTS / Palo Alto Networks. `spec.tpp` and
+`spec.vcp.apiKey` are rejected by the agent.
+Use `venafiConnection.enabled=true` for [secretless authentication](https://docs.cyberark.com/mis-saas/vaas/k8s-components/t-install-tlspk-agent/). When set to true, the `authentication.secret` values will be ignored and the Secret with `authentication.secretName` will _not_ be mounted into the
Discovery Agent Pod.
#### **authentication.venafiConnection.name** ~ `string`
> Default value:
diff --git a/deploy/charts/venafi-kubernetes-agent/crd_bases/jetstack.io_venaficonnections.yaml b/deploy/charts/venafi-kubernetes-agent/crd_bases/jetstack.io_venaficonnections.yaml
index cbd1fb98..9389601d 100644
--- a/deploy/charts/venafi-kubernetes-agent/crd_bases/jetstack.io_venaficonnections.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/crd_bases/jetstack.io_venaficonnections.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
- controller-gen.kubebuilder.io/version: v0.19.0
+ controller-gen.kubebuilder.io/version: v0.21.0
name: venaficonnections.jetstack.io
spec:
group: jetstack.io
@@ -94,12 +94,12 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
- firefly:
+ distributedIssuer:
properties:
accessToken:
description: |-
The list of steps to retrieve the Access Token that will be used to connect
- to Firefly.
+ to the Distributed Issuer.
items:
properties:
hashicorpVaultLDAP:
@@ -141,9 +141,225 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
type: string
role:
description: |-
@@ -191,6 +407,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -218,7 +446,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -255,10 +483,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -271,6 +502,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -284,21 +520,251 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
url:
- description: The URL to connect to the Workload Identity Manager
- instance.
+ description: The URL to connect to the Distributed Issuer instance.
type: string
required:
- url
type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be
+ used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault
+ instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate
+ with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate
+ with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tsgID url] must be set
+ rule: '[has(self.tsgID),has(self.url)].filter(x,x==true).size()
+ == 1'
tpp:
properties:
accessToken:
@@ -344,10 +810,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -394,6 +856,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -421,7 +895,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -458,10 +932,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -474,6 +951,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -487,11 +969,11 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -503,6 +985,7 @@ spec:
venafi-connection-lib.
type: string
required:
+ - accessToken
- url
type: object
vaas:
@@ -554,10 +1037,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -604,6 +1083,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -631,7 +1122,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -668,10 +1159,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -684,6 +1178,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -697,11 +1196,11 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -750,10 +1249,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -800,6 +1295,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -827,7 +1334,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -864,10 +1371,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -880,6 +1390,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -893,11 +1408,11 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -908,10 +1423,10 @@ spec:
type: string
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: apiKey
- or accessToken'
- rule: '(has(self.apiKey) ? 1 : 0) + (has(self.accessToken) ? 1 :
- 0) == 1'
+ - message: exactly one of the fields in [apiKey accessToken] must
+ be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size()
+ == 1'
vcp:
properties:
accessToken:
@@ -959,10 +1474,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -1009,6 +1520,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -1036,7 +1559,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1073,10 +1596,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -1089,6 +1615,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -1102,11 +1633,11 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -1155,10 +1686,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and
- will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -1205,6 +1732,18 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded
+ in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -1232,7 +1771,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1269,10 +1808,13 @@ spec:
- UsernamePassword
- JWT
type: string
- clientId:
- description: ClientID is the clientId used to authenticate
+ clientID:
+ description: ClientID is the clientID used to authenticate
with TPP.
type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
url:
description: |-
The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
@@ -1285,6 +1827,11 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId]
+ may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size()
+ <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -1298,11 +1845,11 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken)
- ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret)
- ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth)
- ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken
+ hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP
+ tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size()
+ == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -1313,16 +1860,16 @@ spec:
type: string
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: apiKey
- or accessToken'
- rule: '(has(self.apiKey) ? 1 : 0) + (has(self.accessToken) ? 1 :
- 0) == 1'
+ - message: exactly one of the fields in [apiKey accessToken] must
+ be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size()
+ == 1'
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: tpp or
- vcp'
- rule: '(has(self.tpp) ? 1 : 0) + (has(self.vaas) ? 1 : 0) + (has(self.vcp)
- ? 1 : 0) + (has(self.firefly) ? 1 : 0) == 1'
+ - message: exactly one of the fields in [tpp ngts vcp vaas distributedIssuer
+ firefly] must be set
+ rule: '[has(self.tpp),has(self.ngts),has(self.vcp),has(self.vaas),has(self.distributedIssuer),has(self.firefly)].filter(x,x==true).size()
+ == 1'
status:
properties:
conditions:
diff --git a/deploy/charts/venafi-kubernetes-agent/templates/_helpers.tpl b/deploy/charts/venafi-kubernetes-agent/templates/_helpers.tpl
index 5517b982..c067a65c 100644
--- a/deploy/charts/venafi-kubernetes-agent/templates/_helpers.tpl
+++ b/deploy/charts/venafi-kubernetes-agent/templates/_helpers.tpl
@@ -60,3 +60,59 @@ Create the name of the service account to use
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+
+{{/*
+Util function for generating an image reference based on the provided options.
+This function is derviced from similar functions used in the cert-manager GitHub organization
+*/}}
+{{- define "venafi-kubernetes-agent.image" -}}
+{{- /*
+Calling convention:
+- (tuple )
+We intentionally pass imageRegistry/imageNamespace as explicit arguments rather than reading
+from `.Values` inside this helper, because `helm-tool lint` does not reliably track `.Values.*`
+usage through tuple/variable indirection.
+*/ -}}
+{{- if ne (len .) 4 -}}
+ {{- fail (printf "ERROR: template \"venafi-kubernetes-agent.image\" expects (tuple ), got %d arguments" (len .)) -}}
+{{- end -}}
+{{- $image := index . 0 -}}
+{{- $imageRegistry := index . 1 | default "" -}}
+{{- $imageNamespace := index . 2 | default "" -}}
+{{- $defaultReference := index . 3 -}}
+{{- $repository := "" -}}
+{{- if $image.repository -}}
+ {{- $repository = $image.repository -}}
+ {{- /*
+ Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry.
+ */ -}}
+ {{- if $image.registry -}}
+ {{- $repository = printf "%s/%s" $image.registry $repository -}}
+ {{- end -}}
+{{- else -}}
+ {{- $name := required "ERROR: image.name must be set when image.repository is empty" $image.name -}}
+ {{- $repository = $name -}}
+ {{- if $imageNamespace -}}
+ {{- $repository = printf "%s/%s" $imageNamespace $repository -}}
+ {{- end -}}
+ {{- if $imageRegistry -}}
+ {{- $repository = printf "%s/%s" $imageRegistry $repository -}}
+ {{- end -}}
+ {{- /*
+ Backwards compatibility: if image.registry is set, additionally prefix the repository with this registry.
+ */ -}}
+ {{- if $image.registry -}}
+ {{- $repository = printf "%s/%s" $image.registry $repository -}}
+ {{- end -}}
+{{- end -}}
+{{- $repository -}}
+{{- if and $image.tag $image.digest -}}
+ {{- printf ":%s@%s" $image.tag $image.digest -}}
+{{- else if $image.tag -}}
+ {{- printf ":%s" $image.tag -}}
+{{- else if $image.digest -}}
+ {{- printf "@%s" $image.digest -}}
+{{- else -}}
+ {{- printf "%s" $defaultReference -}}
+{{- end -}}
+{{- end }}
diff --git a/deploy/charts/venafi-kubernetes-agent/templates/deployment.yaml b/deploy/charts/venafi-kubernetes-agent/templates/deployment.yaml
index f37e8a16..c582d504 100644
--- a/deploy/charts/venafi-kubernetes-agent/templates/deployment.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/templates/deployment.yaml
@@ -30,7 +30,7 @@ spec:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+ image: "{{ template "venafi-kubernetes-agent.image" (tuple .Values.image .Values.imageRegistry .Values.imageNamespace (printf ":%s" .Chart.AppVersion)) }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: POD_NAMESPACE
@@ -81,8 +81,8 @@ spec:
- {{ .Values.config.clientId | quote }}
- "--private-key-path"
- "/etc/venafi/agent/key/{{ .Values.authentication.secretKey }}"
- {{- end }}
- --venafi-cloud
+ {{- end }}
{{- if .Values.metrics.enabled }}
- --enable-metrics
{{- end }}
diff --git a/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.without-validations.yaml b/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.without-validations.yaml
index 1f73351a..b9d2342a 100644
--- a/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.without-validations.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.without-validations.yaml
@@ -99,12 +99,12 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
- firefly:
+ distributedIssuer:
properties:
accessToken:
description: |-
The list of steps to retrieve the Access Token that will be used to connect
- to Firefly.
+ to the Distributed Issuer.
items:
properties:
hashicorpVaultLDAP:
@@ -145,8 +145,208 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
type: string
role:
description: |-
@@ -192,6 +392,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -219,7 +430,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -256,8 +467,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -286,11 +500,220 @@ spec:
type: array
x-kubernetes-list-type: atomic
url:
- description: The URL to connect to the Workload Identity Manager instance.
+ description: The URL to connect to the Distributed Issuer instance.
type: string
required:
- url
type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
tpp:
properties:
accessToken:
@@ -335,9 +758,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -382,6 +802,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -409,7 +840,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -446,8 +877,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -483,6 +917,7 @@ spec:
venafi-connection-lib.
type: string
required:
+ - accessToken
- url
type: object
vaas:
@@ -532,9 +967,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -579,6 +1011,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -606,7 +1049,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -643,8 +1086,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -716,9 +1162,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -763,6 +1206,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -790,7 +1244,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -827,8 +1281,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -908,9 +1365,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -955,6 +1409,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -982,7 +1447,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1019,8 +1484,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -1092,9 +1560,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -1139,6 +1604,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -1166,7 +1642,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1203,8 +1679,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
diff --git a/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.yaml b/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.yaml
index 6e2885e3..1845793a 100644
--- a/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/templates/venafi-connection-crd.yaml
@@ -99,12 +99,12 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
- firefly:
+ distributedIssuer:
properties:
accessToken:
description: |-
The list of steps to retrieve the Access Token that will be used to connect
- to Firefly.
+ to the Distributed Issuer.
items:
properties:
hashicorpVaultLDAP:
@@ -145,8 +145,214 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ url:
+ description: The URL to connect to the Distributed Issuer instance.
+ type: string
+ required:
+ - url
+ type: object
+ firefly:
+ properties:
+ accessToken:
+ description: |-
+ The list of steps to retrieve the Access Token that will be used to connect
+ to the Distributed Issuer.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
type: string
role:
description: |-
@@ -192,6 +398,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -219,7 +436,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -256,8 +473,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -271,6 +491,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -283,17 +506,235 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
url:
- description: The URL to connect to the Workload Identity Manager instance.
+ description: The URL to connect to the Distributed Issuer instance.
type: string
required:
- url
type: object
+ ngts:
+ properties:
+ jwt:
+ description: The list of steps to retrieve the JWT that will be used to connect to the NGTS Data Plane.
+ items:
+ properties:
+ hashicorpVaultLDAP:
+ description: |-
+ HashicorpVaultLDAP is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ ldapPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/ldap/static-cred/:role_name
+ or
+ /v1/ldap/creds/:role_name
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - ldapPath
+ type: object
+ hashicorpVaultOAuth:
+ description: |-
+ HashicorpVaultOAuth is a SecretSource that relies on a prior SecretSource
+ step to provide an OAuth token, which this step uses to authenticate to
+ Vault. The output of this step is a Vault token. This step allows you to use
+ the step `HashicorpVaultSecret` afterwards.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with HashiCorp Vault. The only supported value is "OIDC".
+ enum:
+ - OIDC
+ type: string
+ authPath:
+ description: |-
+ The login URL used for obtaining the Vault token. Example:
+ /v1/auth/oidc/login
+ type: string
+ role:
+ description: |-
+ The role defined in Vault that we want to use when authenticating to
+ Vault.
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - authInputType
+ - authPath
+ - role
+ type: object
+ hashicorpVaultSecret:
+ description: |-
+ HashicorpVaultSecret is a SecretSource step that requires a Vault token in
+ the previous step, either using a step `HashicorpVaultOAuth` or `Secret`. It
+ then fetches the requested secrets from Vault for use in the next step.
+ properties:
+ fields:
+ description: |-
+ The fields are Vault keys pointing to the secrets passed to the next
+ SecretSource step.
+
+ Example 1 (TPP, username and password): imagining that you have stored
+ the username and password for TPP under the keys "username" and
+ "password", you will want to set this field to `["username",
+ "password"]`. The username is expected to be given first, the password
+ second.
+ items:
+ type: string
+ type: array
+ secretPath:
+ description: |-
+ The full HTTP path to the secret in Vault. Example:
+ /v1/secret/data/application-team-a/tpp-username-password
+ type: string
+ url:
+ description: The URL to connect to your HashiCorp Vault instance.
+ type: string
+ required:
+ - fields
+ - secretPath
+ type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
+ secret:
+ description: |-
+ Secret is a SecretSource step meant to be the first step. It retrieves secret
+ values from a Kubernetes Secret, and passes them to the next step.
+ properties:
+ fields:
+ description: |-
+ The names of the fields we want to extract from the Kubernetes secret.
+ These fields are passed to the next step in the chain.
+ items:
+ type: string
+ type: array
+ name:
+ description: The name of the Kubernetes secret.
+ type: string
+ required:
+ - fields
+ - name
+ type: object
+ serviceAccountToken:
+ description: |-
+ ServiceAccountToken is a SecretSource step meant to be the first step. It
+ uses the Kubernetes TokenRequest API to retrieve a token for a given service
+ account, and passes it to the next step.
+ properties:
+ audiences:
+ description: |-
+ Audiences are the intended audiences of the token. A recipient of a
+ token must identify themself with an identifier in the list of
+ audiences of the token, and otherwise should reject the token. A
+ token issued for multiple audiences may be used to authenticate
+ against any of the audiences listed but implies a high degree of
+ trust between the target audiences.
+ items:
+ type: string
+ type: array
+ expirationSeconds:
+ description: |-
+ ExpirationSeconds is the requested duration of validity of the request. The
+ token issuer may return a token with a different validity duration so a
+ client needs to check the 'expiration' field in a response.
+ format: int64
+ type: integer
+ name:
+ description: The name of the Kubernetes service account.
+ type: string
+ required:
+ - audiences
+ - name
+ type: object
+ tppOAuth:
+ description: |-
+ TPPOAuth is a SecretSource step that authenticates to a TPP server. This
+ step is meant to be the last step and requires a prior step that depends
+ on the `authInputType`.
+ properties:
+ authInputType:
+ description: |-
+ AuthInputType is the authentication method to be used to authenticate
+ with TPP. The supported values are "UsernamePassword" and "JWT".
+ enum:
+ - UsernamePassword
+ - JWT
+ type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
+ clientId:
+ description: 'Deprecated: use clientID instead.'
+ type: string
+ url:
+ description: |-
+ The URL to connect to the Certificate Manager, Self-Hosted instance. The two URLs
+ https://tpp.example.com and https://tpp.example.com/vedsdk are
+ equivalent. The ending `/vedsdk` is optional and is stripped out
+ by our client.
+ If not set, defaults to the URL defined at the top-level of the
+ TPP configuration.
+ type: string
+ required:
+ - authInputType
+ type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
+ vcpOAuth:
+ description: |-
+ VCPOAuth is a SecretSource step that authenticates to the
+ Certificate Manager, SaaS. This step is meant to be the last step and requires a prior step
+ that outputs a JWT token.
+ properties:
+ tenantID:
+ description: TenantID is the tenant ID used to authenticate with Certificate Manager, SaaS.
+ type: string
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
+ tsgID:
+ description: |-
+ The TSGID of the NGTS instance to connect to.
+ This is a required field when URL is not set, and is used to construct the default URL in
+ the format https://.ngts.paloaltonetworks.com
+ type: string
+ url:
+ description: |-
+ The URL to connect to the NGTS Data Plane. If not set, the default
+ value https://.ngts.paloaltonetworks.com is used.
+ type: string
+ required:
+ - jwt
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of the fields in [tsgID url] must be set
+ rule: '[has(self.tsgID),has(self.url)].filter(x,x==true).size() == 1'
tpp:
properties:
accessToken:
@@ -338,9 +779,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -385,6 +823,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -412,7 +861,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -449,8 +898,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -464,6 +916,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -476,8 +931,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -489,6 +944,7 @@ spec:
venafi-connection-lib.
type: string
required:
+ - accessToken
- url
type: object
vaas:
@@ -538,9 +994,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -585,6 +1038,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -612,7 +1076,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -649,8 +1113,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -664,6 +1131,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -676,8 +1146,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -725,9 +1195,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -772,6 +1239,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -799,7 +1277,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -836,8 +1314,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -851,6 +1332,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -863,8 +1347,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -875,8 +1359,8 @@ spec:
type: string
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: apiKey or accessToken'
- rule: '(has(self.apiKey) ? 1 : 0) + (has(self.accessToken) ? 1 : 0) == 1'
+ - message: exactly one of the fields in [apiKey accessToken] must be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size() == 1'
vcp:
properties:
accessToken:
@@ -923,9 +1407,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -970,6 +1451,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -997,7 +1489,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1034,8 +1526,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -1049,6 +1544,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -1061,8 +1559,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -1110,9 +1608,6 @@ spec:
The login URL used for obtaining the Vault token. Example:
/v1/auth/oidc/login
type: string
- clientId:
- description: 'Deprecated: This field does nothing and will be removed in the future.'
- type: string
role:
description: |-
The role defined in Vault that we want to use when authenticating to
@@ -1157,6 +1652,17 @@ spec:
- fields
- secretPath
type: object
+ privateKeyJWT:
+ description: |-
+ PrivateKeyJWT is a SecretSource step that generates a JWT token signed by the input private key.
+ This JWT can typically be used to authenticate to the NGTS Data Plane.
+ properties:
+ clientID:
+ description: ClientID is the clientID that will be encoded in the "iss" and "sub" claims of the generated JWT.
+ type: string
+ required:
+ - clientID
+ type: object
secret:
description: |-
Secret is a SecretSource step meant to be the first step. It retrieves secret
@@ -1184,7 +1690,7 @@ spec:
properties:
audiences:
description: |-
- Audiences are the intendend audiences of the token. A recipient of a
+ Audiences are the intended audiences of the token. A recipient of a
token must identify themself with an identifier in the list of
audiences of the token, and otherwise should reject the token. A
token issued for multiple audiences may be used to authenticate
@@ -1221,8 +1727,11 @@ spec:
- UsernamePassword
- JWT
type: string
+ clientID:
+ description: ClientID is the clientID used to authenticate with TPP.
+ type: string
clientId:
- description: ClientID is the clientId used to authenticate with TPP.
+ description: 'Deprecated: use clientID instead.'
type: string
url:
description: |-
@@ -1236,6 +1745,9 @@ spec:
required:
- authInputType
type: object
+ x-kubernetes-validations:
+ - message: at most one of the fields in [clientID clientId] may be set
+ rule: '[has(self.clientID),has(self.clientId)].filter(x,x==true).size() <= 1'
vcpOAuth:
description: |-
VCPOAuth is a SecretSource step that authenticates to the
@@ -1248,8 +1760,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
- - message: must have exactly one field set
- rule: '((has(self.secret) ? 1 : 0) + (has(self.serviceAccountToken) ? 1 : 0) + (has(self.hashicorpVaultOAuth) ? 1 : 0) + (has(self.hashicorpVaultSecret) ? 1 : 0) + (has(self.hashicorpVaultLDAP) ? 1 : 0) + (has(self.tppOAuth) ? 1 : 0) + (has(self.vcpOAuth) ? 1 : 0)) == 1'
+ - message: exactly one of the fields in [secret serviceAccountToken hashicorpVaultOAuth hashicorpVaultSecret hashicorpVaultLDAP tppOAuth vcpOAuth privateKeyJWT] must be set
+ rule: '[has(self.secret),has(self.serviceAccountToken),has(self.hashicorpVaultOAuth),has(self.hashicorpVaultSecret),has(self.hashicorpVaultLDAP),has(self.tppOAuth),has(self.vcpOAuth),has(self.privateKeyJWT)].filter(x,x==true).size() == 1'
maxItems: 50
type: array
x-kubernetes-list-type: atomic
@@ -1260,12 +1772,12 @@ spec:
type: string
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: apiKey or accessToken'
- rule: '(has(self.apiKey) ? 1 : 0) + (has(self.accessToken) ? 1 : 0) == 1'
+ - message: exactly one of the fields in [apiKey accessToken] must be set
+ rule: '[has(self.apiKey),has(self.accessToken)].filter(x,x==true).size() == 1'
type: object
x-kubernetes-validations:
- - message: 'must have exactly ONE of the following fields set: tpp or vcp'
- rule: '(has(self.tpp) ? 1 : 0) + (has(self.vaas) ? 1 : 0) + (has(self.vcp) ? 1 : 0) + (has(self.firefly) ? 1 : 0) == 1'
+ - message: exactly one of the fields in [tpp ngts vcp vaas distributedIssuer firefly] must be set
+ rule: '[has(self.tpp),has(self.ngts),has(self.vcp),has(self.vaas),has(self.distributedIssuer),has(self.firefly)].filter(x,x==true).size() == 1'
status:
properties:
conditions:
diff --git a/deploy/charts/venafi-kubernetes-agent/tests/values/custom-volumes.yaml b/deploy/charts/venafi-kubernetes-agent/test-values/custom-volumes.yaml
similarity index 100%
rename from deploy/charts/venafi-kubernetes-agent/tests/values/custom-volumes.yaml
rename to deploy/charts/venafi-kubernetes-agent/test-values/custom-volumes.yaml
diff --git a/deploy/charts/venafi-kubernetes-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/venafi-kubernetes-agent/tests/__snapshot__/configmap_test.yaml.snap
index 96a51a96..aeca0541 100644
--- a/deploy/charts/venafi-kubernetes-agent/tests/__snapshot__/configmap_test.yaml.snap
+++ b/deploy/charts/venafi-kubernetes-agent/tests/__snapshot__/configmap_test.yaml.snap
@@ -1,6 +1,6 @@
custom-cluster-description:
1: |
- raw: |2
+ raw: |
- Check the credentials Secret exists: "agent-credentials"
> kubectl get secret -n test-ns agent-credentials
- Check the application is running:
@@ -287,7 +287,7 @@ custom-cluster-description:
namespace: test-ns
custom-cluster-name:
1: |
- raw: |2
+ raw: |
- Check the credentials Secret exists: "agent-credentials"
> kubectl get secret -n test-ns agent-credentials
- Check the application is running:
@@ -574,7 +574,7 @@ custom-cluster-name:
namespace: test-ns
custom-configmap:
1: |
- |2
+ |
You are using a custom configuration in the following ConfigMap: "agent-custom-config".
DEPRECATION: The `cluster_id` configuration field is deprecated.
@@ -589,7 +589,7 @@ custom-configmap:
> kubectl logs -n test-ns -l app.kubernetes.io/instance=test
custom-period:
1: |
- raw: |2
+ raw: |
- Check the credentials Secret exists: "agent-credentials"
> kubectl get secret -n test-ns agent-credentials
- Check the application is running:
@@ -876,7 +876,7 @@ custom-period:
namespace: test-ns
defaults:
1: |
- raw: |2
+ raw: |
- Check the credentials Secret exists: "agent-credentials"
> kubectl get secret -n test-ns agent-credentials
- Check the application is running:
diff --git a/deploy/charts/venafi-kubernetes-agent/tests/deployment_test.yaml b/deploy/charts/venafi-kubernetes-agent/tests/deployment_test.yaml
index e44e5bcc..9f770ff1 100644
--- a/deploy/charts/venafi-kubernetes-agent/tests/deployment_test.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/tests/deployment_test.yaml
@@ -78,7 +78,7 @@ tests:
# Check the volumes and volumeMounts works correctly
- it: Volumes and VolumeMounts added correctly
values:
- - ./values/custom-volumes.yaml
+ - ../test-values/custom-volumes.yaml
asserts:
- isKind:
of: Deployment
@@ -133,3 +133,71 @@ tests:
- lengthEqual :
path: spec.template.spec.containers[0].env
count: 4
+
+ # VenafiConnection mode (used for both VCP and NGTS backends) wires the
+ # connection name/namespace through as flags and skips mounting the keypair
+ # Secret. The Secret-based --client-id / --private-key-path flags must not be
+ # present in this mode.
+ - it: VenafiConnection mode passes the connection flags and omits the credentials Secret
+ set:
+ authentication.venafiConnection.enabled: true
+ authentication.venafiConnection.name: my-venconn
+ authentication.venafiConnection.namespace: my-ns
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-connection
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: my-venconn
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-connection-namespace
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: my-ns
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --private-key-path
+ - notContains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-cloud
+ - notContains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: credentials
+ mountPath: /etc/venafi/agent/key
+ readOnly: true
+ - notContains:
+ path: spec.template.spec.volumes
+ content:
+ name: credentials
+ secret:
+ secretName: agent-credentials
+ optional: false
+
+ # Keypair mode (the default, used when authentication.venafiConnection.enabled
+ # is false) still renders --venafi-cloud since the agent's mode-resolution
+ # treats keypair as a Venafi Cloud backend.
+ - it: Keypair mode still passes --venafi-cloud
+ set:
+ config.clientId: "00000000-0000-0000-0000-000000000000"
+ template: deployment.yaml
+ asserts:
+ - isKind:
+ of: Deployment
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --venafi-cloud
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --client-id
+ - contains:
+ path: spec.template.spec.containers[0].args
+ content: --private-key-path
diff --git a/deploy/charts/venafi-kubernetes-agent/values.schema.json b/deploy/charts/venafi-kubernetes-agent/values.schema.json
index 0b4076c7..9b31de55 100644
--- a/deploy/charts/venafi-kubernetes-agent/values.schema.json
+++ b/deploy/charts/venafi-kubernetes-agent/values.schema.json
@@ -36,9 +36,15 @@
"image": {
"$ref": "#/$defs/helm-values.image"
},
+ "imageNamespace": {
+ "$ref": "#/$defs/helm-values.imageNamespace"
+ },
"imagePullSecrets": {
"$ref": "#/$defs/helm-values.imagePullSecrets"
},
+ "imageRegistry": {
+ "$ref": "#/$defs/helm-values.imageRegistry"
+ },
"metrics": {
"$ref": "#/$defs/helm-values.metrics"
},
@@ -131,7 +137,7 @@
},
"helm-values.authentication.venafiConnection.enabled": {
"default": false,
- "description": "When set to true, the Discovery Agent will authenticate to CyberArk Certificate Manager using the configuration in a VenafiConnection resource. Use `venafiConnection.enabled=true` for [secretless authentication](https://docs.cyberark.com/mis-saas/vaas/k8s-components/t-install-tlspk-agent/). When set to true, the `authentication.secret` values will be ignored and the. Secret with `authentication.secretName` will _not_ be mounted into the\nDiscovery Agent Pod.",
+ "description": "When set to true, the Discovery Agent will authenticate to its upload backend using the configuration in a VenafiConnection resource. The backend is determined by the VenafiConnection's spec: use `spec.vcp` for CyberArk Certificate Manager (CMSaaS), or `spec.ngts` (with `tsgID` or\n`url`, and a `jwt` source) for NGTS / Palo Alto Networks. `spec.tpp` and\n`spec.vcp.apiKey` are rejected by the agent.\nUse `venafiConnection.enabled=true` for [secretless authentication](https://docs.cyberark.com/mis-saas/vaas/k8s-components/t-install-tlspk-agent/). When set to true, the `authentication.secret` values will be ignored and the Secret with `authentication.secretName` will _not_ be mounted into the\nDiscovery Agent Pod.",
"type": "boolean"
},
"helm-values.authentication.venafiConnection.name": {
@@ -331,9 +337,18 @@
"helm-values.image": {
"additionalProperties": false,
"properties": {
+ "digest": {
+ "$ref": "#/$defs/helm-values.image.digest"
+ },
+ "name": {
+ "$ref": "#/$defs/helm-values.image.name"
+ },
"pullPolicy": {
"$ref": "#/$defs/helm-values.image.pullPolicy"
},
+ "registry": {
+ "$ref": "#/$defs/helm-values.image.registry"
+ },
"repository": {
"$ref": "#/$defs/helm-values.image.repository"
},
@@ -343,19 +358,38 @@
},
"type": "object"
},
+ "helm-values.image.digest": {
+ "default": "",
+ "description": "Override the image digest to deploy by setting this variable. If set together with `image.tag`, the rendered image will include both tag and digest.",
+ "type": "string"
+ },
+ "helm-values.image.name": {
+ "default": "venafi-agent",
+ "description": "The image name for the Discovery Agent.\nThis is used (together with `imageRegistry` and `imageNamespace`) to construct the full image reference.",
+ "type": "string"
+ },
"helm-values.image.pullPolicy": {
"default": "IfNotPresent",
"description": "Kubernetes imagePullPolicy on Deployment.",
"type": "string"
},
+ "helm-values.image.registry": {
+ "description": "Deprecated: per-component registry prefix.\n\nIf set, this value is *prepended* to the image repository that the chart would otherwise render. This applies both when `image.repository` is set and when the repository is computed from\n`imageRegistry` + `imageNamespace` + `image.name`.\n\nThis can produce \"double registry\" style references such as\n`legacy.example.io/registry.venafi.cloud/venafi-agent/...`. Prefer using the global\n`imageRegistry`/`imageNamespace` values.",
+ "type": "string"
+ },
"helm-values.image.repository": {
- "default": "registry.venafi.cloud/venafi-agent/venafi-agent",
- "description": "The container image for the Discovery Agent.",
+ "default": "",
+ "description": "Full repository override (takes precedence over `imageRegistry`, `imageNamespace`, and `image.name`). Example: registry.venafi.cloud/venafi-agent/venafi-agent",
"type": "string"
},
"helm-values.image.tag": {
- "default": "v0.0.0",
- "description": "Overrides the image tag whose default is the chart appVersion.",
+ "default": "",
+ "description": "Override the image tag to deploy by setting this variable. If no value is set, the chart's appVersion is used.",
+ "type": "string"
+ },
+ "helm-values.imageNamespace": {
+ "default": "venafi-agent",
+ "description": "The repository namespace used for venafi-kubernetes-agent images by default.\nExamples:\n- venafi-agent\n- custom-namespace",
"type": "string"
},
"helm-values.imagePullSecrets": {
@@ -364,6 +398,11 @@
"items": {},
"type": "array"
},
+ "helm-values.imageRegistry": {
+ "default": "registry.venafi.cloud",
+ "description": "The container registry used for venafi-kubernetes-agent images by default. This can include path prefixes (e.g. \"artifactory.example.com/docker\").",
+ "type": "string"
+ },
"helm-values.metrics": {
"additionalProperties": false,
"properties": {
diff --git a/deploy/charts/venafi-kubernetes-agent/values.yaml b/deploy/charts/venafi-kubernetes-agent/values.yaml
index a2eaaec6..09bc018e 100644
--- a/deploy/charts/venafi-kubernetes-agent/values.yaml
+++ b/deploy/charts/venafi-kubernetes-agent/values.yaml
@@ -50,15 +50,53 @@ metrics:
# default replicas, do not scale up
replicaCount: 1
+# The container registry used for venafi-kubernetes-agent images by default.
+# This can include path prefixes (e.g. "artifactory.example.com/docker").
+# +docs:property
+imageRegistry: registry.venafi.cloud
+
+# The repository namespace used for venafi-kubernetes-agent images by default.
+# Examples:
+# - venafi-agent
+# - custom-namespace
+# +docs:property
+imageNamespace: venafi-agent
+
image:
- # The container image for the Discovery Agent.
- repository: registry.venafi.cloud/venafi-agent/venafi-agent
+ # Deprecated: per-component registry prefix.
+ #
+ # If set, this value is *prepended* to the image repository that the chart would otherwise render.
+ # This applies both when `image.repository` is set and when the repository is computed from
+ # `imageRegistry` + `imageNamespace` + `image.name`.
+ #
+ # This can produce "double registry" style references such as
+ # `legacy.example.io/registry.venafi.cloud/venafi-agent/...`. Prefer using the global
+ # `imageRegistry`/`imageNamespace` values.
+ # +docs:property
+ # registry: registry.venafi.cloud
+
+ # Full repository override (takes precedence over `imageRegistry`, `imageNamespace`,
+ # and `image.name`).
+ # Example: registry.venafi.cloud/venafi-agent/venafi-agent
+ # +docs:property
+ repository: ""
+
+ # The image name for the Discovery Agent.
+ # This is used (together with `imageRegistry` and `imageNamespace`) to construct the full
+ # image reference.
+ # +docs:property
+ name: venafi-agent
# Kubernetes imagePullPolicy on Deployment.
pullPolicy: IfNotPresent
- # Overrides the image tag whose default is the chart appVersion.
- tag: "v0.0.0"
+ # Override the image tag to deploy by setting this variable.
+ # If no value is set, the chart's appVersion is used.
+ tag: ""
+
+ # Override the image digest to deploy by setting this variable.
+ # If set together with `image.tag`, the rendered image will include both tag and digest.
+ digest: ""
# Specify image pull credentials if using a private registry. Example:
# - name: my-pull-secret
@@ -200,11 +238,15 @@ authentication:
# +docs:section=Venafi Connection
# Configure VenafiConnection authentication
venafiConnection:
- # When set to true, the Discovery Agent will authenticate to CyberArk Certificate Manager
- # using the configuration in a VenafiConnection resource.
+ # When set to true, the Discovery Agent will authenticate to its upload
+ # backend using the configuration in a VenafiConnection resource. The
+ # backend is determined by the VenafiConnection's spec: use `spec.vcp`
+ # for CyberArk Certificate Manager (CMSaaS), or `spec.ngts` (with `tsgID` or
+ # `url`, and a `jwt` source) for NGTS / Palo Alto Networks. `spec.tpp` and
+ # `spec.vcp.apiKey` are rejected by the agent.
# Use `venafiConnection.enabled=true` for [secretless authentication](https://docs.cyberark.com/mis-saas/vaas/k8s-components/t-install-tlspk-agent/).
- # When set to true, the `authentication.secret` values will be ignored and the
- # Secret with `authentication.secretName` will _not_ be mounted into the
+ # When set to true, the `authentication.secret` values will be ignored and
+ # the Secret with `authentication.secretName` will _not_ be mounted into the
# Discovery Agent Pod.
enabled: false
# The name of a VenafiConnection resource which contains the configuration
diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml
index ea0b28e5..239b8a24 100644
--- a/examples/machinehub.yaml
+++ b/examples/machinehub.yaml
@@ -12,6 +12,10 @@
# go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml
data-gatherers:
+# Gather Kubernetes OIDC information
+- name: ark/oidc
+ kind: oidc
+
# Gather Kubernetes API server version information
- name: ark/discovery
kind: k8s-discovery
@@ -125,3 +129,49 @@ data-gatherers:
resource-type:
version: v1
resource: pods
+
+# Gather Kubernetes config maps with specific conjur.org label
+- name: ark/configmaps
+ kind: k8s-dynamic
+ config:
+ resource-type:
+ resource: configmaps
+ version: v1
+ label-selectors:
+ - conjur.org/name=conjur-connect-configmap
+
+# Gather External Secrets Operator ExternalSecret resources
+- name: ark/esoexternalsecrets
+ kind: k8s-dynamic
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: externalsecrets
+
+# Gather External Secrets Operator SecretStore resources
+- name: ark/esosecretstores
+ kind: k8s-dynamic
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: secretstores
+
+# Gather External Secrets Operator ClusterExternalSecret resources
+- name: ark/esoclusterexternalsecrets
+ kind: k8s-dynamic
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clusterexternalsecrets
+
+# Gather External Secrets Operator ClusterSecretStore resources
+- name: ark/esoclustersecretstores
+ kind: k8s-dynamic
+ config:
+ resource-type:
+ group: external-secrets.io
+ version: v1
+ resource: clustersecretstores
diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json
index 2cdba65c..0b93ef8a 100644
--- a/examples/machinehub/input.json
+++ b/examples/machinehub/input.json
@@ -1,4 +1,34 @@
[
+ {
+ "data-gatherer": "ark/oidc",
+ "data": {
+ "openid_configuration": {
+ "id_token_signing_alg_values_supported": [
+ "RS256"
+ ],
+ "issuer": "https://kubernetes.default.svc.cluster.local",
+ "jwks_uri": "https://10.10.1.2:6443/openid/v1/jwks",
+ "response_types_supported": [
+ "id_token"
+ ],
+ "subject_types_supported": [
+ "public"
+ ]
+ },
+ "jwks": {
+ "keys": [
+ {
+ "alg": "RS256",
+ "e": "AQAB",
+ "kid": "C-2916LkMJqepqULK2nqhq6uzVB6So_yyGnqyuor71Q",
+ "kty": "RSA",
+ "n": "sYh6rDpl5DyzBk8qlnYXo6Sf9WbplnXJv3tPxWTvhCFsVu9G5oWjknkafVDq5UOJrlybJJNjBmUyiEi1wbdnuhceJS7rZ3sRnNp3aNoS0omCR6iHJCOuoboSlcaPuRmYw4oWXlVUXlKyw8PYPVbNCcTLuq9nqf8y33mIqe7XJsf5-Z5P05WbK9Rzj-SJvlZLQ4dSFtIiwqLkm_2fpRLj0d8Af1F6vuztnhhUE2_PDsfIWdl_kJKkrK3B5x7k5tgTyFrNQPzlRBgK9jmK0HskwAFIDaLKb7FUWuUiQjn94rjKCED4iy201YPAoZBKIHFDlFVkQ_S3quwPcRyOS18r7w",
+ "use": "sig"
+ }
+ ]
+ }
+ }
+ },
{
"data-gatherer": "ark/discovery",
"data": {
@@ -123,5 +153,35 @@
"data": {
"items": []
}
+ },
+ {
+ "data-gatherer": "ark/configmaps",
+ "data": {
+ "items": []
+ }
+ },
+ {
+ "data-gatherer": "ark/esoexternalsecrets",
+ "data": {
+ "items": []
+ }
+ },
+ {
+ "data-gatherer": "ark/esosecretstores",
+ "data": {
+ "items": []
+ }
+ },
+ {
+ "data-gatherer": "ark/esoclusterexternalsecrets",
+ "data": {
+ "items": []
+ }
+ },
+ {
+ "data-gatherer": "ark/esoclustersecretstores",
+ "data": {
+ "items": []
+ }
}
]
diff --git a/examples/one-shot-oidc.yaml b/examples/one-shot-oidc.yaml
new file mode 100644
index 00000000..d5ce8e99
--- /dev/null
+++ b/examples/one-shot-oidc.yaml
@@ -0,0 +1,16 @@
+# one-shot-oidc.yaml
+#
+# An example configuration file which can be used for local testing.
+# For example:
+#
+# go run . agent \
+# --agent-config-file examples/one-shot-oidc.yaml \
+# --one-shot \
+# --output-path output.json
+#
+organization_id: "my-organization"
+cluster_id: "my_cluster"
+period: 1m
+data-gatherers:
+- kind: "oidc"
+ name: "ark/oidc"
diff --git a/go.mod b/go.mod
index 0c074ebf..566323ee 100644
--- a/go.mod
+++ b/go.mod
@@ -1,112 +1,119 @@
// TODO(wallrj): Rename the Go module to match the repository name
module github.com/jetstack/preflight
-go 1.24.4
+go 1.26.4
require (
- github.com/Venafi/vcert/v5 v5.12.2
github.com/cenkalti/backoff/v5 v5.0.3
- github.com/fatih/color v1.18.0
+ github.com/fatih/color v1.19.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-multierror v1.1.1
- github.com/jetstack/venafi-connection-lib v0.5.2
+ github.com/jetstack/venafi-connection-lib v0.6.1-0.20260528123542-443dd7e48a1a
+ github.com/lestrrat-go/jwx/v3 v3.1.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/pmylund/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
- golang.org/x/sync v0.19.0
+ golang.org/x/sync v0.20.0
gopkg.in/yaml.v2 v2.4.0
- k8s.io/api v0.34.3
- k8s.io/apimachinery v0.34.3
- k8s.io/client-go v0.34.3
- k8s.io/component-base v0.34.3
- sigs.k8s.io/controller-runtime v0.22.4
+ k8s.io/api v0.36.1
+ k8s.io/apimachinery v0.36.1
+ k8s.io/client-go v0.36.1
+ k8s.io/component-base v0.36.1
+ sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/yaml v1.6.0
)
require (
- cel.dev/expr v0.24.0 // indirect
- github.com/Khan/genqlient v0.8.1 // indirect
- github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ cel.dev/expr v0.25.2 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
- github.com/fsnotify/fsnotify v1.9.0 // indirect
- github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/fsnotify/fsnotify v1.10.1 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect
github.com/go-logr/zapr v1.3.0 // indirect
- github.com/go418/concurrentcache v0.6.0 // indirect
- github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2 // indirect
- github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
- github.com/google/btree v1.1.3 // indirect
- github.com/google/cel-go v0.26.0 // indirect
- github.com/google/gnostic-models v0.7.0 // indirect
+ github.com/go-openapi/swag/cmdutils v0.26.0 // indirect
+ github.com/go-openapi/swag/conv v0.26.0 // indirect
+ github.com/go-openapi/swag/fileutils v0.26.0 // indirect
+ github.com/go-openapi/swag/jsonname v0.26.0 // indirect
+ github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
+ github.com/go-openapi/swag/loading v0.26.0 // indirect
+ github.com/go-openapi/swag/mangling v0.26.0 // indirect
+ github.com/go-openapi/swag/netutils v0.26.0 // indirect
+ github.com/go-openapi/swag/stringutils v0.26.0 // indirect
+ github.com/go-openapi/swag/typeutils v0.26.0 // indirect
+ github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
+ github.com/go418/concurrentcache v0.7.0 // indirect
+ github.com/go418/concurrentcache/logger v0.0.0-20260113125750-8e23f97949aa // indirect
+ github.com/goccy/go-json v0.10.6 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
+ github.com/google/cel-go v0.28.1 // indirect
+ github.com/google/gnostic-models v0.7.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
- github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/pkg/errors v0.9.1 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+ github.com/lestrrat-go/httpcc v1.0.1 // indirect
+ github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect
+ github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/sosodev/duration v1.3.1 // indirect
- github.com/stoewer/go-strcase v1.3.0 // indirect
- github.com/vektah/gqlparser/v2 v2.5.30 // indirect
+ github.com/segmentio/asm v1.2.1 // indirect
+ github.com/sosodev/duration v1.4.0 // indirect
+ github.com/valyala/fastjson v1.6.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
- github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
- go.opentelemetry.io/otel v1.35.0 // indirect
- go.opentelemetry.io/otel/trace v1.35.0 // indirect
+ go.opentelemetry.io/otel v1.44.0 // indirect
+ go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- go.uber.org/zap v1.27.0 // indirect
- go.yaml.in/yaml/v2 v2.4.2 // indirect
+ go.uber.org/zap v1.28.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
- golang.org/x/net v0.47.0 // indirect
- gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
- gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
- gopkg.in/ini.v1 v1.67.0 // indirect
- k8s.io/apiextensions-apiserver v0.34.3 // indirect
- k8s.io/apiserver v0.34.3 // indirect
+ golang.org/x/crypto v0.52.0 // indirect
+ golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
+ golang.org/x/net v0.55.0 // indirect
+ gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+ k8s.io/apiextensions-apiserver v0.36.1 // indirect
+ k8s.io/apiserver v0.36.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
- sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+ github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/go-logr/logr v1.4.3
- github.com/go-openapi/jsonpointer v0.21.0 // indirect
- github.com/go-openapi/jsonreference v0.20.4 // indirect
- github.com/go-openapi/swag v0.23.0 // indirect
- github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/go-openapi/jsonpointer v0.23.1 // indirect
+ github.com/go-openapi/jsonreference v0.21.6 // indirect
+ github.com/go-openapi/swag v0.26.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2
- github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/josharian/intern v1.0.0 // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-colorable v0.1.15 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.66.1 // indirect
- github.com/prometheus/procfs v0.16.1 // indirect
- golang.org/x/oauth2 v0.30.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/term v0.37.0 // indirect
- golang.org/x/text v0.31.0 // indirect
- golang.org/x/time v0.9.0 // indirect
- google.golang.org/protobuf v1.36.8 // indirect
+ github.com/prometheus/common v0.68.1 // indirect
+ github.com/prometheus/procfs v0.20.1 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
+ golang.org/x/sys v0.45.0 // indirect
+ golang.org/x/term v0.43.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
+ golang.org/x/time v0.15.0 // indirect
+ google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1
- k8s.io/klog/v2 v2.130.1
- k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
- k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
- sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+ k8s.io/klog/v2 v2.140.0
+ k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 // indirect
+ k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
)
diff --git a/go.sum b/go.sum
index 7a717d60..3c509778 100644
--- a/go.sum
+++ b/go.sum
@@ -1,48 +1,44 @@
-cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
-cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
-github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
-github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
-github.com/Venafi/vcert/v5 v5.12.2 h1:Ee3/A9fZRiisuwuz22/Nqgl19H0ztQjWv35AC63qPcA=
-github.com/Venafi/vcert/v5 v5.12.2/go.mod h1:x3l0pB0q0E6wuhPe7nzfkUEwwraK7amnBWQ4LtT1bbw=
-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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
-github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
+cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
-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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
-github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
+github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
-github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
-github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
-github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
-github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
+github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
+github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno=
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -51,51 +47,79 @@ 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
-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.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
-github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
-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-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
+github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
+github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
+github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
+github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
+github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
+github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU=
+github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
+github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
+github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
+github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=
+github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=
+github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
+github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
+github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
+github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
+github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
+github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
+github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ=
+github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0=
+github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c=
+github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo=
+github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
+github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
+github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
+github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
+github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
+github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
+github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
+github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
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/go418/concurrentcache v0.6.0 h1:36A7j+c0dChEAMotq+lBQwQPyI4CMCy5HgMCcw8sY1g=
-github.com/go418/concurrentcache v0.6.0/go.mod h1:F498AylMP488QhU9KSE8VoN3u2FhGt7hXOgJ2CdvysM=
-github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2 h1:wVvBhfD+7srZ470Z06t5rp93faukGddvUJR4+owL0Kw=
-github.com/go418/concurrentcache/logger v0.0.0-20250207095056-c0b7f8cc8bc2/go.mod h1:DpmmUFByr4p8fGMbp2gsGJhqgcP1SXjyVZDiW0f8aSY=
-github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
-github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/go418/concurrentcache v0.7.0 h1:1rrZ3StkIPBKoVcYpG6kjW/TvV7fKN/FDgrq/G/n52Y=
+github.com/go418/concurrentcache v0.7.0/go.mod h1:xNgh3I+7SOpYL/shsjGOBZs0v4YcjNPa51wcU8E7LEw=
+github.com/go418/concurrentcache/logger v0.0.0-20260113125750-8e23f97949aa h1:ChHwM7TV4zUrm14aJ6Rgri0QAsaNiqgfpY/VMvyYsig=
+github.com/go418/concurrentcache/logger v0.0.0-20260113125750-8e23f97949aa/go.mod h1:DpmmUFByr4p8fGMbp2gsGJhqgcP1SXjyVZDiW0f8aSY=
+github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
+github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
+github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
-github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
-github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
-github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
-github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
-github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
+github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
+github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
+github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
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/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/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/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
-github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
-github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -103,14 +127,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jetstack/venafi-connection-lib v0.5.2 h1:Mzn8PANYQc5mBPHOhgkTW0VsvnKJsQmO+WcAjDwoR8E=
-github.com/jetstack/venafi-connection-lib v0.5.2/go.mod h1:0seQ/uP6MpB3KVMxf56jUzs/HBVpmRQLKU3Juak9p3Q=
-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/jetstack/venafi-connection-lib v0.6.1-0.20260528123542-443dd7e48a1a h1:DhmA/QBT7cTOAN2hoj36i7QSCBIkWpo7qbJcEeV8gCQ=
+github.com/jetstack/venafi-connection-lib v0.6.1-0.20260528123542-443dd7e48a1a/go.mod h1:KPndhwwPHPkBqv7cocVTtEDPHV/CBrwapLqzUnwbCUs=
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -119,13 +139,24 @@ 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc=
+github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
+github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
+github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
+github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw=
+github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=
+github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
+github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -136,10 +167,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/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.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
-github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
-github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
-github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
+github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
+github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
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=
@@ -151,179 +182,140 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
-github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
-github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
-github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
-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/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
+github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
-github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE=
+github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
-github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
-github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
+github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
+github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
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/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
-github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
-go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
-go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
-go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
-go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
-go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
-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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
-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.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
-go.opentelemetry.io/otel/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.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-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.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM=
+go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q=
+go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50=
+go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw=
+go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY=
+go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
+go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
+go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
+go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
+go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
-go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
-go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
-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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
-golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
-golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
-golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
-google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
-google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
+golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
+golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
+google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
+google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
-k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
-k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
-k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
-k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
-k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
-k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
-k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
-k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
-k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
-k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
-k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c=
-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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
-k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
-k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
-sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
-sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
+k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
+k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks=
+k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8=
+k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
+k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
+k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY=
+k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw=
+k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
+k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
+k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA=
+k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI=
+k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
+k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
+k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 h1:mPMaPMpBij2V1Wv/fR+HW124vVGXXvOSS9ver/9yjWs=
+k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
+k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
+k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
+sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
+sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
-sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo=
+sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/hack/ark/cluster-external-secret.yaml b/hack/ark/cluster-external-secret.yaml
new file mode 100644
index 00000000..882f0085
--- /dev/null
+++ b/hack/ark/cluster-external-secret.yaml
@@ -0,0 +1,27 @@
+# Sample ClusterExternalSecret for e2e testing
+# This is a minimal ClusterExternalSecret CR that will be discovered by the agent.
+# This is a cluster-scoped resource that can create ExternalSecrets in multiple namespaces.
+apiVersion: external-secrets.io/v1
+kind: ClusterExternalSecret
+metadata:
+ name: e2e-test-cluster-external-secret
+ labels:
+ app.kubernetes.io/name: e2e-test
+ app.kubernetes.io/component: cluster-external-secret
+spec:
+ externalSecretSpec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: e2e-test-cluster-secret-store
+ kind: ClusterSecretStore
+ target:
+ name: e2e-test-synced-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: example-key
+ remoteRef:
+ key: dummy/path/to/secret
+ property: password
+ namespaceSelector:
+ matchLabels:
+ environment: test
diff --git a/hack/ark/cluster-secret-store.yaml b/hack/ark/cluster-secret-store.yaml
new file mode 100644
index 00000000..c8fbdd11
--- /dev/null
+++ b/hack/ark/cluster-secret-store.yaml
@@ -0,0 +1,18 @@
+# Sample ClusterSecretStore for e2e testing
+# This is a minimal ClusterSecretStore CR that will be discovered by the agent.
+# This is a cluster-scoped resource that can be referenced by ExternalSecrets in any namespace.
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+ name: e2e-test-cluster-secret-store
+ labels:
+ app.kubernetes.io/name: e2e-test
+ app.kubernetes.io/component: cluster-secret-store
+spec:
+ provider:
+ # Fake provider configuration - this won't actually work but allows the CR to be created
+ fake:
+ data:
+ - key: dummy/path/to/secret
+ value: dummy-value
+ version: "1"
diff --git a/hack/ark/conjur-connect-configmap.yaml b/hack/ark/conjur-connect-configmap.yaml
new file mode 100644
index 00000000..0fff2ac5
--- /dev/null
+++ b/hack/ark/conjur-connect-configmap.yaml
@@ -0,0 +1,40 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: conjur-connect-configmap
+ namespace: default
+ labels:
+ conjur.org/name: conjur-connect-configmap
+ app.kubernetes.io/name: authn-k8s
+ app.kubernetes.io/component: conjur-conn-configmap
+ app.kubernetes.io/instance: pet-store-authn-k8s
+ app.kubernetes.io/part-of: app-namespace-config
+ app.kubernetes.io/managed-by: helm
+ helm.sh/chart: authn-k8s-namespace-prep-1.0.0
+data:
+ CONJUR_ACCOUNT: myConjurAccount
+ CONJUR_APPLIANCE_URL: https://conjur.conjur-ns.svc.cluster.local
+ CONJUR_AUTHN_URL: https://conjur.conjur-ns.svc.cluster.local/authn-k8s/my-authenticator-id
+ CONJUR_AUTHENTICATOR_ID: my-authenticator-id
+ CONJUR_SSL_CERTIFICATE: |
+ -----BEGIN CERTIFICATE-----
+ MIIDYTCCAkmgAwIBAgIUTXBJk7Fm+M9kVD5x66jPiwU2JfcwDQYJKoZIhvcNAQEL
+ BQAwQDErMCkGA1UEAwwiY29uanVyLmNvbmp1ci1ucy5zdmMuY2x1c3Rlci5sb2Nh
+ bDERMA8GA1UECgwIRTJFIFRlc3QwHhcNMjYwMTI4MTMwNzA5WhcNMzYwMTI2MTMw
+ NzA5WjBAMSswKQYDVQQDDCJjb25qdXIuY29uanVyLW5zLnN2Yy5jbHVzdGVyLmxv
+ Y2FsMREwDwYDVQQKDAhFMkUgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+ AQoCggEBALdJ9InvV4oOy5LzP/JfZ7iAuM7RIQzeD1fDjm1EEfQcLqSgobH2yZtA
+ YETlj/c2bfJ8Cc2dTJMoTefwofwjA6iR43SBf0e78raKsGSmR3ors9BqaulvgII5
+ Tk3y5jdZxty7UNIGOJP9QoJ4kPQHu37HhSfaA517yQJNCOa4NSLkpHWK155o6Cvf
+ k03M6Szzs5uL7GTK/8IJnl0WSXJezC7lQ8Q+0VVCR6Cq4CzAKm2ZoVCPGkYDZb+Y
+ 2i0aGe8ideO0JgTOsHzXiv5x1DzaEdX0+DhV+aQKbRJYENa2w5LCG0b1Z6Hpyvm6
+ uT0LobEgNLxJ8fOxa3LEq2IryzHFZjUCAwEAAaNTMFEwHQYDVR0OBBYEFHuXVFoC
+ IaF7T3Iic7fKxyKwVhpkMB8GA1UdIwQYMBaAFHuXVFoCIaF7T3Iic7fKxyKwVhpk
+ MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAF/7DwNERFTpucWi
+ roDVME2SH1kTKiemcKzguoeOkDBZd70GbLejy64gWF9nIbcQ9WYxRIuqSI2h0j8d
+ ED9SGQ66nic3uw16GN5IJk21ucFwAJstgQG3kvWPBbSrxMO9TB0pounRozZ5DkZe
+ ZI+vZ4BNOZDT9TAE08xXLrzVhzVDM8DGAydzXUlvscfhYpTe77Cm7yMxmItO7QTA
+ xTrBaamgxM1XYbx+DiS8nTm1U2G3UVACCv9zH6MXDe2DDREBuX1U3skqqbJlsypf
+ 68ckx8fzdxIU5OLx0LZ4QZOR66cHyambDtngoD3iKqDcR1L8EdXajq+IaPRZfcD6
+ VLEtA4Y=
+ -----END CERTIFICATE-----
diff --git a/hack/ark/external-secret.yaml b/hack/ark/external-secret.yaml
new file mode 100644
index 00000000..5f6b72f8
--- /dev/null
+++ b/hack/ark/external-secret.yaml
@@ -0,0 +1,25 @@
+# Sample ExternalSecret for e2e testing
+# This is a minimal ExternalSecret CR that will be discovered by the agent.
+# Note: This requires the External Secrets Operator CRDs to be installed,
+# but does not require a working secrets backend.
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+ name: e2e-test-external-secret
+ namespace: default
+ labels:
+ app.kubernetes.io/name: e2e-test
+ app.kubernetes.io/component: external-secret
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: e2e-test-secret-store
+ kind: SecretStore
+ target:
+ name: e2e-test-synced-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: example-key
+ remoteRef:
+ key: dummy/path/to/secret
+ property: password
diff --git a/hack/ark/secret-store.yaml b/hack/ark/secret-store.yaml
new file mode 100644
index 00000000..883be16c
--- /dev/null
+++ b/hack/ark/secret-store.yaml
@@ -0,0 +1,20 @@
+# Sample SecretStore for e2e testing
+# This is a minimal SecretStore CR that will be discovered by the agent.
+# Note: This requires the External Secrets Operator CRDs to be installed,
+# but does not require a working secrets backend.
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+ name: e2e-test-secret-store
+ namespace: default
+ labels:
+ app.kubernetes.io/name: e2e-test
+ app.kubernetes.io/component: secret-store
+spec:
+ provider:
+ # Fake provider configuration - this won't actually work but allows the CR to be created
+ fake:
+ data:
+ - key: dummy/path/to/secret
+ value: dummy-value
+ version: "1"
diff --git a/hack/ark/test-e2e.sh b/hack/ark/test-e2e.sh
index 3de586ff..b96a43a9 100755
--- a/hack/ark/test-e2e.sh
+++ b/hack/ark/test-e2e.sh
@@ -65,6 +65,40 @@ kubectl create secret generic agent-credentials \
--from-literal=ARK_SUBDOMAIN=$ARK_SUBDOMAIN \
--from-literal=ARK_DISCOVERY_API=$ARK_DISCOVERY_API
+# Create a sample secret in the cluster
+#
+# TODO(wallrj): See if there's an API for checking that this secret has been
+# imported by the backend. For now we have to log into the Disco web UI and
+# search for this secret.
+kubectl create secret generic e2e-sample-secret-$(date '+%s') \
+ --namespace default \
+ --from-literal=username=${RANDOM}
+
+# Create a sample ConfigMap in the cluster that will be discovered by the agent
+#
+# This ConfigMap has the label that matches the default label-selector configured
+# in the ark/configmaps data gatherer (conjur.org/name=conjur-connect-configmap).
+kubectl apply -f "${root_dir}/hack/ark/conjur-connect-configmap.yaml"
+
+# Install External Secrets Operator CRDs and controller
+#
+# This is required for the agent to discover ExternalSecret and SecretStore resources.
+echo "Installing External Secrets Operator..."
+helm repo add external-secrets https://charts.external-secrets.io
+helm repo update
+helm upgrade --install external-secrets \
+ external-secrets/external-secrets \
+ --namespace external-secrets-system \
+ --create-namespace \
+ --wait \
+ --set installCRDs=true
+
+# Create sample External Secrets Operator resources that will be discovered by the agent
+kubectl apply -f "${root_dir}/hack/ark/secret-store.yaml"
+kubectl apply -f "${root_dir}/hack/ark/external-secret.yaml"
+kubectl apply -f "${root_dir}/hack/ark/cluster-secret-store.yaml"
+kubectl apply -f "${root_dir}/hack/ark/cluster-external-secret.yaml"
+
# We use a non-existent tag and omit the `--version` flag, to work around a Helm
# v4 bug. See: https://github.com/helm/helm/issues/31600
helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \
@@ -75,8 +109,13 @@ helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \
--set-json extraArgs='["--log-level=6"]' \
--set pprof.enabled=true \
--set fullnameOverride=disco-agent \
+ --set "imageRegistry=${OCI_BASE}" \
+ --set "imageNamespace=" \
--set "image.digest=${ARK_IMAGE_DIGEST}" \
+ --set config.clusterName="e2e-test-cluster" \
--set config.clusterDescription="A temporary cluster for E2E testing. Contact @wallrj-cyberark." \
+ --set config.period=60s \
+ --set acceptTerms=true \
--set-json "podLabels={\"disco-agent.cyberark.cloud/test-id\": \"${RANDOM}\"}"
kubectl rollout status deployments/disco-agent --namespace "${NAMESPACE}"
@@ -89,17 +128,17 @@ timeout 60 jq -n \
# Query the Prometheus metrics endpoint to ensure it's working.
kubectl get pod \
- --namespace cyberark \
+ --namespace $NAMESPACE \
--selector app.kubernetes.io/name=disco-agent \
--output jsonpath={.items[*].metadata.name} \
- | xargs -I{} kubectl get --raw /api/v1/namespaces/cyberark/pods/{}:8081/proxy/metrics \
+ | xargs -I{} kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/{}:8081/proxy/metrics \
| grep '^process_'
# Query the pprof endpoint to ensure it's working.
kubectl get pod \
- --namespace cyberark \
+ --namespace $NAMESPACE \
--selector app.kubernetes.io/name=disco-agent \
--output jsonpath={.items[*].metadata.name} \
- | xargs -I{} kubectl get --raw /api/v1/namespaces/cyberark/pods/{}:8081/proxy/debug/pprof/cmdline \
+ | xargs -I{} kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/{}:8081/proxy/debug/pprof/cmdline \
| xargs -0
diff --git a/hack/ngts/custom_ca.yaml b/hack/ngts/custom_ca.yaml
new file mode 100644
index 00000000..f0c9849f
--- /dev/null
+++ b/hack/ngts/custom_ca.yaml
@@ -0,0 +1,12 @@
+# These values are used to set a custom CA bundle during the NGTS test.
+# Only used when developing locally, as detected by logic in test-e2e.sh
+
+volumes:
+- name: custom-ca-volume
+ configMap:
+ name: custom-ca
+
+volumeMounts:
+- name: custom-ca-volume
+ mountPath: /etc/ssl/certs
+
diff --git a/hack/ngts/test-e2e.sh b/hack/ngts/test-e2e.sh
new file mode 100755
index 00000000..06aa339a
--- /dev/null
+++ b/hack/ngts/test-e2e.sh
@@ -0,0 +1,146 @@
+#!/usr/bin/env bash
+#
+# Build and deploy the discovery-agent Helm chart for NGTS.
+# Wait for the agent to log a message indicating successful data upload.
+#
+# Prerequisites:
+# * kubectl: https://kubernetes.io/docs/tasks/tools/#kubectl
+# * kind: https://kind.sigs.k8s.io/docs/user/quick-start/
+# * helm: https://helm.sh/docs/intro/install/
+# * jq: https://jqlang.github.io/jq/download/
+# * make: https://www.gnu.org/software/make/
+#
+# You can run `make ngts-test-e2e` which will automatically download all
+# prerequisites and then run this script.
+
+set -o nounset
+set -o errexit
+set -o pipefail
+
+# NGTS API configuration
+: ${NGTS_CLIENT_ID?}
+: ${NGTS_PRIVATE_KEY?}
+: ${NGTS_TSG_URL?}
+
+# The base URL of the OCI registry used for Docker images and Helm charts
+# E.g. ttl.sh/7e6ca67c-96dc-4dea-9437-80b0f3a69fb1
+: ${OCI_BASE?}
+
+# The Kubernetes namespace to install into
+: ${NAMESPACE:=ngts}
+
+# Set to true to use an existing cluster, otherwise a new kind cluster will be created.
+# Note: the cluster will not be deleted after the test completes.
+: ${USE_EXISTING_CLUSTER:=false}
+
+script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+root_dir=$(cd "${script_dir}/../.." && pwd)
+export TERM=dumb
+
+tmp_dir="$(mktemp -d /tmp/jetstack-secure.XXXXX)"
+trap 'rm -rf "${tmp_dir}"' EXIT
+
+pushd "${tmp_dir}"
+> release.env
+make -C "$root_dir" ngts-release \
+ GITHUB_OUTPUT="${tmp_dir}/release.env" \
+ OCI_SIGN_ON_PUSH=false \
+ oci_platforms="" \
+ NGTS_OCI_BASE="${OCI_BASE}"
+cat release.env
+source release.env
+
+if [[ "$USE_EXISTING_CLUSTER" != true ]]; then
+ kind create cluster || true
+fi
+
+kubectl create ns "$NAMESPACE" || true
+
+kubectl delete secret discovery-agent-credentials --namespace "$NAMESPACE" --ignore-not-found
+kubectl create secret generic discovery-agent-credentials \
+ --namespace "$NAMESPACE" \
+ --from-literal=clientID=$NGTS_CLIENT_ID \
+ --from-literal=privatekey.pem="$NGTS_PRIVATE_KEY"
+
+# Create a sample secret in the cluster
+kubectl create secret generic e2e-sample-secret-$(date '+%s') \
+ --namespace default \
+ --from-literal=username=${RANDOM}
+
+# Create values.yaml file for the helm chart
+cat > "${tmp_dir}/values.yaml" < $CA_BUNDLE_FILE
+
+ kubectl create configmap custom-ca --namespace="$NAMESPACE" --from-file=ca_certs.crt="$CA_BUNDLE_FILE"
+
+ # Need to update values.yaml to add the custom CA bundle
+ custom_ca_yaml="${script_dir}/custom_ca.yaml"
+ yq eval-all '. as $item ireduce ({}; . * $item)' "${tmp_dir}/values.yaml" "$custom_ca_yaml" > "${tmp_dir}/values.merged.yaml"
+ mv "${tmp_dir}/values.merged.yaml" "${tmp_dir}/values.yaml"
+fi
+
+# We use a non-existent tag and omit the `--version` flag, to work around a Helm
+# v4 bug. See: https://github.com/helm/helm/issues/31600
+helm upgrade agent "oci://${NGTS_CHART}:NON_EXISTENT_TAG@${NGTS_CHART_DIGEST}" \
+ --install \
+ --wait \
+ --create-namespace \
+ --namespace "$NAMESPACE" \
+ --values "${tmp_dir}/values.yaml"
+
+kubectl rollout status deployments/discovery-agent --namespace "${NAMESPACE}"
+
+# Wait for log message indicating success.
+# Parse logs as JSON using jq to ensure logs are all JSON formatted.
+timeout 120 jq -n \
+ 'inputs | if .msg | test("Data sent successfully") then . | halt_error(0) else . end' \
+ <(kubectl logs deployments/discovery-agent --namespace "${NAMESPACE}" --follow)
+
+# Query the Prometheus metrics endpoint to ensure it's working.
+kubectl get pod \
+ --namespace ${NAMESPACE} \
+ --selector app.kubernetes.io/name=discovery-agent \
+ --output jsonpath={.items[*].metadata.name} \
+ | xargs -I{} kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/{}:8081/proxy/metrics \
+ | grep '^process_'
+
+# Query the pprof endpoint to ensure it's working.
+kubectl get pod \
+ --namespace ${NAMESPACE} \
+ --selector app.kubernetes.io/name=discovery-agent \
+ --output jsonpath={.items[*].metadata.name} \
+ | xargs -I{} kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods/{}:8081/proxy/debug/pprof/cmdline \
+ | xargs -0
+
+# TODO: should call to SCM and verify that certs are actually uploaded
diff --git a/internal/cyberark/client.go b/internal/cyberark/client.go
index 18c1a31c..92710296 100644
--- a/internal/cyberark/client.go
+++ b/internal/cyberark/client.go
@@ -49,24 +49,23 @@ func LoadClientConfigFromEnvironment() (ClientConfig, error) {
// NewDatauploadClient initializes and returns a new CyberArk Data Upload client.
// It performs service discovery to find the necessary API endpoints and authenticates
// using the provided client configuration.
-func NewDatauploadClient(ctx context.Context, httpClient *http.Client, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
- discoveryClient := servicediscovery.New(httpClient)
- serviceMap, err := discoveryClient.DiscoverServices(ctx, cfg.Subdomain)
- if err != nil {
- return nil, err
- }
+func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMap *servicediscovery.Services, tenantUUID string, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
identityAPI := serviceMap.Identity.API
if identityAPI == "" {
return nil, errors.New("service discovery returned an empty identity API")
}
- identityClient := identity.New(httpClient, identityAPI, cfg.Subdomain)
- err = identityClient.LoginUsernamePassword(ctx, cfg.Username, []byte(cfg.Secret))
- if err != nil {
- return nil, err
- }
+
discoveryAPI := serviceMap.DiscoveryContext.API
if discoveryAPI == "" {
return nil, errors.New("service discovery returned an empty discovery API")
}
- return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil
+
+ identityClient := identity.New(httpClient, identityAPI, cfg.Subdomain)
+
+ err := identityClient.LoginUsernamePassword(ctx, cfg.Username, []byte(cfg.Secret))
+ if err != nil {
+ return nil, err
+ }
+
+ return dataupload.New(httpClient, discoveryAPI, tenantUUID, identityClient.AuthenticateRequest), nil
}
diff --git a/internal/cyberark/client_test.go b/internal/cyberark/client_test.go
index 3c945fc8..9d69da20 100644
--- a/internal/cyberark/client_test.go
+++ b/internal/cyberark/client_test.go
@@ -2,7 +2,8 @@ package cyberark_test
import (
"crypto/x509"
- "errors"
+ "os"
+ "strings"
"testing"
"github.com/jetstack/venafi-connection-lib/http_client"
@@ -13,6 +14,7 @@ import (
"github.com/jetstack/preflight/internal/cyberark"
"github.com/jetstack/preflight/internal/cyberark/dataupload"
"github.com/jetstack/preflight/internal/cyberark/servicediscovery"
+ arktesting "github.com/jetstack/preflight/internal/cyberark/testing"
"github.com/jetstack/preflight/pkg/testutil"
"github.com/jetstack/preflight/pkg/version"
@@ -32,13 +34,21 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
Secret: "somepassword",
}
- cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg)
+ discoveryClient := servicediscovery.New(httpClient, cfg.Subdomain)
+
+ serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context())
+ if err != nil {
+ t.Fatalf("failed to discover mock services: %v", err)
+ }
+
+ cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg)
require.NoError(t, err)
err = cl.PutSnapshot(ctx, dataupload.Snapshot{
- ClusterID: "success-cluster-id",
+ ClusterID: "ffffffff-ffff-ffff-ffff-ffffffffffff",
AgentVersion: version.PreflightVersion,
})
+
require.NoError(t, err)
}
@@ -55,6 +65,15 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
// go test ./internal/cyberark \
// -v -count 1 -run TestCyberArkClient_PutSnapshot_RealAPI -args -testing.v 6
func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
+ if strings.ToLower(os.Getenv("ARK_LIVE_TEST")) != "true" {
+ t.Skip("set ARK_LIVE_TEST=true to run this test against the live service")
+ return
+ }
+
+ arktesting.SkipIfNoEnv(t)
+
+ t.Log("This test runs against a live service and has been known to flake. If you see timeout issues it's possible that the test is flaking and it could be unrelated to your changes.")
+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)
@@ -62,19 +81,22 @@ func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
cfg, err := cyberark.LoadClientConfigFromEnvironment()
+ require.NoError(t, err)
+
+ discoveryClient := servicediscovery.New(httpClient, cfg.Subdomain)
+
+ serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context())
if err != nil {
- if errors.Is(err, cyberark.ErrMissingEnvironmentVariables) {
- t.Skipf("Skipping: %s", err)
- }
- require.NoError(t, err)
+ t.Fatalf("failed to discover services: %v", err)
}
- cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg)
+ cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg)
require.NoError(t, err)
err = cl.PutSnapshot(ctx, dataupload.Snapshot{
- ClusterID: "bb068932-c80d-460d-88df-34bc7f3f3297",
+ ClusterID: "ffffffff-ffff-ffff-ffff-ffffffffffff",
AgentVersion: version.PreflightVersion,
})
+
require.NoError(t, err)
}
diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go
index b9ccb5f5..27e8e856 100644
--- a/internal/cyberark/dataupload/dataupload.go
+++ b/internal/cyberark/dataupload/dataupload.go
@@ -15,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
arkapi "github.com/jetstack/preflight/internal/cyberark/api"
+ "github.com/jetstack/preflight/internal/cyberark/identity"
"github.com/jetstack/preflight/pkg/version"
)
@@ -33,13 +34,19 @@ type CyberArkClient struct {
baseURL string
httpClient *http.Client
- authenticateRequest func(req *http.Request) error
+ tenantUUID string
+
+ authenticateRequest identity.RequestAuthenticator
}
-func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient {
+// New creates a new CyberArkClient. The tenant UUID is best sourced from service discovery along with the base URL.
+func New(httpClient *http.Client, baseURL string, tenantUUID string, authenticateRequest identity.RequestAuthenticator) *CyberArkClient {
return &CyberArkClient{
- baseURL: baseURL,
- httpClient: httpClient,
+ baseURL: baseURL,
+ httpClient: httpClient,
+
+ tenantUUID: tenantUUID,
+
authenticateRequest: authenticateRequest,
}
}
@@ -57,11 +64,30 @@ type Snapshot struct {
ClusterDescription string `json:"cluster_description,omitempty"`
// K8SVersion is the version of Kubernetes which the cluster is running.
K8SVersion string `json:"k8s_version"`
+ // OIDCConfig contains OIDC configuration data from the API server's
+ // `/.well-known/openid-configuration` endpoint
+ OIDCConfig map[string]any `json:"openid_configuration,omitempty"`
+ // OIDCConfigError contains any error encountered while fetching the OIDC configuration
+ OIDCConfigError string `json:"openid_configuration_error,omitempty"`
+ // JWKS contains JWKS data from the API server's `/openid/v1/jwks` endpoint
+ JWKS map[string]any `json:"jwks,omitempty"`
+ // JWKSError contains any error encountered while fetching the JWKS
+ JWKSError string `json:"jwks_error,omitempty"`
// Secrets is a list of Secret resources in the cluster. Not all Secret
// types are included and only a subset of the Secret data is included.
Secrets []runtime.Object `json:"secrets"`
// ServiceAccounts is a list of ServiceAccount resources in the cluster.
ServiceAccounts []runtime.Object `json:"serviceaccounts"`
+ // ConfigMaps is a list of ConfigMap resources in the cluster.
+ ConfigMaps []runtime.Object `json:"configmaps"`
+ // ExternalSecrets is a list of ExternalSecret resources in the cluster.
+ ExternalSecrets []runtime.Object `json:"externalsecrets"`
+ // SecretStores is a list of SecretStore resources in the cluster.
+ SecretStores []runtime.Object `json:"secretstores"`
+ // ClusterExternalSecrets is a list of ClusterExternalSecret resources in the cluster.
+ ClusterExternalSecrets []runtime.Object `json:"clusterexternalsecrets"`
+ // ClusterSecretStores is a list of ClusterSecretStore resources in the cluster.
+ ClusterSecretStores []runtime.Object `json:"clustersecretstores"`
// Roles is a list of Role resources in the cluster.
Roles []runtime.Object `json:"roles"`
// ClusterRoles is a list of ClusterRole resources in the cluster.
@@ -91,13 +117,6 @@ type Snapshot struct {
// has been received intact.
// Read [Checking object integrity for data uploads in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html),
// to learn more.
-//
-// TODO(wallrj): There is a bug in the AWS backend:
-// [S3 Presigned PutObjectCommand URLs ignore Sha256 Hash when uploading](https://github.com/aws/aws-sdk/issues/480)
-// ...which means that the `x-amz-checksum-sha256` request header is optional.
-// If you omit that header, it is possible to PUT any data.
-// There is a work around listed in that issue which we have shared with the
-// CyberArk API team.
func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) error {
if snapshot.ClusterID == "" {
return fmt.Errorf("programmer mistake: the snapshot cluster ID cannot be left empty")
@@ -108,10 +127,12 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(snapshot); err != nil {
return err
}
+
checksum := hash.Sum(nil)
checksumHex := hex.EncodeToString(checksum)
checksumBase64 := base64.StdEncoding.EncodeToString(checksum)
- presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, checksumHex, snapshot.ClusterID)
+
+ presignedUploadURL, username, err := c.retrievePresignedUploadURL(ctx, checksumHex, snapshot.ClusterID, int64(encodedBody.Len()))
if err != nil {
return fmt.Errorf("while retrieving snapshot upload URL: %s", err)
}
@@ -121,7 +142,21 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
if err != nil {
return err
}
+
req.Header.Set("X-Amz-Checksum-Sha256", checksumBase64)
+ req.Header.Set("X-Amz-Server-Side-Encryption", "AES256")
+
+ q := url.Values{}
+
+ q.Add("agent_version", snapshot.AgentVersion)
+ q.Add("tenant_id", c.tenantUUID)
+ q.Add("upload_type", "k8s_snapshot")
+ q.Add("uploader_id", snapshot.ClusterID)
+ q.Add("username", username)
+ q.Add("vendor", "k8s")
+
+ req.Header.Set("X-Amz-Tagging", q.Encode())
+
version.SetUserAgent(req)
res, err := c.httpClient.Do(req)
@@ -141,36 +176,58 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
return nil
}
-func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, clusterID string) (string, error) {
+const SigV4Support = "sigv4"
+
+// RetrievePresignedUploadURLRequest is the JSON body sent to the inventory API to request a presigned upload URL.
+type RetrievePresignedUploadURLRequest struct {
+ ClusterID string `json:"cluster_id"`
+ Checksum string `json:"checksum_sha256"`
+
+ // AgentVersion is the v-prefixed version of the agent uploading the snapshot.
+ // Note that some versions of the backend rely on this version being v-prefixed semver,
+ // but that requirement was dropped in favour of the SigV4Support field below.
+ AgentVersion string `json:"agent_version"`
+
+ // FileSize is the size of the data we'll upload in bytes
+ FileSize int64 `json:"file_size"`
+
+ // SignatureVersion allows the agent to specify which version of AWS's signature scheme it expects for the presigned URL.
+ // Older versions of the agent will not send this. All versions which support this field will unconditionally set it to the
+ // value of SigV4Support, so the backend can rely on this field being set.
+ SignatureVersion string `json:"signature_version"`
+}
+
+func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, clusterID string, fileSize int64) (string, string, error) {
uploadURL, err := url.JoinPath(c.baseURL, apiPathSnapshotLinks)
if err != nil {
- return "", err
+ return "", "", err
}
- request := struct {
- ClusterID string `json:"cluster_id"`
- Checksum string `json:"checksum_sha256"`
- AgentVersion string `json:"agent_version"`
- }{
- ClusterID: clusterID,
- Checksum: checksum,
- AgentVersion: version.PreflightVersion,
+ request := RetrievePresignedUploadURLRequest{
+ ClusterID: clusterID,
+ Checksum: checksum,
+ AgentVersion: version.PreflightVersion,
+ FileSize: fileSize,
+ SignatureVersion: SigV4Support,
}
encodedBody := &bytes.Buffer{}
if err := json.NewEncoder(encodedBody).Encode(request); err != nil {
- return "", err
+ return "", "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, encodedBody)
if err != nil {
- return "", err
+ return "", "", err
}
req.Header.Set("Content-Type", "application/json")
- if err := c.authenticateRequest(req); err != nil {
- return "", fmt.Errorf("failed to authenticate request: %s", err)
+
+ username, err := c.authenticateRequest(req)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to authenticate request: %s", err)
}
+
version.SetUserAgent(req)
// Add telemetry headers
@@ -178,7 +235,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
res, err := c.httpClient.Do(req)
if err != nil {
- return "", err
+ return "", "", err
}
defer res.Body.Close()
@@ -187,7 +244,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
if len(body) == 0 {
body = []byte(``)
}
- return "", fmt.Errorf("received response with status code %d: %s", code, bytes.TrimSpace(body))
+ return "", "", fmt.Errorf("received response with status code %d: %s", code, bytes.TrimSpace(body))
}
response := struct {
@@ -196,11 +253,11 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
if err := json.NewDecoder(io.LimitReader(res.Body, maxRetrievePresignedUploadURLBodySize)).Decode(&response); err != nil {
if err == io.ErrUnexpectedEOF {
- return "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
+ return "", "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
}
- return "", fmt.Errorf("failed to parse JSON from otherwise successful request to start data upload: %s", err)
+ return "", "", fmt.Errorf("failed to parse JSON from otherwise successful request to start data upload: %s", err)
}
- return response.URL, nil
+ return response.URL, username, nil
}
diff --git a/internal/cyberark/dataupload/dataupload_test.go b/internal/cyberark/dataupload/dataupload_test.go
index 1f2ff636..d78c4bf3 100644
--- a/internal/cyberark/dataupload/dataupload_test.go
+++ b/internal/cyberark/dataupload/dataupload_test.go
@@ -10,6 +10,7 @@ import (
"k8s.io/klog/v2/ktesting"
"github.com/jetstack/preflight/internal/cyberark/dataupload"
+ "github.com/jetstack/preflight/internal/cyberark/identity"
"github.com/jetstack/preflight/pkg/version"
_ "k8s.io/klog/v2/ktesting/init"
@@ -19,23 +20,23 @@ import (
// mock API server. The mock server is configured to return different responses
// based on the cluster ID and bearer token used in the request.
func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
- setToken := func(token string) func(*http.Request) error {
- return func(req *http.Request) error {
+ setToken := func(token string) identity.RequestAuthenticator {
+ return func(req *http.Request) (string, error) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
- return nil
+ return "foo@example.com", nil // set a dummy username for testing purposes; the actual value is not important for these tests
}
}
tests := []struct {
name string
snapshot dataupload.Snapshot
- authenticate func(req *http.Request) error
+ authenticate identity.RequestAuthenticator
requireFn func(t *testing.T, err error)
}{
{
name: "successful upload",
snapshot: dataupload.Snapshot{
- ClusterID: "success-cluster-id",
+ ClusterID: "ffffffff-ffff-ffff-ffff-ffffffffffff",
AgentVersion: version.PreflightVersion,
},
authenticate: setToken("success-token"),
@@ -96,7 +97,7 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
datauploadAPIBaseURL, httpClient := dataupload.MockDataUploadServer(t)
- cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, tc.authenticate)
+ cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, "test-tenant-uuid", tc.authenticate)
err := cyberArkClient.PutSnapshot(ctx, tc.snapshot)
tc.requireFn(t, err)
diff --git a/internal/cyberark/dataupload/mock.go b/internal/cyberark/dataupload/mock.go
index d84ea1d4..28403775 100644
--- a/internal/cyberark/dataupload/mock.go
+++ b/internal/cyberark/dataupload/mock.go
@@ -2,13 +2,17 @@ package dataupload
import (
"bytes"
+ "crypto/rand"
"crypto/sha256"
"encoding/base64"
+ "encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
+ "net/url"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -22,12 +26,22 @@ import (
const (
successBearerToken = "success-token"
- successClusterID = "success-cluster-id"
+ successClusterID = "ffffffff-ffff-ffff-ffff-ffffffffffff"
)
+type uploadValues struct {
+ ClusterID string
+ FileSize int64
+}
+
type mockDataUploadServer struct {
t testing.TB
serverURL string
+
+ mux *http.ServeMux
+
+ expectedUploadValues map[string]uploadValues
+ expectedUploadValuesMutex sync.Mutex
}
// MockDataUploadServer starts a server which mocks the CyberArk
@@ -45,13 +59,24 @@ type mockDataUploadServer struct {
// responses.
func MockDataUploadServer(t testing.TB) (string, *http.Client) {
mux := http.NewServeMux()
- server := httptest.NewTLSServer(mux)
- t.Cleanup(server.Close)
mds := &mockDataUploadServer{
- t: t,
- serverURL: server.URL,
+ t: t,
+
+ expectedUploadValues: make(map[string]uploadValues),
}
- mux.Handle("/", mds)
+
+ mux.HandleFunc("POST "+apiPathSnapshotLinks, mds.handleSnapshotLinks)
+
+ // The path includes random data to ensure that each request is treated separately by the mock server, allowing us to track data across calls.
+ // It also ensures that the client isn't using some pre-saved path and is actually using the presigned URL returned by the mock server in the previous step, which is important for test validity.
+ mux.HandleFunc("PUT /presigned-upload/{randData}", mds.handlePresignedUpload)
+
+ server := httptest.NewTLSServer(mds)
+ t.Cleanup(server.Close)
+
+ mds.mux = mux
+ mds.serverURL = server.URL
+
httpClient := server.Client()
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
return server.URL, httpClient
@@ -59,25 +84,23 @@ func MockDataUploadServer(t testing.TB) (string, *http.Client) {
func (mds *mockDataUploadServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mds.t.Log(r.Method, r.RequestURI)
- switch r.URL.Path {
- case apiPathSnapshotLinks:
- mds.handleSnapshotLinks(w, r)
- return
- case "/presigned-upload":
- mds.handlePresignedUpload(w, r)
- return
- default:
- w.WriteHeader(http.StatusNotFound)
- }
+
+ mds.mux.ServeHTTP(w, r)
}
-func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusMethodNotAllowed)
- _, _ = w.Write([]byte(`{"message":"method not allowed"}`))
- return
+// randHex reads 8 random bytes and returns them as a hex string. It is used to generate
+// unique paths per-request to ensure that file size is tracked across calls.
+func randHex() string {
+ b := make([]byte, 8)
+ _, err := rand.Read(b)
+ if err != nil {
+ panic("failed to read random bytes: " + err.Error())
}
+ return hex.EncodeToString(b)
+}
+
+func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("User-Agent") != version.UserAgent() {
http.Error(w, "should set user agent on all requests", http.StatusInternalServerError)
return
@@ -99,18 +122,21 @@ func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *h
return
}
+ var req RetrievePresignedUploadURLRequest
+
decoder := json.NewDecoder(r.Body)
- var req struct {
- ClusterID string `json:"cluster_id"`
- Checksum string `json:"checksum_sha256"`
- AgentVersion string `json:"agent_version"`
- }
decoder.DisallowUnknownFields()
+
if err := decoder.Decode(&req); err != nil {
http.Error(w, `{"error": "Invalid request format"}`, http.StatusBadRequest)
return
}
+ if req.SignatureVersion != SigV4Support {
+ http.Error(w, fmt.Sprintf("post body does not set signature_version=%s", SigV4Support), http.StatusInternalServerError)
+ return
+ }
+
if req.AgentVersion != version.PreflightVersion {
http.Error(w, fmt.Sprintf("post body contains unexpected agent version: %s", req.AgentVersion), http.StatusInternalServerError)
return
@@ -135,10 +161,33 @@ func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *h
return
}
+ if req.FileSize <= 0 {
+ http.Error(w, "file size must be greater than 0", http.StatusInternalServerError)
+ return
+ }
+
+ randomData := randHex()
+
+ mds.expectedUploadValuesMutex.Lock()
+ defer mds.expectedUploadValuesMutex.Unlock()
+
+ uploadValues := uploadValues{
+ ClusterID: req.ClusterID,
+ FileSize: req.FileSize,
+ }
+
+ mds.expectedUploadValues[randomData] = uploadValues
+
+ presignedURL, err := url.JoinPath(mds.serverURL, "presigned-upload", randomData)
+ if err != nil {
+ http.Error(w, "failed to generate presigned URL", http.StatusInternalServerError)
+ mds.t.Logf("failed to generate presigned URL: %v", err)
+ return
+ }
+
// Write response body
- w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
- presignedURL := mds.serverURL + "/presigned-upload"
+ w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(struct {
URL string `json:"url"`
}{presignedURL})
@@ -155,9 +204,18 @@ const amzExampleChecksumError = `
`
func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPut {
- w.WriteHeader(http.StatusMethodNotAllowed)
- _, _ = w.Write([]byte(`{"message":"method not allowed"}`))
+ randData := r.PathValue("randData")
+ if randData == "" {
+ http.Error(w, "missing randData in path; should match that returned in presigned url", http.StatusInternalServerError)
+ return
+ }
+
+ mds.expectedUploadValuesMutex.Lock()
+ uploadValues, ok := mds.expectedUploadValues[randData]
+ mds.expectedUploadValuesMutex.Unlock()
+
+ if !ok {
+ http.Error(w, "didn't find a prior call to generate presigned URL", http.StatusInternalServerError)
return
}
@@ -178,9 +236,65 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r
return
}
+ sseHeader := r.Header.Get("X-Amz-Server-Side-Encryption")
+ if sseHeader != "AES256" {
+ http.Error(w, "should set x-amz-server-side-encryption header to AES256 on all requests", http.StatusInternalServerError)
+ return
+ }
+
+ taggingHeader := r.Header.Get("X-Amz-Tagging")
+ if taggingHeader == "" {
+ http.Error(w, "should set x-amz-tagging header on all requests", http.StatusInternalServerError)
+ return
+ }
+
+ tags, err := url.ParseQuery(taggingHeader)
+ if err != nil {
+ http.Error(w, "x-amz-tagging header should be encoded as a valid query string", http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("agent_version") != version.PreflightVersion {
+ http.Error(w, fmt.Sprintf("x-amz-tagging should contain an agent_version tag with value %s", version.PreflightVersion), http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("tenant_id") == "" {
+ // TODO: if we change setup a bit, we can check the tenant_id matches the expected tenant_id from the test config, but for now, just check it's set
+ http.Error(w, "x-amz-tagging should contain a tenant_id tag", http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("upload_type") != "k8s_snapshot" {
+ http.Error(w, "x-amz-tagging should contain an upload_type tag with value k8s_snapshot", http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("uploader_id") != uploadValues.ClusterID {
+ http.Error(w, "x-amz-tagging should contain an uploader_id tag which matches the cluster ID sent in the RetrievePresignedUploadURL request", http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("username") == "" {
+ // TODO: if we change setup a bit, we can check the username matches the expected username from the test config
+ // but for now, just check it's set
+ http.Error(w, "x-amz-tagging should contain a username tag", http.StatusInternalServerError)
+ return
+ }
+
+ if tags.Get("vendor") != "k8s" {
+ http.Error(w, "x-amz-tagging should contain a vendor tag with value k8s", http.StatusInternalServerError)
+ return
+ }
+
body, err := io.ReadAll(r.Body)
require.NoError(mds.t, err)
+ if uploadValues.FileSize != int64(len(body)) {
+ http.Error(w, fmt.Sprintf("file size in request body should match that sent in RetrievePresignedUploadURL request; expected %d, got %d", uploadValues.FileSize, len(body)), http.StatusInternalServerError)
+ return
+ }
+
hash := sha256.New()
_, err = hash.Write(body)
require.NoError(mds.t, err)
diff --git a/internal/cyberark/identity/advance_authentication_test.go b/internal/cyberark/identity/advance_authentication_test.go
index 9340da30..0c17cd0b 100644
--- a/internal/cyberark/identity/advance_authentication_test.go
+++ b/internal/cyberark/identity/advance_authentication_test.go
@@ -131,13 +131,18 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
return
}
- if len(client.tokenCached) == 0 {
+ if client.tokenCached.Username != testSpec.username {
+ t.Errorf("expected username %s to be set on cached token after authentication but got %q", testSpec.username, client.tokenCached.Username)
+ return
+ }
+
+ if len(client.tokenCached.Token) == 0 {
t.Errorf("expected token for %s to be set to %q but wasn't found", testSpec.username, mockSuccessfulStartAuthenticationToken)
return
}
- if client.tokenCached != mockSuccessfulStartAuthenticationToken {
- t.Errorf("expected token for %s to be set to %q but was set to %q", testSpec.username, mockSuccessfulStartAuthenticationToken, client.tokenCached)
+ if client.tokenCached.Token != mockSuccessfulStartAuthenticationToken {
+ t.Errorf("expected token for %s to be set to %q but was set to %q", testSpec.username, mockSuccessfulStartAuthenticationToken, client.tokenCached.Token)
}
})
}
diff --git a/internal/cyberark/identity/authenticated_http_client.go b/internal/cyberark/identity/authenticated_http_client.go
index 901d14db..c20d5bfb 100644
--- a/internal/cyberark/identity/authenticated_http_client.go
+++ b/internal/cyberark/identity/authenticated_http_client.go
@@ -5,15 +5,19 @@ import (
"net/http"
)
-func (c *Client) AuthenticateRequest(req *http.Request) error {
+type RequestAuthenticator func(req *http.Request) (string, error)
+
+// AuthenticateRequest is a helper function that adds the Authorization header to an HTTP request using a cached token.
+// It sets the Header directly, and if successful returns the username corresponding to the token.
+func (c *Client) AuthenticateRequest(req *http.Request) (string, error) {
c.tokenCachedMutex.Lock()
defer c.tokenCachedMutex.Unlock()
- if len(c.tokenCached) == 0 {
- return fmt.Errorf("no token cached")
+ if len(c.tokenCached.Token) == 0 {
+ return "", fmt.Errorf("no token cached")
}
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(c.tokenCached)))
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.tokenCached.Token))
- return nil
+ return c.tokenCached.Username, nil
}
diff --git a/internal/cyberark/identity/cmd/testidentity/main.go b/internal/cyberark/identity/cmd/testidentity/main.go
index 8729cfbe..0a8df80b 100644
--- a/internal/cyberark/identity/cmd/testidentity/main.go
+++ b/internal/cyberark/identity/cmd/testidentity/main.go
@@ -50,8 +50,8 @@ func run(ctx context.Context) error {
var rootCAs *x509.CertPool
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
- sdClient := servicediscovery.New(httpClient)
- services, err := sdClient.DiscoverServices(ctx, subdomain)
+ sdClient := servicediscovery.New(httpClient, subdomain)
+ services, _, err := sdClient.DiscoverServices(ctx)
if err != nil {
return fmt.Errorf("while performing service discovery: %s", err)
}
diff --git a/internal/cyberark/identity/identity.go b/internal/cyberark/identity/identity.go
index e88ba0c1..c245c978 100644
--- a/internal/cyberark/identity/identity.go
+++ b/internal/cyberark/identity/identity.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"sync"
+ "time"
"k8s.io/klog/v2"
@@ -51,7 +52,7 @@ var (
)
// startAuthenticationRequestBody is the body sent to the StartAuthentication endpoint in CyberArk Identity;
-// see https://api-docs.cyberark.com/docs/identity-api-reference/authentication-and-authorization/operations/create-a-security-start-authentication
+// see https://api-docs.cyberark.com/identity-docs-api/docs/security-api#/Login/start-authentication
type startAuthenticationRequestBody struct {
// TenantID is the internal ID of the tenant containing the user attempting to log in. In testing,
// it seems that the subdomain works in this field.
@@ -135,6 +136,7 @@ type startAuthenticationMechanism struct {
// advanceAuthenticationRequestBody is a request body for the AdvanceAuthentication call to CyberArk Identity,
// which should usually be obtained by making requests to StartAuthentication first.
// WARNING: This struct can hold secret data (a user's password)
+// See: https://api-docs.cyberark.com/identity-docs-api/docs/security-api#/Login/advance-authentication
type advanceAuthenticationRequestBody struct {
// Action is a string identifying how we're intending to log in; for username/password, this is
// set to "Answer" to indicate that the password is held in the Answer field
@@ -180,10 +182,14 @@ type Client struct {
tokenCached token
tokenCachedMutex sync.Mutex
+ tokenCachedTime time.Time
}
// token is a wrapper type for holding auth tokens we want to cache.
-type token string
+type token struct {
+ Username string
+ Token string
+}
// New returns an initialized CyberArk Identity client using a default service discovery client.
func New(httpClient *http.Client, baseURL string, subdomain string) *Client {
@@ -192,7 +198,7 @@ func New(httpClient *http.Client, baseURL string, subdomain string) *Client {
baseURL: baseURL,
subdomain: subdomain,
- tokenCached: "",
+ tokenCached: token{},
tokenCachedMutex: sync.Mutex{},
}
}
@@ -202,12 +208,23 @@ func New(httpClient *http.Client, baseURL string, subdomain string) *Client {
// Tokens are cached internally and are not directly accessible to code; use Client.AuthenticateRequest to add credentials
// to an *http.Request.
func (c *Client) LoginUsernamePassword(ctx context.Context, username string, password []byte) error {
+ // note: we hold the mutex for the whole login attempt to ensure that only one login attempt can be in flight at once,
+ // and to ensure that the token cache is correctly updated
+ c.tokenCachedMutex.Lock()
+ defer c.tokenCachedMutex.Unlock()
+
defer func() {
for i := range password {
password[i] = 0x00
}
}()
+ if time.Since(c.tokenCachedTime) < 15*time.Minute && c.tokenCached.Username == username {
+ // If the cached token is recent and for the same username, we can reuse it.
+ klog.FromContext(ctx).V(2).Info("reusing cached token for user", "username", username)
+ return nil
+ }
+
advanceRequestBody, err := c.doStartAuthentication(ctx, username)
if err != nil {
return err
@@ -227,7 +244,7 @@ func (c *Client) LoginUsernamePassword(ctx context.Context, username string, pas
// It returns a partially initialized advanceAuthenticationRequestBody ready to send to the server to complete
// the login. As this function doesn't have access to the password, it must be added to the returned request body
// by the caller before being used as a request to AdvanceAuthentication.
-// See https://api-docs.cyberark.com/docs/identity-api-reference/authentication-and-authorization/operations/create-a-security-start-authentication
+// See https://api-docs.cyberark.com/identity-docs-api/docs/security-api#/Login/start-authentication
func (c *Client) doStartAuthentication(ctx context.Context, username string) (advanceAuthenticationRequestBody, error) {
response := advanceAuthenticationRequestBody{}
@@ -342,6 +359,7 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
// doAdvanceAuthentication performs the second step of the login process, sending the password to the server
// and receiving a token in response.
+// See: https://api-docs.cyberark.com/identity-docs-api/docs/security-api#/Login/advance-authentication
func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, password *[]byte, requestBody advanceAuthenticationRequestBody) error {
if password == nil {
return fmt.Errorf("password must not be nil; this is a programming error")
@@ -402,11 +420,12 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, p
klog.FromContext(ctx).Info("successfully completed AdvanceAuthentication request to CyberArk Identity; login complete", "username", username)
- c.tokenCachedMutex.Lock()
-
- c.tokenCached = token(advanceAuthResponse.Result.Token)
-
- c.tokenCachedMutex.Unlock()
+ // NB: This assumes we already hold the token cache mutex, which we do in LoginUsernamePassword, so this is safe.
+ c.tokenCachedTime = time.Now()
+ c.tokenCached = token{
+ Username: username,
+ Token: advanceAuthResponse.Result.Token,
+ }
return nil
}
diff --git a/internal/cyberark/identity/identity_test.go b/internal/cyberark/identity/identity_test.go
index 732805e7..0915f46c 100644
--- a/internal/cyberark/identity/identity_test.go
+++ b/internal/cyberark/identity/identity_test.go
@@ -53,7 +53,7 @@ func TestLoginUsernamePassword_RealAPI(t *testing.T) {
arktesting.SkipIfNoEnv(t)
subdomain := os.Getenv("ARK_SUBDOMAIN")
httpClient := http.DefaultClient
- services, err := servicediscovery.New(httpClient).DiscoverServices(t.Context(), subdomain)
+ services, _, err := servicediscovery.New(httpClient, subdomain).DiscoverServices(t.Context())
require.NoError(t, err)
loginUsernamePasswordTests(t, func(t testing.TB) inputs {
diff --git a/internal/cyberark/servicediscovery/discovery.go b/internal/cyberark/servicediscovery/discovery.go
index e838e507..93598d5c 100644
--- a/internal/cyberark/servicediscovery/discovery.go
+++ b/internal/cyberark/servicediscovery/discovery.go
@@ -9,6 +9,8 @@ import (
"net/url"
"os"
"path"
+ "sync"
+ "time"
arkapi "github.com/jetstack/preflight/internal/cyberark/api"
"github.com/jetstack/preflight/pkg/version"
@@ -35,21 +37,34 @@ const (
// users to fetch URLs for various APIs available in CyberArk. This client is specialised to
// fetch only API endpoints, since only API endpoints are required by the Venafi Kubernetes Agent currently.
type Client struct {
- client *http.Client
- baseURL string
+ client *http.Client
+ baseURL string
+ subdomain string
+
+ cachedResponse *Services
+ cachedTenantID string
+ cachedResponseTime time.Time
+ cachedResponseMutex sync.Mutex
}
// New creates a new CyberArk Service Discovery client. If the ARK_DISCOVERY_API
// environment variable is set, it is used as the base URL for the service
// discovery API. Otherwise, the production URL is used.
-func New(httpClient *http.Client) *Client {
+func New(httpClient *http.Client, subdomain string) *Client {
baseURL := os.Getenv("ARK_DISCOVERY_API")
if baseURL == "" {
baseURL = ProdDiscoveryAPIBaseURL
}
+
client := &Client{
- client: httpClient,
- baseURL: baseURL,
+ client: httpClient,
+ baseURL: baseURL,
+ subdomain: subdomain,
+
+ cachedResponse: nil,
+ cachedTenantID: "",
+ cachedResponseTime: time.Time{},
+ cachedResponseMutex: sync.Mutex{},
}
return client
@@ -93,19 +108,30 @@ type Services struct {
DiscoveryContext ServiceEndpoint
}
-// DiscoverServices fetches from the service discovery service for a given subdomain
+// DiscoverServices fetches from the service discovery service for the configured subdomain
// and parses the CyberArk Identity API URL and Inventory API URL.
-func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, error) {
+// It also returns the Tenant ID UUID corresponding to the subdomain.
+func (c *Client) DiscoverServices(ctx context.Context) (*Services, string, error) {
+ c.cachedResponseMutex.Lock()
+ defer c.cachedResponseMutex.Unlock()
+
+ if c.cachedResponse != nil && time.Since(c.cachedResponseTime) < 1*time.Hour {
+ return c.cachedResponse, c.cachedTenantID, nil
+ }
+
u, err := url.Parse(c.baseURL)
if err != nil {
- return nil, fmt.Errorf("invalid base URL for service discovery: %w", err)
+ return nil, "", fmt.Errorf("invalid base URL for service discovery: %w", err)
}
+
u.Path = path.Join(u.Path, "api/public/tenant-discovery")
- u.RawQuery = url.Values{"bySubdomain": []string{subdomain}}.Encode()
+ u.RawQuery = url.Values{"bySubdomain": []string{c.subdomain}}.Encode()
+
endpoint := u.String()
+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
- return nil, fmt.Errorf("failed to initialise request to %s: %s", endpoint, err)
+ return nil, "", fmt.Errorf("failed to initialise request to %s: %s", endpoint, err)
}
request.Header.Set("Accept", "application/json")
@@ -114,7 +140,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
arkapi.SetTelemetryRequestHeader(request)
resp, err := c.client.Do(request)
if err != nil {
- return nil, fmt.Errorf("failed to perform HTTP request: %s", err)
+ return nil, "", fmt.Errorf("failed to perform HTTP request: %s", err)
}
defer resp.Body.Close()
@@ -123,19 +149,19 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
// a 404 error is returned with an empty JSON body "{}" if the subdomain is unknown; at the time of writing, we haven't observed
// any other errors and so we can't special case them
if resp.StatusCode == http.StatusNotFound {
- return nil, fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain)
+ return nil, "", fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", c.subdomain)
}
- return nil, fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
+ return nil, "", fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
}
var discoveryResp DiscoveryResponse
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&discoveryResp)
if err != nil {
if err == io.ErrUnexpectedEOF {
- return nil, fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
+ return nil, "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
}
- return nil, fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
+ return nil, "", fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
}
var identityAPI, discoveryContextAPI string
for _, svc := range discoveryResp.Services {
@@ -158,13 +184,19 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
}
if identityAPI == "" {
- return nil, fmt.Errorf("didn't find %s in service discovery response, "+
+ return nil, "", fmt.Errorf("didn't find %s in service discovery response, "+
"which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName)
}
//TODO: Should add a check for discoveryContextAPI too?
- return &Services{
+ services := &Services{
Identity: ServiceEndpoint{API: identityAPI},
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
- }, nil
+ }
+
+ c.cachedResponse = services
+ c.cachedTenantID = discoveryResp.TenantID
+ c.cachedResponseTime = time.Now()
+
+ return services, discoveryResp.TenantID, nil
}
diff --git a/internal/cyberark/servicediscovery/discovery_test.go b/internal/cyberark/servicediscovery/discovery_test.go
index d1091307..618e63f9 100644
--- a/internal/cyberark/servicediscovery/discovery_test.go
+++ b/internal/cyberark/servicediscovery/discovery_test.go
@@ -64,9 +64,9 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
},
})
- client := New(httpClient)
+ client := New(httpClient, testSpec.subdomain)
- services, err := client.DiscoverServices(ctx, testSpec.subdomain)
+ services, _, err := client.DiscoverServices(ctx)
if testSpec.expectedError != nil {
assert.EqualError(t, err, testSpec.expectedError.Error())
assert.Nil(t, services)
diff --git a/internal/cyberark/servicediscovery/testdata/discovery_success.json.template b/internal/cyberark/servicediscovery/testdata/discovery_success.json.template
index c503028c..ee04b067 100644
--- a/internal/cyberark/servicediscovery/testdata/discovery_success.json.template
+++ b/internal/cyberark/servicediscovery/testdata/discovery_success.json.template
@@ -3,6 +3,7 @@
"dr_region": "us-east-2",
"subdomain": "venafi-test",
"platform_id": "platform-123",
+ "tenant_id": "tenant-123",
"identity_id": "identity-456",
"default_url": "https://venafi-test.integration-cyberark.cloud",
"tenant_flags": {
diff --git a/internal/envelope/doc.go b/internal/envelope/doc.go
index 751b5bbd..403eb728 100644
--- a/internal/envelope/doc.go
+++ b/internal/envelope/doc.go
@@ -1,12 +1,13 @@
// Package envelope provides types and interfaces for envelope encryption.
//
// Envelope encryption combines asymmetric and symmetric cryptography to
-// efficiently encrypt data. The EncryptedData type holds the result, and
-// the Encryptor interface defines the encryption operation.
+// efficiently encrypt data. The Encryptor interface defines the encryption
+// operation, returning data in JWE (JSON Web Encryption) format as defined
+// in RFC 7516.
//
// Implementations are available in subpackages:
//
-// - internal/envelope/rsa: RSA-OAEP + AES-256-GCM
+// - internal/envelope/rsa: RSA-OAEP-256 + AES-256-GCM using JWE
//
// See subpackage documentation for usage examples.
package envelope
diff --git a/internal/envelope/keyfetch/client.go b/internal/envelope/keyfetch/client.go
new file mode 100644
index 00000000..53a8d5b9
--- /dev/null
+++ b/internal/envelope/keyfetch/client.go
@@ -0,0 +1,208 @@
+package keyfetch
+
+import (
+ "context"
+ "crypto/rsa"
+ "crypto/x509"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ "github.com/jetstack/venafi-connection-lib/http_client"
+ "github.com/lestrrat-go/jwx/v3/jwk"
+ "k8s.io/klog/v2"
+
+ "github.com/jetstack/preflight/internal/cyberark"
+ "github.com/jetstack/preflight/internal/cyberark/identity"
+ "github.com/jetstack/preflight/internal/cyberark/servicediscovery"
+ "github.com/jetstack/preflight/pkg/version"
+)
+
+const (
+ // minRSAKeySize is the minimum RSA key size in bits; we'd expect that keys will be larger but 2048 is a sane floor
+ // to enforce to ensure that a weak key can't accidentally be used
+ minRSAKeySize = 2048
+)
+
+// KeyFetcher is an interface for fetching public keys.
+type KeyFetcher interface {
+ // FetchKey retrieves a public key from the key source.
+ FetchKey(ctx context.Context) (PublicKey, error)
+}
+
+// Compile-time check that Client implements KeyFetcher
+var _ KeyFetcher = (*Client)(nil)
+
+// PublicKey represents an RSA public key retrieved from the key server.
+type PublicKey struct {
+ // KeyID is the unique identifier for this key
+ KeyID string
+
+ // Key is the actual RSA public key
+ Key *rsa.PublicKey
+}
+
+// Client fetches public keys from a CyberArk HTTP endpoint that provides keys in JWKS format.
+// It can be expanded in future to support other key types and formats, but for now it only supports RSA keys
+// and ignored other types.
+type Client struct {
+ discoveryClient *servicediscovery.Client
+ identityClient *identity.Client
+ cfg cyberark.ClientConfig
+
+ // httpClient is the HTTP client used for requests
+ httpClient *http.Client
+
+ cachedKey PublicKey
+ cachedKeyMutex sync.Mutex
+ cachedKeyTime time.Time
+}
+
+// NewClient creates a new key fetching client.
+// Uses CyberArk service discovery to derive the JWKS endpoint and CyberArk identity client for authentication.
+// Constructing the client involves a service discovery call to initialise the identity client,
+// so this may return an error if the discovery client is not able to connect to the service discovery endpoint.
+// If httpClient is nil, a default HTTP client will be created.
+func NewClient(ctx context.Context, discoveryClient *servicediscovery.Client, cfg cyberark.ClientConfig, httpClient *http.Client) (*Client, error) {
+ if httpClient == nil {
+ var rootCAs *x509.CertPool
+ httpClient = http_client.NewDefaultClient(version.UserAgent(), rootCAs)
+ }
+
+ services, _, err := discoveryClient.DiscoverServices(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get services from discovery client for initialising identity client: %w", err)
+ }
+
+ return &Client{
+ discoveryClient: discoveryClient,
+ identityClient: identity.New(httpClient, services.Identity.API, cfg.Subdomain),
+ cfg: cfg,
+ httpClient: httpClient,
+ }, nil
+}
+
+// FetchKey retrieves the public keys from the configured endpoint.
+// It returns a slice of PublicKey structs containing the key material and metadata.
+func (c *Client) FetchKey(ctx context.Context) (PublicKey, error) {
+ logger := klog.FromContext(ctx).WithName("keyfetch")
+ c.cachedKeyMutex.Lock()
+ defer c.cachedKeyMutex.Unlock()
+
+ if time.Since(c.cachedKeyTime) < 15*time.Minute {
+ klog.FromContext(ctx).WithName("keyfetch").V(2).Info("using cached key", "fetchedAt", c.cachedKeyTime.Format(time.RFC3339Nano), "kid", c.cachedKey.KeyID)
+ return c.cachedKey, nil
+ }
+
+ services, _, err := c.discoveryClient.DiscoverServices(ctx)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to get services from discovery client: %w", err)
+ }
+
+ err = c.identityClient.LoginUsernamePassword(ctx, c.cfg.Username, []byte(c.cfg.Secret))
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to authenticate for fetching JWKs: %w", err)
+ }
+
+ endpoint, err := url.JoinPath(services.DiscoveryContext.API, "discovery-context/jwks")
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to construct endpoint URL: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ _, err = c.identityClient.AuthenticateRequest(req)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to authenticate request: %s", err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+ version.SetUserAgent(req)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to fetch keys from %s: %w", endpoint, err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return PublicKey{}, fmt.Errorf("unexpected status code %d from %s: %s", resp.StatusCode, endpoint, string(body))
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to read response body: %w", err)
+ }
+
+ keySet, err := jwk.Parse(body)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to parse JWKs response: %w", err)
+ }
+
+ for i := range keySet.Len() {
+ key, ok := keySet.Key(i)
+ if !ok {
+ continue
+ }
+
+ // Only process RSA keys
+ if key.KeyType().String() != "RSA" {
+ continue
+ }
+
+ var rawKey any
+ if err := jwk.Export(key, &rawKey); err != nil {
+ // skip unparseable keys
+ continue
+ }
+
+ rsaKey, ok := rawKey.(*rsa.PublicKey)
+ if !ok {
+ // only process RSA keys (for now)
+ continue
+ }
+
+ if rsaKey.N.BitLen() < minRSAKeySize {
+ // skip keys that are too small to be secure
+ continue
+ }
+
+ kid, ok := key.KeyID()
+ if !ok {
+ // skip any keys which don't have an ID
+ continue
+ }
+
+ alg, ok := key.Algorithm()
+ if !ok {
+ // skip any keys which don't have an algorithm specified
+ continue
+ }
+
+ if alg.String() != "RSA-OAEP-256" {
+ // we only use RSA keys for RSA-OAEP-256
+ continue
+ }
+
+ // return the first valid key we find
+
+ logger.Info("fetched valid RSA key", "kid", kid)
+
+ c.cachedKey = PublicKey{
+ KeyID: kid,
+ Key: rsaKey,
+ }
+ c.cachedKeyTime = time.Now()
+
+ return c.cachedKey, nil
+ }
+
+ return PublicKey{}, fmt.Errorf("no valid RSA keys found at %s", endpoint)
+}
diff --git a/internal/envelope/keyfetch/client_test.go b/internal/envelope/keyfetch/client_test.go
new file mode 100644
index 00000000..6af307db
--- /dev/null
+++ b/internal/envelope/keyfetch/client_test.go
@@ -0,0 +1,396 @@
+package keyfetch
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/jetstack/preflight/internal/cyberark"
+ "github.com/jetstack/preflight/internal/cyberark/identity"
+ "github.com/jetstack/preflight/internal/cyberark/servicediscovery"
+)
+
+// testClientSetup sets up a complete test environment with mock identity and discovery servers
+// and returns a configured client along with the test ClientConfig
+func testClientSetup(t *testing.T, jwksServerURL string) (*Client, cyberark.ClientConfig) {
+ t.Helper()
+
+ // Create mock identity server
+ identityURL, httpClient := identity.MockIdentityServer(t)
+
+ // Set up services for mock discovery server
+ services := servicediscovery.Services{
+ Identity: servicediscovery.ServiceEndpoint{
+ IsActive: true,
+ Type: "main",
+ API: identityURL,
+ },
+ DiscoveryContext: servicediscovery.ServiceEndpoint{
+ IsActive: true,
+ Type: "main",
+ API: jwksServerURL,
+ },
+ }
+
+ // Create mock discovery server
+ _ = servicediscovery.MockDiscoveryServer(t, services)
+
+ // Create discovery client
+ discoveryClient := servicediscovery.New(httpClient, servicediscovery.MockDiscoverySubdomain)
+
+ // Create test config with credentials that match the mock identity server
+ cfg := cyberark.ClientConfig{
+ Subdomain: servicediscovery.MockDiscoverySubdomain,
+ Username: "test@example.com", // matches successUser in mock identity server
+ Secret: "somepassword", // matches successPassword in mock identity server
+ }
+
+ // Create the keyfetch client with the properly configured httpClient
+ client, err := NewClient(t.Context(), discoveryClient, cfg, httpClient)
+ require.NoError(t, err)
+
+ return client, cfg
+}
+
+func mockJWKSServer(t *testing.T, statusCode int, jwksResponse string) *httptest.Server {
+ t.Helper()
+
+ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Check if this is the JWKS endpoint
+ if r.URL.Path == "/discovery-context/jwks" {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, "application/json", r.Header.Get("Accept"))
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ _, err := w.Write([]byte(jwksResponse))
+ require.NoError(t, err)
+ }
+ }))
+
+ t.Cleanup(server.Close)
+
+ return server
+}
+
+func TestClient_FetchKey(t *testing.T) {
+ // Sample JWKs response with a valid RSA key
+ // This is a minimal example with the required fields, used in multiple tests
+ jwksResponse := `{
+"keys": [
+ {
+ "kty": "RSA",
+ "use": "enc",
+ "kid": "test-key-1",
+ "alg": "RSA-OAEP-256",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ t.Run("successful fetch", func(t *testing.T) {
+
+ server := mockJWKSServer(t, http.StatusOK, jwksResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ key, err := client.FetchKey(t.Context())
+
+ require.NoError(t, err)
+
+ assert.Equal(t, "test-key-1", key.KeyID)
+ assert.NotNil(t, key.Key)
+ assert.NotNil(t, key.Key.N)
+ assert.Greater(t, key.Key.E, 0)
+ })
+
+ t.Run("multiple keys", func(t *testing.T) {
+ // want to check that FetchKey returns the first valid RSA key, even if there are multiple keys in the response
+ multiKeyResponse := `{
+ "keys": [
+ {
+ "kty": "RSA",
+ "kid": "key-1",
+ "alg": "RSA-OAEP-256",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ },
+ {
+ "kty": "RSA",
+ "kid": "key-2",
+ "alg": "RSA-OAEP-256",
+ "n": "4J0VE8FK1rSQUBGiLpk4MkPyFApCyCugOfkuH0hiHclxZay96JgyZylH97eqs-ZmWXtv42ynYctIj2ZleaoqVDfMOqZ1GsbccyNAYReDtUYgeUtJEajpfUo1vitoh6OEB6nB0Hau07ELLqcUoxH_zkH5Kwoi_BgxByJDQ1HOut6nyEPTXLTMrAYK_pqL_kzsU0OtrCgSBh6j-11ToqUfxsLupbadRC0t5zrq4-3mZKqxBUz4XB2g3b9d2lH7mOTl5J_E8jcD4tK9DePzjdbkRWonBEJetWl9f2mh_VD1sxJbie1kzM5cdQylXzV_AvhSr58w00qy6XR_QXI10UU16Q",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, multiKeyResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ key, err := client.FetchKey(t.Context())
+
+ require.NoError(t, err)
+
+ assert.Equal(t, "key-1", key.KeyID)
+ })
+
+ t.Run("filters non-RSA keys", func(t *testing.T) {
+ // check that the client correctly filters out non-RSA keys and returns the first valid RSA key
+ mixedKeyResponse := `{
+ "keys": [
+ {
+ "kty": "EC",
+ "kid": "ec-key-1",
+ "alg": "ES256",
+ "crv": "P-256",
+ "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
+ "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE"
+ },
+ {
+ "kty": "RSA",
+ "kid": "rsa-key-1",
+ "alg": "RSA-OAEP-256",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, mixedKeyResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ key, err := client.FetchKey(t.Context())
+
+ require.NoError(t, err)
+ assert.Equal(t, "rsa-key-1", key.KeyID)
+ })
+
+ t.Run("error on non-200 status", func(t *testing.T) {
+ server := mockJWKSServer(t, http.StatusInternalServerError, "") // Response body won't be used since we return 500
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "unexpected status code 500")
+ })
+
+ t.Run("error on invalid JSON", func(t *testing.T) {
+ server := mockJWKSServer(t, http.StatusOK, "invalid json")
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to parse JWKs response")
+ })
+
+ t.Run("error on no RSA keys", func(t *testing.T) {
+ ecOnlyResponse := `{
+ "keys": [
+ {
+ "kty": "EC",
+ "kid": "ec-key-1",
+ "alg": "ES256",
+ "crv": "P-256",
+ "x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
+ "y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, ecOnlyResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "no valid RSA keys found")
+ })
+
+ t.Run("context cancellation", func(t *testing.T) {
+ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // This handler will never respond
+ <-r.Context().Done()
+ }))
+ defer server.Close()
+
+ client, _ := testClientSetup(t, server.URL)
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel() // Cancel immediately
+
+ _, err := client.FetchKey(ctx)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "context canceled")
+ })
+
+ t.Run("authentication failure", func(t *testing.T) {
+ server := mockJWKSServer(t, http.StatusOK, jwksResponse)
+
+ // Create mock identity server
+ identityURL, httpClient := identity.MockIdentityServer(t)
+
+ // Set up services for mock discovery server
+ services := servicediscovery.Services{
+ Identity: servicediscovery.ServiceEndpoint{
+ IsActive: true,
+ Type: "main",
+ API: identityURL,
+ },
+ DiscoveryContext: servicediscovery.ServiceEndpoint{
+ IsActive: true,
+ Type: "main",
+ API: server.URL,
+ },
+ }
+
+ // Create mock discovery server
+ _ = servicediscovery.MockDiscoveryServer(t, services)
+
+ // Create discovery client
+ discoveryClient := servicediscovery.New(httpClient, servicediscovery.MockDiscoverySubdomain)
+
+ // Create test config with WRONG credentials
+ // Use the failureUser from the mock identity server
+ cfg := cyberark.ClientConfig{
+ Subdomain: servicediscovery.MockDiscoverySubdomain,
+ Username: "test-fail@example.com", // This user is configured to fail in the mock server // TODO: export these constants from the identity package to avoid hardcoding them here
+ Secret: "somepassword",
+ }
+
+ // Create the keyfetch client
+ client, err := NewClient(t.Context(), discoveryClient, cfg, httpClient)
+ require.NoError(t, err)
+
+ _, err = client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to authenticate")
+ })
+
+ t.Run("service discovery fails", func(t *testing.T) {
+ // Create mock identity server (won't be used but needed for setup)
+ identityURL, httpClient := identity.MockIdentityServer(t)
+
+ // Set up services for mock discovery server
+ services := servicediscovery.Services{
+ Identity: servicediscovery.ServiceEndpoint{
+ IsActive: true,
+ Type: "main",
+ API: identityURL,
+ },
+ }
+
+ // Create mock discovery server
+ _ = servicediscovery.MockDiscoveryServer(t, services)
+
+ // Create discovery client with a subdomain that triggers failure
+ discoveryClient := servicediscovery.New(httpClient, "bad-request")
+
+ cfg := cyberark.ClientConfig{
+ Subdomain: "bad-request",
+ Username: "test@example.com",
+ Secret: "somepassword",
+ }
+
+ _, err := NewClient(t.Context(), discoveryClient, cfg, httpClient)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to get services from discovery client")
+ })
+
+ // Note: We used to test with smaller key sizes but the library started to reject such small keys.
+ // Therefore, we have removed the test for the time being.
+
+ t.Run("skips keys without kid", func(t *testing.T) {
+ noKidResponse := `{
+ "keys": [
+ {
+ "kty": "RSA",
+ "alg": "RSA-OAEP-256",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, noKidResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "no valid RSA keys found")
+ })
+
+ t.Run("filters keys with wrong algorithm", func(t *testing.T) {
+ wrongAlgResponse := `{
+ "keys": [
+ {
+ "kty": "RSA",
+ "kid": "wrong-alg-key",
+ "alg": "RS256",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ },
+ {
+ "kty": "RSA",
+ "kid": "correct-alg-key",
+ "alg": "RSA-OAEP-256",
+ "n": "4J0VE8FK1rSQUBGiLpk4MkPyFApCyCugOfkuH0hiHclxZay96JgyZylH97eqs-ZmWXtv42ynYctIj2ZleaoqVDfMOqZ1GsbccyNAYReDtUYgeUtJEajpfUo1vitoh6OEB6nB0Hau07ELLqcUoxH_zkH5Kwoi_BgxByJDQ1HOut6nyEPTXLTMrAYK_pqL_kzsU0OtrCgSBh6j-11ToqUfxsLupbadRC0t5zrq4-3mZKqxBUz4XB2g3b9d2lH7mOTl5J_E8jcD4tK9DePzjdbkRWonBEJetWl9f2mh_VD1sxJbie1kzM5cdQylXzV_AvhSr58w00qy6XR_QXI10UU16Q",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, wrongAlgResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ key, err := client.FetchKey(t.Context())
+
+ require.NoError(t, err)
+ // Should skip the RS256 key and return the RSA-OAEP-256 key
+ assert.Equal(t, "correct-alg-key", key.KeyID)
+ })
+
+ t.Run("skips keys without algorithm", func(t *testing.T) {
+ noAlgResponse := `{
+ "keys": [
+ {
+ "kty": "RSA",
+ "kid": "no-alg-key",
+ "n": "vDdioGpDuAEQDd4WRXyWa4sZ5EeS9OPsRrU_jU3PbZdDcANxfh_WSeSvSBKGfGXGC3fIzu0Ernk9VjXcs3LeFdRq2N4nNRZvCzsd_MjBtn7CWgjM_Sk9DXEGn3cHHilcJUJQ4i2YgX9bHu0odNgE6cSVIUEMIC2EGuGk_I7lwroinAAwXpNLLQkV_25kv_QQof2i5f7AocY6QTd0SAo8ZUqFBzanupkeFpl3-Bsz6_zdt_N0x9k5XHQn42Q2oTupTwvXFbE1x8XtCpiaP3_fsQ9dN7t4z6HtwlNUJB2tFfF6PgdKZ9LuJpYjFPYzJQ6Rv28fuc8YHcF7Jittjyzmew",
+ "e": "AQAB"
+ }
+ ]
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, noAlgResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "no valid RSA keys found")
+ })
+
+ t.Run("handles empty key set", func(t *testing.T) {
+ emptyKeysResponse := `{
+ "keys": []
+ }`
+
+ server := mockJWKSServer(t, http.StatusOK, emptyKeysResponse)
+
+ client, _ := testClientSetup(t, server.URL)
+ _, err := client.FetchKey(t.Context())
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "no valid RSA keys found")
+ })
+}
diff --git a/internal/envelope/keyfetch/doc.go b/internal/envelope/keyfetch/doc.go
new file mode 100644
index 00000000..d3cb09da
--- /dev/null
+++ b/internal/envelope/keyfetch/doc.go
@@ -0,0 +1,9 @@
+// Package keyfetch provides a client for fetching encryption keys from an HTTP endpoint.
+//
+// The client retrieves public keys in JSON Web Key Set (JWKs) format from a remote
+// server and converts them into usable cryptographic keys for envelope encryption.
+//
+// This package uses github.com/lestrrat-go/jwx/v3/jwk for JWK parsing and handling.
+//
+// Currently, keyfetch only supports RSA keys for envelope encryption.
+package keyfetch
diff --git a/internal/envelope/keyfetch/fake.go b/internal/envelope/keyfetch/fake.go
new file mode 100644
index 00000000..d7226b2b
--- /dev/null
+++ b/internal/envelope/keyfetch/fake.go
@@ -0,0 +1,85 @@
+package keyfetch
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/rsa"
+ "fmt"
+)
+
+// Compile-time check that FakeClient implements KeyFetcher
+var _ KeyFetcher = (*FakeClient)(nil)
+
+// FakeClient is a fake implementation of the key fetcher for testing.
+// It can be configured to return specific keys or errors for testing different scenarios.
+type FakeClient struct {
+ // Key is the public key that will be returned by FetchKey.
+ // If nil, a random key will be generated on the first call.
+ Key *PublicKey
+
+ // Err is the error that will be returned by FetchKey.
+ // If both Key and Err are set, Err takes precedence.
+ Err error
+
+ // FetchKeyCalls tracks how many times FetchKey was called
+ FetchKeyCalls int
+}
+
+// NewFakeClient creates a new fake client for testing.
+func NewFakeClient() *FakeClient {
+ return &FakeClient{}
+}
+
+// NewFakeClientWithKey creates a new fake client that returns the specified key.
+func NewFakeClientWithKey(keyID string, key *rsa.PublicKey) *FakeClient {
+ return &FakeClient{
+ Key: &PublicKey{
+ KeyID: keyID,
+ Key: key,
+ },
+ }
+}
+
+// NewFakeClientWithError creates a new fake client that returns the specified error.
+func NewFakeClientWithError(err error) *FakeClient {
+ return &FakeClient{
+ Err: err,
+ }
+}
+
+// FetchKey implements the key fetching interface for testing.
+// It returns the configured key or error, or generates a random key if none is configured.
+func (f *FakeClient) FetchKey(ctx context.Context) (PublicKey, error) {
+ f.FetchKeyCalls++
+
+ // Check if context is canceled
+ if ctx.Err() != nil {
+ return PublicKey{}, ctx.Err()
+ }
+
+ // If an error is configured, return it
+ if f.Err != nil {
+ return PublicKey{}, f.Err
+ }
+
+ // If a key is configured, return it
+ if f.Key != nil {
+ return *f.Key, nil
+ }
+
+ // Generate a random key for testing
+ privateKey, err := rsa.GenerateKey(rand.Reader, minRSAKeySize)
+ if err != nil {
+ return PublicKey{}, fmt.Errorf("failed to generate test key: %w", err)
+ }
+
+ generatedKey := PublicKey{
+ KeyID: "test-key",
+ Key: &privateKey.PublicKey,
+ }
+
+ // Cache the generated key for subsequent calls
+ f.Key = &generatedKey
+
+ return generatedKey, nil
+}
diff --git a/internal/envelope/keyfetch/fake_test.go b/internal/envelope/keyfetch/fake_test.go
new file mode 100644
index 00000000..c2bc255d
--- /dev/null
+++ b/internal/envelope/keyfetch/fake_test.go
@@ -0,0 +1,89 @@
+package keyfetch
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/rsa"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestFakeClient(t *testing.T) {
+ t.Run("returns generated key by default", func(t *testing.T) {
+ fake := NewFakeClient()
+
+ key, err := fake.FetchKey(t.Context())
+ require.NoError(t, err)
+
+ assert.Equal(t, "test-key", key.KeyID)
+ assert.NotNil(t, key.Key)
+ assert.Equal(t, 1, fake.FetchKeyCalls)
+
+ // Subsequent calls return the same key
+ key2, err := fake.FetchKey(t.Context())
+ require.NoError(t, err)
+ assert.Equal(t, key.KeyID, key2.KeyID)
+ assert.Equal(t, key.Key, key2.Key)
+ assert.Equal(t, 2, fake.FetchKeyCalls)
+ })
+
+ t.Run("returns configured key", func(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, minRSAKeySize)
+ require.NoError(t, err)
+
+ fake := NewFakeClientWithKey("custom-key", &privateKey.PublicKey)
+
+ key, err := fake.FetchKey(t.Context())
+ require.NoError(t, err)
+
+ assert.Equal(t, "custom-key", key.KeyID)
+ assert.Equal(t, &privateKey.PublicKey, key.Key)
+ assert.Equal(t, 1, fake.FetchKeyCalls)
+ })
+
+ t.Run("returns configured error", func(t *testing.T) {
+ expectedErr := errors.New("test error")
+ fake := NewFakeClientWithError(expectedErr)
+
+ _, err := fake.FetchKey(t.Context())
+ require.Error(t, err)
+
+ assert.Equal(t, expectedErr, err)
+ assert.Equal(t, 1, fake.FetchKeyCalls)
+ })
+
+ t.Run("respects context cancellation", func(t *testing.T) {
+ fake := NewFakeClient()
+
+ ctx, cancel := context.WithCancel(t.Context())
+ cancel()
+
+ _, err := fake.FetchKey(ctx)
+ require.Error(t, err)
+
+ assert.Equal(t, context.Canceled, err)
+ assert.Equal(t, 1, fake.FetchKeyCalls)
+ })
+
+ t.Run("error takes precedence over key", func(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, minRSAKeySize)
+ require.NoError(t, err)
+
+ expectedErr := errors.New("test error")
+ fake := &FakeClient{
+ Key: &PublicKey{
+ KeyID: "custom-key",
+ Key: &privateKey.PublicKey,
+ },
+ Err: expectedErr,
+ }
+
+ _, err = fake.FetchKey(t.Context())
+ require.Error(t, err)
+
+ assert.Equal(t, expectedErr, err)
+ })
+}
diff --git a/internal/envelope/rsa/doc.go b/internal/envelope/rsa/doc.go
index 9817f844..750d3209 100644
--- a/internal/envelope/rsa/doc.go
+++ b/internal/envelope/rsa/doc.go
@@ -1,3 +1,11 @@
-// Package rsa implements RSA envelope encryption, conforming to the interface in the envelope package.
-// It uses RSA-OAEP with SHA-256 for key encryption, and AES-256-GCM for data encryption.
+// Package rsa implements RSA envelope encryption using JWE (JSON Web Encryption) format.
+// It conforms to the interface in the envelope package.
+//
+// The implementation uses:
+// - RSA-OAEP-256 (RSA-OAEP with SHA-256) for key encryption
+// - AES-256-GCM (A256GCM) for content encryption
+// - JWE Compact Serialization format as defined in RFC 7516
+//
+// The output is a JWE string with 5 base64url-encoded parts separated by dots:
+// header.encryptedKey.iv.ciphertext.tag
package rsa
diff --git a/internal/envelope/rsa/encryptor.go b/internal/envelope/rsa/encryptor.go
index 7e29066c..fd5b1cab 100644
--- a/internal/envelope/rsa/encryptor.go
+++ b/internal/envelope/rsa/encryptor.go
@@ -1,128 +1,72 @@
package rsa
import (
- "crypto/aes"
- "crypto/cipher"
- "crypto/rand"
- "crypto/rsa"
- "crypto/sha256"
+ "context"
"fmt"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwe"
+
"github.com/jetstack/preflight/internal/envelope"
+ "github.com/jetstack/preflight/internal/envelope/keyfetch"
)
const (
- // aesKeySize is the size of the AES-256 key in bytes; aes.NewCipher generates cipher.Block based
- // on the size of key passed in, and 32 bytes corresponds to a 256-bit AES key
- aesKeySize = 32
-
- // minRSAKeySize is the minimum RSA key size in bits; we'd expect that keys will be larger but 2048 is a sane floor
- // to enforce to ensure that a weak key can't accidentally be used
- minRSAKeySize = 2048
-
- // keyAlgorithmIdentifier is set in EncryptedData to identify the key wrapping algorithm used in this package
- keyAlgorithmIdentifier = "RSA-OAEP-SHA256"
+ // EncryptionType is the type identifier for RSA JWE encryption
+ EncryptionType = "JWE-RSA"
)
// Compile-time check that Encryptor implements envelope.Encryptor
var _ envelope.Encryptor = (*Encryptor)(nil)
-// Encryptor provides envelope encryption using RSA for key wrapping
-// and AES-256-GCM for data encryption.
+// Encryptor provides envelope encryption using RSA-OAEP-256 for key wrapping
+// and AES-256-GCM for data encryption, outputting JWE Compact Serialization format.
type Encryptor struct {
- keyID string
- rsaPublicKey *rsa.PublicKey
+ fetcher keyfetch.KeyFetcher
}
-// NewEncryptor creates a new Encryptor with the provided RSA public key.
-// The RSA key must be at least minRSAKeySize bits
-func NewEncryptor(keyID string, publicKey *rsa.PublicKey) (*Encryptor, error) {
- if publicKey == nil {
- return nil, fmt.Errorf("RSA public key cannot be nil")
- }
-
- // Validate key size
- keySize := publicKey.N.BitLen()
- if keySize < minRSAKeySize {
- return nil, fmt.Errorf("RSA key size must be at least %d bits, got %d bits", minRSAKeySize, keySize)
- }
-
- if len(keyID) == 0 {
- return nil, fmt.Errorf("keyID cannot be empty")
- }
-
+// NewEncryptor creates a new Encryptor with the provided key fetcher.
+// The encryptor will use RSA-OAEP-256 for key encryption and A256GCM for content encryption.
+func NewEncryptor(fetcher keyfetch.KeyFetcher) (*Encryptor, error) {
return &Encryptor{
- keyID: keyID,
- rsaPublicKey: publicKey,
+ fetcher: fetcher,
}, nil
}
// Encrypt performs envelope encryption on the provided data.
-// It generates a random AES-256 key, encrypts the data with AES-256-GCM,
-// then encrypts the AES key with RSA-OAEP-SHA256.
-func (e *Encryptor) Encrypt(data []byte) (*envelope.EncryptedData, error) {
+// It returns an EncryptedData struct containing JWE Compact Serialization format and type metadata.
+// The JWE uses RSA-OAEP-256 for key encryption and A256GCM for content encryption.
+func (e *Encryptor) Encrypt(ctx context.Context, data []byte) (*envelope.EncryptedData, error) {
if len(data) == 0 {
return nil, fmt.Errorf("data to encrypt cannot be empty")
}
- aesKey := make([]byte, aesKeySize)
- if _, err := rand.Read(aesKey); err != nil {
- return nil, fmt.Errorf("failed to generate AES key: %w", err)
- }
-
- // zero the key from memory before the function returns
- // TODO: in go1.26+, consider using secret.Do in this function
- defer func() {
- for i := range aesKey {
- aesKey[i] = 0
- }
- }()
-
- block, err := aes.NewCipher(aesKey)
- if err != nil {
- return nil, fmt.Errorf("failed to create AES cipher: %w", err)
- }
-
- gcm, err := cipher.NewGCM(block)
+ key, err := e.fetcher.FetchKey(ctx)
if err != nil {
- return nil, fmt.Errorf("failed to create GCM cipher: %w", err)
+ return nil, fmt.Errorf("failed to fetch encryption key: %w", err)
}
- encryptedData := &envelope.EncryptedData{
- KeyID: e.keyID,
- KeyAlgorithm: keyAlgorithmIdentifier,
- EncryptedKey: nil,
- EncryptedData: nil,
- Nonce: make([]byte, gcm.NonceSize()),
+ // Create headers with the key ID
+ headers := jwe.NewHeaders()
+ if err := headers.Set("kid", key.KeyID); err != nil {
+ return nil, fmt.Errorf("failed to set key ID header: %w", err)
}
- // Generate a random nonce for AES-GCM.
- // Security: Nonces must never be re-used for a given key. Since we generate a new AES key for each encryption,
- // the risk of nonce reuse is not a concern here.
- if _, err := rand.Read(encryptedData.Nonce); err != nil {
- return nil, fmt.Errorf("failed to generate nonce: %w", err)
- }
-
- // Seal encrypts and authenticates the data. This could include additional authenticated data,
- // but we don't make use of that here.
- // First nil: allocate new slice for output.
- // Last nil: no additional authenticated data (AAD) needed.
-
- encryptedData.EncryptedData = gcm.Seal(nil, encryptedData.Nonce, data, nil)
-
- // Encrypt AES key with RSA-OAEP-SHA256. The nil parameter means no additional
- // context data is mixed into the hash; this could be used to disambiguate different uses of the same key,
- // but we only have one use for the key here.
- encryptedData.EncryptedKey, err = rsa.EncryptOAEP(
- sha256.New(),
- rand.Reader,
- e.rsaPublicKey,
- aesKey,
- nil,
+ // Encrypt using RSA-OAEP-256 for key algorithm and A256GCM for content encryption
+ // TODO: When standardised, consider using secret.Do to wrap this call, since it will generate an AES key
+ // (see https://pkg.go.dev/runtime/secret)
+ encrypted, err := jwe.Encrypt(
+ data,
+ jwe.WithKey(jwa.RSA_OAEP_256(), key.Key, jwe.WithPerRecipientHeaders(headers)),
+ jwe.WithContentEncryption(jwa.A256GCM()),
+ jwe.WithCompact(),
)
if err != nil {
- return nil, fmt.Errorf("failed to encrypt AES key with RSA: %w", err)
+ return nil, fmt.Errorf("failed to encrypt data: %w", err)
}
- return encryptedData, nil
+ return &envelope.EncryptedData{
+ Data: encrypted,
+ Type: EncryptionType,
+ }, nil
}
diff --git a/internal/envelope/rsa/encryptor_test.go b/internal/envelope/rsa/encryptor_test.go
index ede0ea3b..50534fed 100644
--- a/internal/envelope/rsa/encryptor_test.go
+++ b/internal/envelope/rsa/encryptor_test.go
@@ -3,13 +3,23 @@ package rsa
import (
"crypto/rand"
"crypto/rsa"
+ "encoding/base64"
+ "strings"
"sync"
"testing"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwe"
"github.com/stretchr/testify/require"
+
+ "github.com/jetstack/preflight/internal/envelope/keyfetch"
)
-const testKeyID = "test-key-id"
+const (
+ testKeyID = "test-key-id"
+ // minRSAKeySize is the minimum RSA key size used for test key generation
+ minRSAKeySize = 2048
+)
var (
testKeyOnce sync.Once
@@ -31,58 +41,10 @@ func testKey() *rsa.PrivateKey {
return internalTestKey
}
-func TestNewEncryptor_ValidKeys(t *testing.T) {
- tests := []struct {
- name string
- keySize int
- }{
- {"2048 bits", 2048},
- {"3072 bits", 3072},
- {"4096 bits", 4096},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- key, err := rsa.GenerateKey(rand.Reader, tt.keySize)
- require.NoError(t, err)
-
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
- require.NoError(t, err)
- require.NotNil(t, enc)
- })
- }
-}
-
-func TestNewEncryptor_RejectsSmallKeys(t *testing.T) {
- key, err := rsa.GenerateKey(rand.Reader, 1024)
- require.NoError(t, err)
-
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
- require.Error(t, err)
- require.Nil(t, enc)
- require.Contains(t, err.Error(), "must be at least 2048 bits")
-}
-
-func TestNewEncryptor_NilKey(t *testing.T) {
- enc, err := NewEncryptor(testKeyID, nil)
- require.Error(t, err)
- require.Nil(t, enc)
- require.Contains(t, err.Error(), "cannot be nil")
-}
-
-func TestNewEncryptor_EmptyKeyID(t *testing.T) {
- key := testKey()
-
- enc, err := NewEncryptor("", &key.PublicKey)
- require.Error(t, err)
- require.Nil(t, enc)
- require.Contains(t, err.Error(), "keyID cannot be empty")
-}
-
func TestEncrypt_VariousDataSizes(t *testing.T) {
- key := testKey()
+ fetcher := keyfetch.NewFakeClient()
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
+ enc, err := NewEncryptor(fetcher)
require.NoError(t, err)
tests := []struct {
@@ -100,90 +62,108 @@ func TestEncrypt_VariousDataSizes(t *testing.T) {
_, err := rand.Read(data)
require.NoError(t, err)
- result, err := enc.Encrypt(data)
+ result, err := enc.Encrypt(t.Context(), data)
require.NoError(t, err)
require.NotNil(t, result)
+ require.Equal(t, EncryptionType, result.Type, "Type should be JWE-RSA")
- // Verify all fields are populated
- require.NotEmpty(t, result.EncryptedKey)
- require.NotEmpty(t, result.EncryptedData)
- require.NotEmpty(t, result.Nonce)
+ // Verify JWE Compact Serialization format (5 base64url parts separated by dots)
+ jweString := string(result.Data)
+ parts := strings.Split(jweString, ".")
+ require.Len(t, parts, 5, "JWE Compact Serialization should have 5 parts")
- // Verify KeyID and KeyAlgorithm are set correctly
- require.Equal(t, testKeyID, result.KeyID)
- require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm)
+ // Verify each part is non-empty
+ for i, part := range parts {
+ require.NotEmpty(t, part, "JWE part %d should not be empty", i)
- // Verify nonce is correct size (12 bytes for GCM)
- require.Len(t, result.Nonce, 12)
+ _, err = base64.RawURLEncoding.DecodeString(part)
+ require.NoError(t, err, "JWE part %d should be valid base64url: %s", i, part)
+ }
- // Verify encrypted data differs from input
- require.NotEqual(t, data, result.EncryptedData)
+ // Verify the result differs from input
+ require.NotEqual(t, data, result.Data)
})
}
}
func TestEncrypt_EmptyData(t *testing.T) {
- key := testKey()
+ fetcher := keyfetch.NewFakeClient()
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
+ enc, err := NewEncryptor(fetcher)
require.NoError(t, err)
- result, err := enc.Encrypt([]byte{})
+ result, err := enc.Encrypt(t.Context(), []byte{})
require.Error(t, err)
require.Nil(t, result)
require.Contains(t, err.Error(), "cannot be empty")
}
func TestEncrypt_NonDeterministic(t *testing.T) {
- key := testKey()
+ fetcher := keyfetch.NewFakeClient()
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
+ enc, err := NewEncryptor(fetcher)
require.NoError(t, err)
data := []byte("test data for encryption")
// Encrypt the same data twice
- result1, err := enc.Encrypt(data)
+ result1, err := enc.Encrypt(t.Context(), data)
require.NoError(t, err)
+ require.Equal(t, EncryptionType, result1.Type, "Type should be JWE-RSA")
- result2, err := enc.Encrypt(data)
+ result2, err := enc.Encrypt(t.Context(), data)
require.NoError(t, err)
+ require.Equal(t, EncryptionType, result2.Type, "Type should be JWE-RSA")
- // Verify KeyID and KeyAlgorithm are set correctly in both results
- require.Equal(t, testKeyID, result1.KeyID)
- require.Equal(t, keyAlgorithmIdentifier, result1.KeyAlgorithm)
- require.Equal(t, testKeyID, result2.KeyID)
- require.Equal(t, keyAlgorithmIdentifier, result2.KeyAlgorithm)
+ // Results should be different due to random nonces and RSA-OAEP randomness
+ require.NotEqual(t, result1.Data, result2.Data, "Encrypting the same data twice should produce different JWE outputs")
+}
- // Nonces should be different (random)
- require.NotEqual(t, result1.Nonce, result2.Nonce)
+func TestEncrypt_JWEFormat(t *testing.T) {
+ key := testKey()
+ fetcher := keyfetch.NewFakeClientWithKey(testKeyID, &key.PublicKey)
+
+ enc, err := NewEncryptor(fetcher)
+ require.NoError(t, err)
- // Encrypted data should be different due to different nonces
- require.NotEqual(t, result1.EncryptedData, result2.EncryptedData)
+ data := []byte("test data")
+ result, err := enc.Encrypt(t.Context(), data)
+ require.NoError(t, err)
+ require.Equal(t, EncryptionType, result.Type, "Type should be JWE-RSA")
- // Encrypted keys should be different due to RSA-OAEP randomness
- require.NotEqual(t, result1.EncryptedKey, result2.EncryptedKey)
+ // Parse and decrypt the JWE to verify format and algorithms
+ decrypted, err := jwe.Decrypt(result.Data, jwe.WithKey(jwa.RSA_OAEP_256(), key), jwe.WithContext(t.Context()))
+ require.NoError(t, err, "Result should be valid JWE with RSA-OAEP-256 and A256GCM, and should decrypt successfully")
+ require.Equal(t, data, decrypted, "Decrypted data should match original")
}
-func TestEncrypt_AllFieldsPopulated(t *testing.T) {
+func TestEncrypt_DecryptRoundtrip(t *testing.T) {
key := testKey()
+ fetcher := keyfetch.NewFakeClientWithKey(testKeyID, &key.PublicKey)
- enc, err := NewEncryptor(testKeyID, &key.PublicKey)
+ enc, err := NewEncryptor(fetcher)
require.NoError(t, err)
- data := []byte("test data")
- result, err := enc.Encrypt(data)
+ originalData := []byte("test data for roundtrip encryption and decryption")
+
+ // Encrypt the data
+ encrypted, err := enc.Encrypt(t.Context(), originalData)
require.NoError(t, err)
+ require.Equal(t, EncryptionType, encrypted.Type, "Type should be JWE-RSA")
+
+ msg, err := jwe.Parse(encrypted.Data)
+ require.NoError(t, err)
+
+ headers := msg.ProtectedHeaders()
- require.NotNil(t, result)
- require.NotEmpty(t, result.EncryptedKey, "EncryptedKey should be populated")
- require.NotEmpty(t, result.EncryptedData, "EncryptedData should be populated")
- require.NotEmpty(t, result.Nonce, "Nonce should be populated")
+ kidHeader, ok := headers.KeyID()
+ require.True(t, ok, "JWE should contain 'kid' header")
+ require.Equal(t, testKeyID, kidHeader, "JWE 'kid' header should match the encryptor's key ID")
- // Verify KeyID and KeyAlgorithm are set correctly
- require.Equal(t, testKeyID, result.KeyID, "KeyID should match the encryptor's keyID")
- require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm, "KeyAlgorithm should be the value of keyAlgorithmIdentifier")
+ // Decrypt using the private key
+ decrypted, err := jwe.Decrypt(encrypted.Data, jwe.WithKey(jwa.RSA_OAEP_256(), key), jwe.WithContext(t.Context()))
+ require.NoError(t, err, "Decryption should succeed with the correct private key")
- // Verify encrypted key size is appropriate for RSA 2048
- require.Equal(t, 256, len(result.EncryptedKey), "EncryptedKey should be 256 bytes for RSA 2048")
+ // Verify the decrypted data matches the original
+ require.Equal(t, originalData, decrypted, "Decrypted data should match original data")
}
diff --git a/internal/envelope/rsa/keys.go b/internal/envelope/rsa/keys.go
index 3cf5e96b..7c79db69 100644
--- a/internal/envelope/rsa/keys.go
+++ b/internal/envelope/rsa/keys.go
@@ -10,6 +10,25 @@ import (
// This file contains helpers for loading keys. In practice we'll retrieve keys in some format from a DisCo endpoint
+const (
+ // HardcodedPublicKeyPEM contains a temporary hardcoded RSA public key (2048-bit) for envelope encryption.
+ // This is a TEMPORARY solution for initial development and testing.
+ // TODO: Replace with dynamic key fetching from CyberArk Discovery & Context API.
+ HardcodedPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoeq+dk4aoGdV9xjrnGJt
+VbUh5jvkQgynkP+9Ph2NVeoasXWqYOmOVeKOI7Yr58W/L8Mro6C22iSEJrPFgPF6
+t+RJsLAsAY6w1Pocq16COeelAWtxhHQGXt77WQKk0kmwhOJZ4VSeiQC4hWLUnq4N
+Ft7lwLw/50opTXLuSErrwec/bEV7G/Xp11BMsHGEL7dzpwWAfIrbCEomyWrO/L6p
+O3SAgYMdfup5ddnszeCU2FbFQziOkuMLOyir91XXk8wgdSy4IGAEGpwNx88i8fuj
+Qafze2aGWUtpWlOEQPP8lH2cj2TGUgLxGITbczJRcwuGIoJBOzAmPDWi/bapj4b6
+zQIDAQAB
+-----END PUBLIC KEY-----`
+
+ // hardcodedUID is a temporary hardcoded UID associated with the hardcoded public key
+ // It was randomly generated with the macOS "uuidgen" command
+ hardcodedUID = "A39798E6-8CE7-4E6E-9CF6-24A3C923B3A7"
+)
+
// LoadPublicKeyFromPEM parses an RSA public key from PEM-encoded bytes.
// The PEM block should be of type "PUBLIC KEY" or "RSA PUBLIC KEY".
func LoadPublicKeyFromPEM(pemBytes []byte) (*rsa.PublicKey, error) {
@@ -55,3 +74,16 @@ func LoadPublicKeyFromPEMFile(path string) (*rsa.PublicKey, error) {
return LoadPublicKeyFromPEM(pemBytes)
}
+
+// LoadHardcodedPublicKey loads and parses the hardcoded RSA public key.
+// Returns a hardcoded UID associated with the key.
+// This is a temporary solution for initial development and testing.
+// Returns an error if the hardcoded key is invalid or cannot be parsed.
+func LoadHardcodedPublicKey() (*rsa.PublicKey, string, error) {
+ key, err := LoadPublicKeyFromPEM([]byte(HardcodedPublicKeyPEM))
+ if err != nil {
+ return nil, "", err
+ }
+
+ return key, hardcodedUID, nil
+}
diff --git a/internal/envelope/rsa/keys_test.go b/internal/envelope/rsa/keys_test.go
index cccdb475..1a138a35 100644
--- a/internal/envelope/rsa/keys_test.go
+++ b/internal/envelope/rsa/keys_test.go
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/jetstack/preflight/internal/envelope/keyfetch"
internalrsa "github.com/jetstack/preflight/internal/envelope/rsa"
)
@@ -142,3 +143,25 @@ func TestLoadPublicKeyFromPEMFile_InvalidContent(t *testing.T) {
require.Error(t, err)
require.Nil(t, key)
}
+
+func TestLoadHardcodedPublicKey_CanBeUsedWithEncryptor(t *testing.T) {
+ // Test that the hardcoded key can be used to create an encryptor
+ // First, test that the key can be loaded successfully
+ key, uid, err := internalrsa.LoadHardcodedPublicKey()
+ require.NoError(t, err)
+ require.NotNil(t, key)
+ require.NotEmpty(t, uid)
+
+ fetcher := keyfetch.NewFakeClientWithKey(uid, key)
+ encryptor, err := internalrsa.NewEncryptor(fetcher)
+ require.NoError(t, err)
+ require.NotNil(t, encryptor)
+
+ // Test that the encryptor can encrypt data
+ testData := []byte("test data for encryption")
+ encryptedData, err := encryptor.Encrypt(t.Context(), testData)
+ require.NoError(t, err)
+ require.NotNil(t, encryptedData)
+ require.NotEmpty(t, encryptedData.Data)
+ require.Equal(t, "JWE-RSA", encryptedData.Type)
+}
diff --git a/internal/envelope/types.go b/internal/envelope/types.go
index 83fe5ea2..6618ce6c 100644
--- a/internal/envelope/types.go
+++ b/internal/envelope/types.go
@@ -1,31 +1,41 @@
package envelope
-// EncryptedData contains the result of envelope encryption.
-// It includes the encrypted data, the encrypted symmetric key which was used for encrypting the original data,
-// and the nonce needed for the symmetric decryption.
+import (
+ "context"
+ "encoding/json"
+)
+
+// EncryptedData represents encrypted data along with metadata about the encryption type.
type EncryptedData struct {
- // KeyID is the identifier of the asymmetric key used to encrypt the AES key.
- KeyID string `json:"key_id"`
+ // Data contains the encrypted payload
+ Data []byte `json:"data"`
+ // Type indicates the encryption format (e.g., "JWE-RSA")
+ Type string `json:"type"`
+}
- // KeyAlgorithm is the algorithm of the asymmetric key used to encrypt the AES key.
- KeyAlgorithm string `json:"key_algorithm"`
+// ToMap converts the EncryptedData struct to a map representation. Since we store data as an "_encryptedData" field in
+// a Kubernetes unstructured object, passing a raw struct would cause a panic due to the behaviour of
+// https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#DeepCopyJSONValue
+// Passing a map to unstructured.SetNestedField avoids this issue.
+func (ed *EncryptedData) ToMap() map[string]any {
+ marshalled, err := json.Marshal(ed)
+ if err != nil {
+ return nil
+ }
- // EncryptedKey is an encrypted AES-256-GCM symmetric key, used to encrypt EncryptedData.
- // This is ciphertext and should only be decryptable by the holder of the private key.
- EncryptedKey []byte `json:"encrypted_key"`
+ var out map[string]any
- // EncryptedData is the actual data encrypted using the AES-256-GCM in EncryptedKey.
- // This is ciphertext and requires the decrypted AES key and nonce for decryption.
- EncryptedData []byte `json:"encrypted_data"`
+ err = json.Unmarshal(marshalled, &out)
+ if err != nil {
+ return nil
+ }
- // Nonce is the 12-byte nonce used for AES-GCM encryption.
- // This is intentionally plaintext.
- Nonce []byte `json:"nonce"`
+ return out
}
// Encryptor performs envelope encryption on arbitrary data.
type Encryptor interface {
- // Encrypt encrypts data using envelope encryption, returning the resulting data along
- // with identifiers of the asymmetric key used to encrypt the AES key.
- Encrypt(data []byte) (*EncryptedData, error)
+ // Encrypt encrypts data using envelope encryption, returning an EncryptedData struct
+ // containing the encrypted payload and encryption type metadata.
+ Encrypt(ctx context.Context, data []byte) (*EncryptedData, error)
}
diff --git a/klone.yaml b/klone.yaml
index d9e5edb8..79241e87 100644
--- a/klone.yaml
+++ b/klone.yaml
@@ -10,55 +10,55 @@ targets:
- folder_name: generate-verify
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/generate-verify
- folder_name: go
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/go
- folder_name: helm
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/helm
- folder_name: help
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/help
- folder_name: kind
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/kind
- folder_name: klone
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/klone
- folder_name: licenses
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/licenses
- folder_name: oci-build
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/oci-build
- folder_name: oci-publish
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/oci-publish
- folder_name: repository-base
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/repository-base
- folder_name: tools
repo_url: https://github.com/cert-manager/makefile-modules.git
repo_ref: main
- repo_hash: 2c5045aea89e02724fed0b9148e6b21abca94e9a
+ repo_hash: aef3f64fa51b7c1097eaa4102d325b0a08be938c
repo_path: modules/tools
diff --git a/make/00_mod.mk b/make/00_mod.mk
index 25dd3526..f9d6069b 100644
--- a/make/00_mod.mk
+++ b/make/00_mod.mk
@@ -61,10 +61,7 @@ helm_image_tag ?= $(oci_preflight_image_tag)
# Allows us to replace the Helm values.yaml's image.repository and image.tag
# with the right values.
define helm_values_mutation_function
-$(YQ) \
- '( .image.repository = "$(helm_image_name)" ) | \
- ( .image.tag = "$(helm_image_tag)" )' \
- $1 --inplace
+echo "no mutations defined for this chart"
endef
golangci_lint_config := .golangci.yaml
@@ -72,3 +69,4 @@ go_header_file := /dev/null
include make/extra_tools.mk
include make/ark/00_mod.mk
+include make/ngts/00_mod.mk
diff --git a/make/02_mod.mk b/make/02_mod.mk
index 6973f66c..e18e6973 100644
--- a/make/02_mod.mk
+++ b/make/02_mod.mk
@@ -1,5 +1,6 @@
include make/test-unit.mk
include make/ark/02_mod.mk
+include make/ngts/02_mod.mk
GITHUB_OUTPUT ?= /dev/stderr
.PHONY: release
@@ -30,7 +31,7 @@ $(helm_chart_source_dir)/crd_bases/jetstack.io_venaficonnections.yaml: go.mod |
echo "# DO NOT EDIT: Use 'make generate-crds-venconn' to regenerate." >$@
$(GO) run ./make/connection_crd >>$@
-$(helm_chart_source_dir)/templates/venafi-connection-crd.without-validations.yaml: $(helm_chart_source_dir)/crd_bases/jetstack.io_venaficonnections.yaml $(helm_chart_source_dir)/crd_bases/crd.header.yaml $(helm_chart_source_dir)/crd_bases/crd.footer.yaml | $(NEEDS_YQ)
+$(helm_chart_source_dir)/templates/venafi-connection-crd.without-validations.yaml: $(helm_chart_source_dir)/crd_bases/jetstack.io_venaficonnections.yaml $(helm_chart_source_dir)/crd_bases/crd.header-without-validations.yaml $(helm_chart_source_dir)/crd_bases/crd.footer.yaml | $(NEEDS_YQ)
cat $(helm_chart_source_dir)/crd_bases/crd.header-without-validations.yaml >$@
$(YQ) -I2 '{"spec": .spec}' $< | $(YQ) 'del(.. | ."x-kubernetes-validations"?) | del(.metadata.creationTimestamp)' | grep -v "DO NOT EDIT" >>$@
cat $(helm_chart_source_dir)/crd_bases/crd.footer.yaml >>$@
@@ -40,10 +41,7 @@ $(helm_chart_source_dir)/templates/venafi-connection-crd.yaml: $(helm_chart_sour
$(YQ) -I2 '{"spec": .spec}' $< | $(YQ) 'del(.metadata.creationTimestamp)' | grep -v "DO NOT EDIT" >>$@
cat $(helm_chart_source_dir)/crd_bases/crd.footer.yaml >>$@
-# The generate-crds target doesn't need to be run anymore when running
-# "generate". Let's replace it with "generate-crds-venconn".
-shared_generate_targets := $(filter-out generate-crds,$(shared_generate_targets))
-shared_generate_targets += generate-crds-venconn
+shared_generate_targets_dirty += generate-crds-venconn
.PHONY: test-e2e-gke
## Run a basic E2E test on a GKE cluster
@@ -54,17 +52,12 @@ shared_generate_targets += generate-crds-venconn
test-e2e-gke: | $(NEEDS_HELM) $(NEEDS_STEP) $(NEEDS_VENCTL)
./hack/e2e/test.sh
-.PHONY: test-helm
-## Run `helm unittest`.
-## @category Testing
-test-helm: | $(NEEDS_HELM-UNITTEST)
- $(HELM-UNITTEST) ./deploy/charts/{venafi-kubernetes-agent,disco-agent}
-
.PHONY: test-helm-snapshot
## Update the `helm unittest` snapshots.
+## Note that running helm unit tests is done through "make verify" using the Helm makefile-module
## @category Testing
test-helm-snapshot: | $(NEEDS_HELM-UNITTEST)
- $(HELM-UNITTEST) ./deploy/charts/{venafi-kubernetes-agent,disco-agent} -u
+ $(HELM-UNITTEST) ./deploy/charts/{venafi-kubernetes-agent,disco-agent,discovery-agent} -u
.PHONY: helm-plugins
## Install required helm plugins
diff --git a/make/_shared/generate-verify/02_mod.mk b/make/_shared/generate-verify/02_mod.mk
index f0677298..d4efdd8b 100644
--- a/make/_shared/generate-verify/02_mod.mk
+++ b/make/_shared/generate-verify/02_mod.mk
@@ -12,12 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+# Literal newline so the $(foreach)es below emit one $(MAKE) per recipe line.
+# Without this the dirty list expands to "make a make b make c" on a single
+# line, which under -j builds every goal in one parallel invocation.
+define _generate_verify_newline
+
+
+endef
+
.PHONY: generate
## Generate all generate targets.
## @category [shared] Generate/ Verify
generate: $$(shared_generate_targets)
@echo "The following targets cannot be run simultaneously with each other or other generate scripts:"
- $(foreach TARGET,$(shared_generate_targets_dirty), $(MAKE) $(TARGET))
+ $(foreach TARGET,$(shared_generate_targets_dirty),$(MAKE) $(TARGET)$(_generate_verify_newline))
verify_script := $(dir $(lastword $(MAKEFILE_LIST)))/util/verify.sh
@@ -36,4 +44,4 @@ verify_targets_dirty = $(sort $(verify_generated_targets_dirty) $(shared_verify_
## @category [shared] Generate/ Verify
verify: $$(verify_targets)
@echo "The following targets create temporary files in the current directory, that is why they have to be run last:"
- $(foreach TARGET,$(verify_targets_dirty), $(MAKE) $(TARGET))
+ $(foreach TARGET,$(verify_targets_dirty),$(MAKE) $(TARGET)$(_generate_verify_newline))
diff --git a/make/_shared/go/base/.github/workflows/govulncheck.yaml b/make/_shared/go/base/.github/workflows/govulncheck.yaml
index 5607fbc4..1e3667d4 100644
--- a/make/_shared/go/base/.github/workflows/govulncheck.yaml
+++ b/make/_shared/go/base/.github/workflows/govulncheck.yaml
@@ -20,7 +20,7 @@ jobs:
if: github.repository == '{{REPLACE:GH-REPOSITORY}}'
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Adding `fetch-depth: 0` makes sure tags are also fetched. We need
# the tags so `git describe` returns a valid version.
# see https://github.com/actions/checkout/issues/701 for extra info about this option
@@ -30,7 +30,7 @@ jobs:
run: |
make print-go-version >> "$GITHUB_OUTPUT"
- - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ steps.go-version.outputs.result }}
diff --git a/make/_shared/helm/helm.mk b/make/_shared/helm/helm.mk
index 6c84d1f7..c73fc3c3 100644
--- a/make/_shared/helm/helm.mk
+++ b/make/_shared/helm/helm.mk
@@ -115,6 +115,14 @@ verify-helm-values: | $(NEEDS_HELM-TOOL) $(NEEDS_GOJQ)
shared_verify_targets += verify-helm-values
+.PHONY: verify-helm-unittest
+## Run Helm chart unit tests using helm-unittest.
+## @category [shared] Generate/ Verify
+verify-helm-unittest: | $(NEEDS_HELM-UNITTEST)
+ $(HELM-UNITTEST) -f 'tests/**/*.yaml' $(helm_chart_source_dir)
+
+shared_verify_targets += verify-helm-unittest
+
$(bin_dir)/scratch/kyverno:
@mkdir -p $@
diff --git a/make/_shared/kind/00_kind_image_versions.mk b/make/_shared/kind/00_kind_image_versions.mk
index aab83879..c9e58770 100755
--- a/make/_shared/kind/00_kind_image_versions.mk
+++ b/make/_shared/kind/00_kind_image_versions.mk
@@ -15,16 +15,16 @@
# This file is auto-generated by the learn_kind_images.sh script in the makefile-modules repo.
# Do not edit manually.
-kind_image_kindversion := v0.30.0
+kind_image_kindversion := v0.32.0
-kind_image_kube_1.31_amd64 := docker.io/kindest/node:v1.31.12@sha256:9b0369c99755c4201e15618015138240182efd9cf2ba21351498d2e0176e8169
-kind_image_kube_1.31_arm64 := docker.io/kindest/node:v1.31.12@sha256:55d14ffa8767b87ae715fbb154cf13a356e5871f2bea87532a2f911fcc5b0103
-kind_image_kube_1.32_amd64 := docker.io/kindest/node:v1.32.8@sha256:284cc1c33c7170ea6cbfda225ad20484157c9781691c74451c4aaddf88d34114
-kind_image_kube_1.32_arm64 := docker.io/kindest/node:v1.32.8@sha256:71488e3e22f1923d2f6508950a7e9121a3bcb7090e12779468f961cdc2974fc2
-kind_image_kube_1.33_amd64 := docker.io/kindest/node:v1.33.4@sha256:59b665b0dc14d1fed52a9e172e4877bf00da1720aa2252128f91007544b0039d
-kind_image_kube_1.33_arm64 := docker.io/kindest/node:v1.33.4@sha256:5e665c9be32ef55fb307e16a6a8e643602bafb01363f4b34d45fa55427ee03cf
-kind_image_kube_1.34_amd64 := docker.io/kindest/node:v1.34.0@sha256:ede5bb6984830375d264e41b7169d03643f586d3e35316bf5229eaeb12bb18e0
-kind_image_kube_1.34_arm64 := docker.io/kindest/node:v1.34.0@sha256:3229b411042d457a44ea8807e90520e4b7da8700a8617ed20f0c4cb799bebe2b
+kind_image_kube_1.33_amd64 := docker.io/kindest/node:v1.33.12@sha256:c899edd3a9766ac69d5cccc9c8ee342e1920434a2d8eab0e02f461d36df96b41
+kind_image_kube_1.33_arm64 := docker.io/kindest/node:v1.33.12@sha256:0155d6fe15912ea2db47168aa91b2b1f880276a96dba320602d80ed3d1dd16dc
+kind_image_kube_1.34_amd64 := docker.io/kindest/node:v1.34.8@sha256:e9f9fd553828988bcae9c852223fb99f83380f251722a9313aa99f370bde7bfd
+kind_image_kube_1.34_arm64 := docker.io/kindest/node:v1.34.8@sha256:d413a5edd9be36fc8ea52529672fac7fdedb6708e84448b2029a37ad8e771c23
+kind_image_kube_1.35_amd64 := docker.io/kindest/node:v1.35.5@sha256:397bcc4ab091b9632fb3639d5cf020943ca40e90fe7bcc38409738a4a0d056ee
+kind_image_kube_1.35_arm64 := docker.io/kindest/node:v1.35.5@sha256:625f4633a546aba1e159ab56e52f9111b1b5044a165cc64ffe46d15d3dd0b0bf
+kind_image_kube_1.36_amd64 := docker.io/kindest/node:v1.36.1@sha256:21c46cf61fd45873f89e6a1bfcba4b7904dffa84c2bec88aeeca9a0409af4725
+kind_image_kube_1.36_arm64 := docker.io/kindest/node:v1.36.1@sha256:8d85371a247dc0fddce0d585d3de1ce40f2ee43f97fc0d0b6ee37b158771109e
-kind_image_latest_amd64 := $(kind_image_kube_1.34_amd64)
-kind_image_latest_arm64 := $(kind_image_kube_1.34_arm64)
+kind_image_latest_amd64 := $(kind_image_kube_1.36_amd64)
+kind_image_latest_arm64 := $(kind_image_kube_1.36_arm64)
diff --git a/make/_shared/oci-build/00_mod.mk b/make/_shared/oci-build/00_mod.mk
index a9c850f9..f7d6c185 100644
--- a/make/_shared/oci-build/00_mod.mk
+++ b/make/_shared/oci-build/00_mod.mk
@@ -14,11 +14,11 @@
# Use distroless as minimal base image to package the manager binary
# To get latest SHA run "crane digest quay.io/jetstack/base-static:latest"
-base_image_static := quay.io/jetstack/base-static@sha256:1da2e7de36c9d7a1931d765e8054a3c9fe7ed5126bacf728bb7429e923386146
+base_image_static := quay.io/jetstack/base-static@sha256:fc47ae209bb67bc8433e61e75fbf3ed836772cc5a9f6d37990184c6eb9a011a3
# Use custom apko-built image as minimal base image to package the manager binary
# To get latest SHA run "crane digest quay.io/jetstack/base-static-csi:latest"
-base_image_csi-static := quay.io/jetstack/base-static-csi@sha256:05ec9b9d5798fdd80680a54eab9eb69134d3cdaae948935bb1af07dadeb6e9be
+base_image_csi-static := quay.io/jetstack/base-static-csi@sha256:70ddf94fe35edb4715744b278889b0465c1b3ef67a18c9d04fff09d6650af610
# Utility functions
fatal_if_undefined = $(if $(findstring undefined,$(origin $1)),$(error $1 is not set))
diff --git a/make/_shared/repository-base/base/.github/workflows/make-self-upgrade.yaml b/make/_shared/repository-base/base/.github/workflows/make-self-upgrade.yaml
index 07857ebf..d7b0a707 100644
--- a/make/_shared/repository-base/base/.github/workflows/make-self-upgrade.yaml
+++ b/make/_shared/repository-base/base/.github/workflows/make-self-upgrade.yaml
@@ -38,7 +38,7 @@ jobs:
scope: '{{REPLACE:GH-REPOSITORY}}'
identity: make-self-upgrade
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Adding `fetch-depth: 0` makes sure tags are also fetched. We need
# the tags so `git describe` returns a valid version.
# see https://github.com/actions/checkout/issues/701 for extra info about this option
@@ -50,7 +50,7 @@ jobs:
run: |
make print-go-version >> "$GITHUB_OUTPUT"
- - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ steps.go-version.outputs.result }}
@@ -81,7 +81,7 @@ jobs:
git push -f origin "$SELF_UPGRADE_BRANCH"
- if: ${{ steps.is-up-to-date.outputs.result != 'true' }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.octo-sts.outputs.token }}
script: |
diff --git a/make/_shared/repository-base/base/OWNERS_ALIASES b/make/_shared/repository-base/base/OWNERS_ALIASES
index 672704c9..6de8798c 100644
--- a/make/_shared/repository-base/base/OWNERS_ALIASES
+++ b/make/_shared/repository-base/base/OWNERS_ALIASES
@@ -12,3 +12,4 @@ aliases:
- inteon
- thatsmrtalbot
- erikgb
+ - hjoshi123
diff --git a/make/_shared/tools/00_mod.mk b/make/_shared/tools/00_mod.mk
index 3767da09..780fbe26 100644
--- a/make/_shared/tools/00_mod.mk
+++ b/make/_shared/tools/00_mod.mk
@@ -30,6 +30,10 @@ endif
export DOWNLOAD_DIR ?= $(default_shared_dir)/downloaded
export GOVENDOR_DIR ?= $(default_shared_dir)/go_vendor
+# https://go.dev/dl/
+# renovate: datasource=golang-version packageName=go
+VENDORED_GO_VERSION := 1.26.4
+
$(bin_dir)/tools $(DOWNLOAD_DIR)/tools:
@mkdir -p $@
@@ -66,51 +70,54 @@ NEEDS_CTR = __require-ctr
tools :=
# https://github.com/helm/helm/releases
# renovate: datasource=github-releases packageName=helm/helm
-tools += helm=v4.0.1
+tools += helm=v4.2.0
+# https://github.com/helm-unittest/helm-unittest/releases
+# renovate: datasource=github-releases packageName=helm-unittest/helm-unittest
+tools += helm-unittest=v1.1.0
# https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl
# renovate: datasource=github-releases packageName=kubernetes/kubernetes
-tools += kubectl=v1.34.3
+tools += kubectl=v1.36.1
# https://github.com/kubernetes-sigs/kind/releases
# renovate: datasource=github-releases packageName=kubernetes-sigs/kind
-tools += kind=v0.30.0
+tools += kind=v0.32.0
# https://www.vaultproject.io/downloads
# renovate: datasource=github-releases packageName=hashicorp/vault
-tools += vault=v1.21.1
+tools += vault=v2.0.0
# https://github.com/Azure/azure-workload-identity/releases
# renovate: datasource=github-releases packageName=Azure/azure-workload-identity
tools += azwi=v1.5.1
# https://github.com/kyverno/kyverno/releases
# renovate: datasource=github-releases packageName=kyverno/kyverno
-tools += kyverno=v1.16.1
+tools += kyverno=v1.18.1
# https://github.com/mikefarah/yq/releases
# renovate: datasource=github-releases packageName=mikefarah/yq
-tools += yq=v4.49.2
+tools += yq=v4.53.2
# https://github.com/ko-build/ko/releases
# renovate: datasource=github-releases packageName=ko-build/ko
-tools += ko=0.18.0
+tools += ko=0.18.1
# https://github.com/protocolbuffers/protobuf/releases
# renovate: datasource=github-releases packageName=protocolbuffers/protobuf
-tools += protoc=v33.2
+tools += protoc=v35.0
# https://github.com/aquasecurity/trivy/releases
# renovate: datasource=github-releases packageName=aquasecurity/trivy
-tools += trivy=v0.68.1
+tools += trivy=v0.71.0
# https://github.com/vmware-tanzu/carvel-ytt/releases
# renovate: datasource=github-releases packageName=vmware-tanzu/carvel-ytt
-tools += ytt=v0.52.1
+tools += ytt=v0.55.1
# https://github.com/rclone/rclone/releases
# renovate: datasource=github-releases packageName=rclone/rclone
-tools += rclone=v1.72.0
+tools += rclone=v1.74.2
# https://github.com/istio/istio/releases
# renovate: datasource=github-releases packageName=istio/istio
-tools += istioctl=1.28.1
+tools += istioctl=1.30.0
### go packages
# https://pkg.go.dev/sigs.k8s.io/controller-tools/cmd/controller-gen?tab=versions
# renovate: datasource=go packageName=sigs.k8s.io/controller-tools
-tools += controller-gen=v0.19.0
+tools += controller-gen=v0.21.0
# https://pkg.go.dev/golang.org/x/tools/cmd/goimports?tab=versions
# renovate: datasource=go packageName=golang.org/x/tools
-tools += goimports=v0.40.0
+tools += goimports=v0.45.0
# https://pkg.go.dev/github.com/google/go-licenses/v2?tab=versions
# renovate: datasource=go packageName=github.com/inteon/go-licenses/v2
tools += go-licenses=v2.0.0-20250821024731-e4be79958780
@@ -119,28 +126,28 @@ tools += go-licenses=v2.0.0-20250821024731-e4be79958780
tools += gotestsum=v1.13.0
# https://pkg.go.dev/sigs.k8s.io/kustomize/kustomize/v5?tab=versions
# renovate: datasource=go packageName=sigs.k8s.io/kustomize/kustomize/v5
-tools += kustomize=v5.8.0
+tools += kustomize=v5.8.1
# https://pkg.go.dev/github.com/itchyny/gojq?tab=versions
# renovate: datasource=go packageName=github.com/itchyny/gojq
-tools += gojq=v0.12.18
+tools += gojq=v0.12.19
# https://pkg.go.dev/github.com/google/go-containerregistry/pkg/crane?tab=versions
# renovate: datasource=go packageName=github.com/google/go-containerregistry
-tools += crane=v0.20.7
+tools += crane=v0.21.5
# https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go?tab=versions
# renovate: datasource=go packageName=google.golang.org/protobuf
-tools += protoc-gen-go=v1.36.10
+tools += protoc-gen-go=v1.36.11
# https://pkg.go.dev/github.com/sigstore/cosign/v2/cmd/cosign?tab=versions
# renovate: datasource=go packageName=github.com/sigstore/cosign/v2
-tools += cosign=v2.6.1
+tools += cosign=v2.6.3
# https://pkg.go.dev/github.com/cert-manager/boilersuite?tab=versions
# renovate: datasource=go packageName=github.com/cert-manager/boilersuite
-tools += boilersuite=v0.1.0
+tools += boilersuite=v0.2.0
# https://pkg.go.dev/github.com/princjef/gomarkdoc/cmd/gomarkdoc?tab=versions
# renovate: datasource=go packageName=github.com/princjef/gomarkdoc
tools += gomarkdoc=v1.1.0
# https://pkg.go.dev/oras.land/oras/cmd/oras?tab=versions
# renovate: datasource=go packageName=oras.land/oras
-tools += oras=v1.3.0
+tools += oras=v1.3.2
# https://pkg.go.dev/github.com/onsi/ginkgo/v2/ginkgo?tab=versions
# The gingko version should be kept in sync with the version used in code.
# If there is no go.mod file (which is only the case for the makefile-modules
@@ -153,10 +160,10 @@ tools += ginkgo=$(detected_ginkgo_version)
tools += klone=v0.2.0
# https://pkg.go.dev/github.com/goreleaser/goreleaser/v2?tab=versions
# renovate: datasource=go packageName=github.com/goreleaser/goreleaser/v2
-tools += goreleaser=v2.13.1
+tools += goreleaser=v2.16.0
# https://pkg.go.dev/github.com/anchore/syft/cmd/syft?tab=versions
# renovate: datasource=go packageName=github.com/anchore/syft
-tools += syft=v1.38.2
+tools += syft=v1.45.0
# https://github.com/cert-manager/helm-tool/releases
# renovate: datasource=github-releases packageName=cert-manager/helm-tool
tools += helm-tool=v0.5.3
@@ -165,31 +172,31 @@ tools += helm-tool=v0.5.3
tools += image-tool=v0.1.0
# https://github.com/cert-manager/cmctl/releases
# renovate: datasource=github-releases packageName=cert-manager/cmctl
-tools += cmctl=v2.4.0
+tools += cmctl=v2.5.0
# https://pkg.go.dev/github.com/cert-manager/release/cmd/cmrel?tab=versions
# renovate: datasource=go packageName=github.com/cert-manager/release
tools += cmrel=v1.12.15-0.20241121151736-e3cbe5171488
# https://pkg.go.dev/github.com/golangci/golangci-lint/v2/cmd/golangci-lint?tab=versions
# renovate: datasource=go packageName=github.com/golangci/golangci-lint/v2
-tools += golangci-lint=v2.7.2
+tools += golangci-lint=v2.12.2
# https://pkg.go.dev/golang.org/x/vuln?tab=versions
# renovate: datasource=go packageName=golang.org/x/vuln
-tools += govulncheck=v1.1.4
+tools += govulncheck=v1.3.0
# https://github.com/operator-framework/operator-sdk/releases
# renovate: datasource=github-releases packageName=operator-framework/operator-sdk
-tools += operator-sdk=v1.42.0
+tools += operator-sdk=v1.42.2
# https://pkg.go.dev/github.com/cli/cli/v2?tab=versions
# renovate: datasource=go packageName=github.com/cli/cli/v2
-tools += gh=v2.83.1
+tools += gh=v2.93.0
# https://github.com/redhat-openshift-ecosystem/openshift-preflight/releases
# renovate: datasource=github-releases packageName=redhat-openshift-ecosystem/openshift-preflight
-tools += preflight=1.15.2
+tools += preflight=1.19.0
# https://github.com/daixiang0/gci/releases
# renovate: datasource=github-releases packageName=daixiang0/gci
-tools += gci=v0.13.7
+tools += gci=v0.14.0
# https://github.com/google/yamlfmt/releases
# renovate: datasource=github-releases packageName=google/yamlfmt
-tools += yamlfmt=v0.20.0
+tools += yamlfmt=v0.21.0
# https://github.com/yannh/kubeconform/releases
# renovate: datasource=github-releases packageName=yannh/kubeconform
tools += kubeconform=v0.7.0
@@ -197,7 +204,7 @@ tools += kubeconform=v0.7.0
# FIXME(erikgb): cert-manager needs the ability to override the version set here
# https://pkg.go.dev/k8s.io/code-generator/cmd?tab=versions
# renovate: datasource=go packageName=k8s.io/code-generator
-K8S_CODEGEN_VERSION ?= v0.34.3
+K8S_CODEGEN_VERSION ?= v0.36.1
tools += client-gen=$(K8S_CODEGEN_VERSION)
tools += deepcopy-gen=$(K8S_CODEGEN_VERSION)
tools += informer-gen=$(K8S_CODEGEN_VERSION)
@@ -207,11 +214,11 @@ tools += defaulter-gen=$(K8S_CODEGEN_VERSION)
tools += conversion-gen=$(K8S_CODEGEN_VERSION)
# https://github.com/kubernetes/kube-openapi
# renovate: datasource=go packageName=k8s.io/kube-openapi
-tools += openapi-gen=v0.0.0-20251125145642-4e65d59e963e
+tools += openapi-gen=v0.0.0-20260520065146-aa012df4f4af
# https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/master/envtest-releases.yaml
# FIXME: Find a way to configure Renovate to suggest upgrades
-KUBEBUILDER_ASSETS_VERSION := v1.34.1
+KUBEBUILDER_ASSETS_VERSION := v1.36.0
tools += etcd=$(KUBEBUILDER_ASSETS_VERSION)
tools += kube-apiserver=$(KUBEBUILDER_ASSETS_VERSION)
@@ -219,10 +226,6 @@ tools += kube-apiserver=$(KUBEBUILDER_ASSETS_VERSION)
ADDITIONAL_TOOLS ?=
tools += $(ADDITIONAL_TOOLS)
-# https://go.dev/dl/
-# renovate: datasource=golang-version packageName=go
-VENDORED_GO_VERSION := 1.25.5
-
# Print the go version which can be used in GH actions
.PHONY: print-go-version
print-go-version:
@@ -468,10 +471,10 @@ $(call for_each_kv,go_dependency,$(go_dependencies))
# File downloads #
##################
-go_linux_amd64_SHA256SUM=9e9b755d63b36acf30c12a9a3fc379243714c1c6d3dd72861da637f336ebb35b
-go_linux_arm64_SHA256SUM=b00b694903d126c588c378e72d3545549935d3982635ba3f7a964c9fa23fe3b9
-go_darwin_amd64_SHA256SUM=b69d51bce599e5381a94ce15263ae644ec84667a5ce23d58dc2e63e2c12a9f56
-go_darwin_arm64_SHA256SUM=bed8ebe824e3d3b27e8471d1307f803fc6ab8e1d0eb7a4ae196979bd9b801dd3
+go_linux_amd64_SHA256SUM=1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f
+go_linux_arm64_SHA256SUM=ef758ae7c6cf9267c9c0ef080b8965f453d89ab2d25d9eb22de4405925238768
+go_darwin_amd64_SHA256SUM=05dc9b5f9997744520aaebb3d5deaa7c755371aebbfb7f97c2511a9f3367538d
+go_darwin_arm64_SHA256SUM=b62ad2b6d7d2464f12a5bcad7ff47f19d08325773b5efd21610e445a05a9bf53
.PRECIOUS: $(DOWNLOAD_DIR)/tools/go@$(VENDORED_GO_VERSION)_$(HOST_OS)_$(HOST_ARCH).tar.gz
$(DOWNLOAD_DIR)/tools/go@$(VENDORED_GO_VERSION)_$(HOST_OS)_$(HOST_ARCH).tar.gz: | $(DOWNLOAD_DIR)/tools
@@ -479,10 +482,10 @@ $(DOWNLOAD_DIR)/tools/go@$(VENDORED_GO_VERSION)_$(HOST_OS)_$(HOST_ARCH).tar.gz:
$(CURL) https://go.dev/dl/go$(VENDORED_GO_VERSION).$(HOST_OS)-$(HOST_ARCH).tar.gz -o $(outfile); \
$(checkhash_script) $(outfile) $(go_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM)
-helm_linux_amd64_SHA256SUM=e0365548f01ed52a58a1181ad310b604a3244f59257425bb1739499372bdff60
-helm_linux_arm64_SHA256SUM=959fa52d34e2e1f0154e3220ed5f22263c8593447647a43af07890bba4b004d1
-helm_darwin_amd64_SHA256SUM=a8d1ca46c3ff5484b2b635dfc25832add4f36fdd09cf2a36fb709829c05b4112
-helm_darwin_arm64_SHA256SUM=8e0b9615cf72a62faaa0cfc0e22115f05bcddfd3d7ee58406ef97bc1ba563ae8
+helm_linux_amd64_SHA256SUM=97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096
+helm_linux_arm64_SHA256SUM=1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670
+helm_darwin_amd64_SHA256SUM=1376ea697140e4db316736e760d5a47d12afc1524dce704476ef06fd7fdeddc6
+helm_darwin_arm64_SHA256SUM=f13f959015447b6bc309f9fd506509926543988a39035c088b52522ec95e2acb
.PRECIOUS: $(DOWNLOAD_DIR)/tools/helm@$(HELM_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/helm@$(HELM_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -493,10 +496,30 @@ $(DOWNLOAD_DIR)/tools/helm@$(HELM_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD
chmod +x $(outfile); \
rm -f $(outfile).tar.gz
-kubectl_linux_amd64_SHA256SUM=ab60ca5f0fd60c1eb81b52909e67060e3ba0bd27e55a8ac147cbc2172ff14212
-kubectl_linux_arm64_SHA256SUM=46913a7aa0327f6cc2e1cc2775d53c4a2af5e52f7fd8dacbfbfd098e757f19e9
-kubectl_darwin_amd64_SHA256SUM=657afbd0e653c4ce3af1b5a645a4eaba282cf8eb2bcda7191ff60866e50e4d7f
-kubectl_darwin_arm64_SHA256SUM=e51367d2107d605f4edd7c2fb25897b0c0695a7de1a9f9d04cd6c9356b890b14
+helm-unittest_linux_amd64_SHA256SUM=3a8adc16a6d56cb2176b3668b4c4a3ec55130a8a8a52ae78804fd9b12ddf3418
+helm-unittest_linux_arm64_SHA256SUM=f75a6e428e24cdd2809d9a5cc8e484e6cfb0dfc145369731279dcb81c311d43c
+helm-unittest_darwin_amd64_SHA256SUM=debe7ec6dd960da56a59d87fcbbe037d17d96e1054d2b369a272be109b83ad1a
+helm-unittest_darwin_arm64_SHA256SUM=46bcde358c027b7122b0f910c4b786b91f9a975e8c6764ce697eed8ec272c0ae
+
+# helm-unittest uses "macos" instead of "darwin" in release filenames
+helm_unittest_os := $(HOST_OS)
+ifeq ($(HOST_OS),darwin)
+helm_unittest_os := macos
+endif
+
+.PRECIOUS: $(DOWNLOAD_DIR)/tools/helm-unittest@$(HELM-UNITTEST_VERSION)_$(HOST_OS)_$(HOST_ARCH)
+$(DOWNLOAD_DIR)/tools/helm-unittest@$(HELM-UNITTEST_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
+ @source $(lock_script) $@; \
+ $(CURL) https://github.com/helm-unittest/helm-unittest/releases/download/$(HELM-UNITTEST_VERSION)/helm-unittest-$(helm_unittest_os)-$(HOST_ARCH)-$(HELM-UNITTEST_VERSION:v%=%).tgz -o $(outfile).tgz; \
+ $(checkhash_script) $(outfile).tgz $(helm-unittest_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
+ tar xfO $(outfile).tgz untt-$(helm_unittest_os)-$(HOST_ARCH) > $(outfile); \
+ chmod +x $(outfile); \
+ rm -f $(outfile).tgz
+
+kubectl_linux_amd64_SHA256SUM=629d3f410e09bf49b64ae7079f7f0bda1191efed311f7d37fdbab0ad5b0ec2b7
+kubectl_linux_arm64_SHA256SUM=59f7ee8e477fae658447607dc3c8790ac17a1b016c01c622c12070e969e2d4e7
+kubectl_darwin_amd64_SHA256SUM=b4973e90ebb00537d735b63d6f8293c1959156e6ff435f6a43c08aeaa1a2e7d7
+kubectl_darwin_arm64_SHA256SUM=9092778abaef3079449da4cd70ded0e4be112480c93efcdeace3155968d1d133
.PRECIOUS: $(DOWNLOAD_DIR)/tools/kubectl@$(KUBECTL_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/kubectl@$(KUBECTL_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -505,10 +528,10 @@ $(DOWNLOAD_DIR)/tools/kubectl@$(KUBECTL_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DO
$(checkhash_script) $(outfile) $(kubectl_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
chmod +x $(outfile)
-kind_linux_amd64_SHA256SUM=517ab7fc89ddeed5fa65abf71530d90648d9638ef0c4cde22c2c11f8097b8889
-kind_linux_arm64_SHA256SUM=7ea2de9d2d190022ed4a8a4e3ac0636c8a455e460b9a13ccf19f15d07f4f00eb
-kind_darwin_amd64_SHA256SUM=4f0b6e3b88bdc66d922c08469f05ef507d4903dd236e6319199bb9c868eed274
-kind_darwin_arm64_SHA256SUM=ceaf40df1d1551c481fb50e3deb5c3deecad5fd599df5469626b70ddf52a1518
+kind_linux_amd64_SHA256SUM=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
+kind_linux_arm64_SHA256SUM=b92cd615e97585de8ddade28ed5cd7feb4248d717c233eea5b03c37298900f5d
+kind_darwin_amd64_SHA256SUM=295ac6d0d634c9819c9907df45e3017d1f13166bd13c3404c45e79f7faa47498
+kind_darwin_arm64_SHA256SUM=dca67911095a110c2b5c36e26df6cac860c602033e456c0db47be498cdef1ebb
.PRECIOUS: $(DOWNLOAD_DIR)/tools/kind@$(KIND_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/kind@$(KIND_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -517,10 +540,10 @@ $(DOWNLOAD_DIR)/tools/kind@$(KIND_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD
$(checkhash_script) $(outfile) $(kind_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
chmod +x $(outfile)
-vault_linux_amd64_SHA256SUM=4088617653eba4ea341b6166130239fcbe42edc7839c7f7c6209d280948769c7
-vault_linux_arm64_SHA256SUM=f83f541e4293289bf1cc3f1e62e41a29a9ce20aeb9a152ada2b00ca42e7e856d
-vault_darwin_amd64_SHA256SUM=d33bb27a0ad194e79c2bed9cad198a1f1319d8ca68bc6c4e6f68212c734cda09
-vault_darwin_arm64_SHA256SUM=add728e2ca2101826de030b4da6de77cee5a61f3c9cde74f5628d63332bea0ab
+vault_linux_amd64_SHA256SUM=0367bdd46dd1fff1ff19fc44e60df48866515bb519c80527236b3808ea879ac2
+vault_linux_arm64_SHA256SUM=5f04207fd0fbabbb8c6cca494fdee96f81bb0a82e1176670649e1aeeaadf0281
+vault_darwin_amd64_SHA256SUM=4fe88b981fcf14917a5f1b1c1ffaf4f9231c3f646ab778ba44e71dfb80e5b234
+vault_darwin_arm64_SHA256SUM=3b8ad2cc6de8b6cc13e030465e83729aec1070ef91327a55be0a28af81a530bf
.PRECIOUS: $(DOWNLOAD_DIR)/tools/vault@$(VAULT_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/vault@$(VAULT_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -544,10 +567,10 @@ $(DOWNLOAD_DIR)/tools/azwi@$(AZWI_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD
tar xfO $(outfile).tar.gz azwi > $(outfile) && chmod 775 $(outfile); \
rm -f $(outfile).tar.gz
-kubebuilder_tools_linux_amd64_SHA256SUM=c8500090806ed5ce4064eeeb2a5666476a5168c1f4ff0eadd54fe59b22c4baa7
-kubebuilder_tools_linux_arm64_SHA256SUM=cb56759108ea15933abf79d8573bbf66cca8c13e20425d7bc9f95941a060649d
-kubebuilder_tools_darwin_amd64_SHA256SUM=84d47d6c3a2fa4d14571249b4cccfafad1eb77087bb680693553b438b8ec8c43
-kubebuilder_tools_darwin_arm64_SHA256SUM=f25c213bc88582750935b370fa2c6108f0259b9c8f59ece5a82345d48858fc7d
+kubebuilder_tools_linux_amd64_SHA256SUM=d84f910bcefa3f6ab0205a49a7255672150c73b14bca3c36ac627db65040edf0
+kubebuilder_tools_linux_arm64_SHA256SUM=84df585fea6e5b5ce9034dc66e4ceffef0cd300999811ae1102aab00ee9b4da6
+kubebuilder_tools_darwin_amd64_SHA256SUM=1cbddd87af008b6bad1be5cf424ff88f7b5138489b488129723d1699c95cbd1b
+kubebuilder_tools_darwin_arm64_SHA256SUM=211e620e9f61085ac2e3a176a4f4fc5ebc60d40be1dae9ab5e35895f0c748700
.PRECIOUS: $(DOWNLOAD_DIR)/tools/kubebuilder_tools_$(KUBEBUILDER_ASSETS_VERSION)_$(HOST_OS)_$(HOST_ARCH).tar.gz
$(DOWNLOAD_DIR)/tools/kubebuilder_tools_$(KUBEBUILDER_ASSETS_VERSION)_$(HOST_OS)_$(HOST_ARCH).tar.gz: | $(DOWNLOAD_DIR)/tools
@@ -565,10 +588,10 @@ $(DOWNLOAD_DIR)/tools/kube-apiserver@$(KUBEBUILDER_ASSETS_VERSION)_$(HOST_OS)_$(
@source $(lock_script) $@; \
tar xfO $< controller-tools/envtest/kube-apiserver > $(outfile) && chmod 775 $(outfile)
-kyverno_linux_amd64_SHA256SUM=0c0216e4c3bb535eaf94ea1c2e13e4d66f7be2ec6446c37aee6c3133650167e7
-kyverno_linux_arm64_SHA256SUM=c1d349a272c2adf1bc9d2caf23a354ff4edc10687664c7a04da6fb84ce502c20
-kyverno_darwin_amd64_SHA256SUM=7985d522952e88adf7f21058439099b0e27c099baab0589b3a501862daebe842
-kyverno_darwin_arm64_SHA256SUM=25a704a74683a3da5bb50cb9e7a11a4df686121674d1271f49c0261618c94f1d
+kyverno_linux_amd64_SHA256SUM=5e6bba9ca85beec6c93e94ca7fb0972a66df3b2e67636a08bef090cd3fc6535c
+kyverno_linux_arm64_SHA256SUM=55eb60200925bf878b020e8af8771ce800d85d2186724a93155058c103ce6bf9
+kyverno_darwin_amd64_SHA256SUM=c0d343842a6f630c20f0581d4c5618a8cbef2f3a7bfc935866771af6080c59d7
+kyverno_darwin_arm64_SHA256SUM=40d957b4b05be802b4872858e5599ecf3f383949965166fded77c7acd8e9813e
.PRECIOUS: $(DOWNLOAD_DIR)/tools/kyverno@$(KYVERNO_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/kyverno@$(KYVERNO_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -582,10 +605,10 @@ $(DOWNLOAD_DIR)/tools/kyverno@$(KYVERNO_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DO
chmod +x $(outfile); \
rm -f $(outfile).tar.gz
-yq_linux_amd64_SHA256SUM=be2c0ddcf426b6a231648610ec5d1666ae50e9f6473e82f6486f9f4cb6e3e2f7
-yq_linux_arm64_SHA256SUM=783aa3c3beedcf2bf4aaf6262eca38b92a16d3ea31e2218ca80ba8ec7226b248
-yq_darwin_amd64_SHA256SUM=c14cd4ae68d42074e58463f5ebdbc3c49ec27c6de6a23b4af58a483bc3f15aa0
-yq_darwin_arm64_SHA256SUM=b0b70ede2b392ba02091b8137b42db819a7968cf232d595dd7394ac5668b4a0b
+yq_linux_amd64_SHA256SUM=d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b
+yq_linux_arm64_SHA256SUM=03061b2a50c7a498de2bbb92d7cb078ce433011f085a4994117c2726be4106ea
+yq_darwin_amd64_SHA256SUM=616b0a0f6a5b79d746f05a169c2b9bb40dee00c605ef165b9a1c1681bba738ac
+yq_darwin_arm64_SHA256SUM=541ba2287560df70f561955e2d7f7e1cd00cf2a15a884f6b5c87a4bfa887bc07
.PRECIOUS: $(DOWNLOAD_DIR)/tools/yq@$(YQ_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/yq@$(YQ_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -594,10 +617,10 @@ $(DOWNLOAD_DIR)/tools/yq@$(YQ_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR
$(checkhash_script) $(outfile) $(yq_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
chmod +x $(outfile)
-ko_linux_amd64_SHA256SUM=ce8c8776b243357e0a822c279b06c34302460221e834765dee5f4e9e2c0b7b38
-ko_linux_arm64_SHA256SUM=cf9abbdcc4fb7cf85f5e5ba029eba257ee98ef9410bcef94fae17056ec32bab5
-ko_darwin_amd64_SHA256SUM=066013c67e6e4b7c5f7c1a6b3c93ba66989e47de435558ff7edb875608028668
-ko_darwin_arm64_SHA256SUM=2efa5796986e38994a3a233641b98404fa071a76456e3c99b3c00df0436d5833
+ko_linux_amd64_SHA256SUM=048ab11818089a43b7b74bc554494a79a3fd0d9822c061142e5cd3cf8b30cb27
+ko_linux_arm64_SHA256SUM=9a26698876892128952fa3d038a4e99bea961d0d225865c60474b79e3db12e99
+ko_darwin_amd64_SHA256SUM=0e0dd8fddbefebb8572ece4dca8f07a7472de862fedd7e9845fd9d651e0d5dbe
+ko_darwin_arm64_SHA256SUM=752a639e0fbc013a35a43974b5ed87e7008bc2aee4952dfd2cc19f0013205492
.PRECIOUS: $(DOWNLOAD_DIR)/tools/ko@$(KO_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/ko@$(KO_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -612,10 +635,10 @@ $(DOWNLOAD_DIR)/tools/ko@$(KO_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR
chmod +x $(outfile); \
rm -f $(outfile).tar.gz
-protoc_linux_amd64_SHA256SUM=b24b53f87c151bfd48b112fe4c3a6e6574e5198874f38036aff41df3456b8caf
-protoc_linux_arm64_SHA256SUM=706662a332683aa2fffe1c4ea61588279d31679cd42d91c7d60a69651768edb8
-protoc_darwin_amd64_SHA256SUM=dba51cfcc85076d56e7de01a647865c5a7f995c3dce427d5215b53e50b7be43f
-protoc_darwin_arm64_SHA256SUM=5be1427127788c9f7dd7d606c3e69843dd3587327dea993917ffcb77e7234b44
+protoc_linux_amd64_SHA256SUM=a45cda0989c17dd950db55f6fbe1e5814c50fda08e87aa422980ac1f89dddbbc
+protoc_linux_arm64_SHA256SUM=36b518ac14d90351cc6598228ed2bbe5afe4e357b1af470b07e0ec1609875de2
+protoc_darwin_amd64_SHA256SUM=3580c2d115fccb5b0239960c8f70f8da14787b1973a46b2f39c315ad71c11e01
+protoc_darwin_arm64_SHA256SUM=45444963204757fd3e2fbe304bc1fdadfb488d8556ff099c4cc06575eab88976
.PRECIOUS: $(DOWNLOAD_DIR)/tools/protoc@$(PROTOC_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/protoc@$(PROTOC_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -630,10 +653,10 @@ $(DOWNLOAD_DIR)/tools/protoc@$(PROTOC_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWN
chmod +x $(outfile); \
rm -f $(outfile).zip
-trivy_linux_amd64_SHA256SUM=63e37242088e418651931f891963c19554faa19f0591fe6b40b606152051df2f
-trivy_linux_arm64_SHA256SUM=b29ea550f573afbcae3c86fb2b5e0ebba76b7cb0965e3787c4e8cb884d2c1d57
-trivy_darwin_amd64_SHA256SUM=d5b5bd3b3c3626d223c3981cc40f4709f00a6327a681b588d2fc64a3aa9d02c5
-trivy_darwin_arm64_SHA256SUM=4dd3d2e74e1b6f6f7fd5fbf55489727698f586d6a6a0cff3421031a05b80bcac
+trivy_linux_amd64_SHA256SUM=30a3d22b23f88c233f1658f562fb477cae3b3e8b4761109d515b7698daf85814
+trivy_linux_arm64_SHA256SUM=2561be394a3199c911f82fced606cbc05e1cb23eb6ce1da6935540adb76f4252
+trivy_darwin_amd64_SHA256SUM=4558afb13d017615ca85011901caab78b4f09196e320b05a56c9fdc5615a1428
+trivy_darwin_arm64_SHA256SUM=95d4e896b120816edd0995a2df8adca26a8621b7bf62036a89b1d54a7b718a74
.PRECIOUS: $(DOWNLOAD_DIR)/tools/trivy@$(TRIVY_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/trivy@$(TRIVY_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -648,10 +671,10 @@ $(DOWNLOAD_DIR)/tools/trivy@$(TRIVY_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLO
chmod +x $(outfile); \
rm $(outfile).tar.gz
-ytt_linux_amd64_SHA256SUM=490f138ae5b6864071d3c20a5a231e378cee7487cd4aeffc79dbf66718e65408
-ytt_linux_arm64_SHA256SUM=7d86bd3299e43d1455201fc213d698bae7482cd88f3e05de2f935e6eab842db9
-ytt_darwin_amd64_SHA256SUM=1975e52b3b97bd9be72f4efb714562da6a80cf181f036ae1f86eec215e208498
-ytt_darwin_arm64_SHA256SUM=a205f49267a44cd495e4c8b245754d8a216931a28ef29c78ae161c370a9b6117
+ytt_linux_amd64_SHA256SUM=3a2c925ed222f8db4956946d40279688edd6ceb3e919f03f919a8fc8b8532eda
+ytt_linux_arm64_SHA256SUM=ce61f7aee3f66f9b78d5781ef8528b7c8e199a2747796ef17a954118d3e65724
+ytt_darwin_amd64_SHA256SUM=b6a946878b74883c093bcc3e93960c68a6058a7e2be6ee2c78f1ba5f80fe3c02
+ytt_darwin_arm64_SHA256SUM=cf4d4afcf32e5cab1ba55a74f436c7e4bd04326c168a11be17078162629100e9
.PRECIOUS: $(DOWNLOAD_DIR)/tools/ytt@$(YTT_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/ytt@$(YTT_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -660,10 +683,10 @@ $(DOWNLOAD_DIR)/tools/ytt@$(YTT_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_D
$(checkhash_script) $(outfile) $(ytt_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
chmod +x $(outfile)
-rclone_linux_amd64_SHA256SUM=f3757aa829828c0f3359301bea25eef4d4fd62de735c47546ee6866c5b5545e2
-rclone_linux_arm64_SHA256SUM=c1669ef42d4ad65e3bb3f2cf0b2acf76cf0cbffefe463349a4f2244d8dbed701
-rclone_darwin_amd64_SHA256SUM=b1abd9e0287b19db435b7182faa0bc05478d6d412b839d7f819dee7ec4d9e5d0
-rclone_darwin_arm64_SHA256SUM=8396a06f793668da6cf0d8cf2e6a2da4c971bcbc7584286ffda7e3bf87f40148
+rclone_linux_amd64_SHA256SUM=72a806370072015ccbe4d81bcd348cc5eaf3beca6c65ba693fd43fb31fcca5b1
+rclone_linux_arm64_SHA256SUM=bc2b2eb8269b743ed7bcea869f3782cfb4931e41efa53fc8befc6dc8308b7a50
+rclone_darwin_amd64_SHA256SUM=fc24831eefa3918c278c4a10be4de78288422426e2f7e64509205167f845874d
+rclone_darwin_arm64_SHA256SUM=e170fc4f225cbe3685695c4761259fe5883115a2b022a2f39b7298f946b8d898
.PRECIOUS: $(DOWNLOAD_DIR)/tools/rclone@$(RCLONE_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/rclone@$(RCLONE_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -677,10 +700,10 @@ $(DOWNLOAD_DIR)/tools/rclone@$(RCLONE_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWN
chmod +x $(outfile); \
rm -f $(outfile).zip
-istioctl_linux_amd64_SHA256SUM=4e5d96f1efacd2186cd2ed664055e3ad90e8652a56f0303f812705c577c84f87
-istioctl_linux_arm64_SHA256SUM=1e156834e757b09a5048e50c50e177b05637f83a470eecf0878addd3ede0d09f
-istioctl_darwin_amd64_SHA256SUM=656e1f504d38cd209572dfdce9cb744f1122f248ed496feaddea9206f5a93c1b
-istioctl_darwin_arm64_SHA256SUM=24557042710431346d78a81c43881b3f54865b66f323c468c4d08398624fe1c3
+istioctl_linux_amd64_SHA256SUM=33664a95900d8cfc99b476cbbd7b967adc163b1981ef622d9b213a5d8156719e
+istioctl_linux_arm64_SHA256SUM=8a810443c0d85bb219bbe3902fc5a4e339a8c57d3a356e890bd6f0ee9cbbf467
+istioctl_darwin_amd64_SHA256SUM=72e79be133fb99b55a2eb28b9c2e1bc95c6faac008ec52ef4d705eb69c349c0f
+istioctl_darwin_arm64_SHA256SUM=e4f315c077ebe98c1ef0d820575743ebf80d8c3c754d81b0cda62f75f7d8fa75
.PRECIOUS: $(DOWNLOAD_DIR)/tools/istioctl@$(ISTIOCTL_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/istioctl@$(ISTIOCTL_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -694,10 +717,10 @@ $(DOWNLOAD_DIR)/tools/istioctl@$(ISTIOCTL_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(
chmod +x $(outfile); \
rm $(outfile).tar.gz
-preflight_linux_amd64_SHA256SUM=803684554991d64f8a06ccc7bfdd1f7c7f702921322297adab01da3e9886e5a8
-preflight_linux_arm64_SHA256SUM=d9bf232aa0ad44847e1f5d58143b4699aaa3d136f00a8418aef1404235a2e15a
-preflight_darwin_amd64_SHA256SUM=9b948767e70a973d1e8aa6c8c3f8529582c7cebc8d47b07605626b9552a50633
-preflight_darwin_arm64_SHA256SUM=4d397b6a70c3dc7358bfe669c29efcec334debfa589d6ae68296552094f77dc2
+preflight_linux_amd64_SHA256SUM=1d7a845c4528f9476c8ef9a551b4da5c06d62de558e56a37054c6fa737d583e5
+preflight_linux_arm64_SHA256SUM=09e59f31c1d13e30381260ddf64d9f46120131e490e94bd0e7a958ba1af0d6cb
+preflight_darwin_amd64_SHA256SUM=ec8c8be7a6fd48e2acf8c4630f75b6a8eae4fed0b5e76cf295f2bf6216a61440
+preflight_darwin_arm64_SHA256SUM=ec4ff1ec8b2369b6955121dada4c3d5389a6d1b5f9462758b94bbb04b79a530d
.PRECIOUS: $(DOWNLOAD_DIR)/tools/preflight@$(PREFLIGHT_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/preflight@$(PREFLIGHT_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
@@ -706,10 +729,10 @@ $(DOWNLOAD_DIR)/tools/preflight@$(PREFLIGHT_VERSION)_$(HOST_OS)_$(HOST_ARCH): |
$(checkhash_script) $(outfile) $(preflight_$(HOST_OS)_$(HOST_ARCH)_SHA256SUM); \
chmod +x $(outfile)
-operator-sdk_linux_amd64_SHA256SUM=5b730c233dbc8da816dde11ac96ff538929cb9a11aca93cb98d68fe63e89303a
-operator-sdk_linux_arm64_SHA256SUM=36ccecbfe6b4f22ca13bb6ae32d5f131f845357b51cabc01381a98a245ea8a37
-operator-sdk_darwin_amd64_SHA256SUM=2a2b03ae4e54d6e7fba42f89b7bdb366cf76ad33ce39967bde5775fbd0c0dba8
-operator-sdk_darwin_arm64_SHA256SUM=57d68ba70d8db64bc7f5bfa754623e0a08f81f85104254aff3774fd3baf88662
+operator-sdk_linux_amd64_SHA256SUM=8847c45ea994ac62b3cd134f77934df2a16a56a39a634eb988e0d1db99d1a413
+operator-sdk_linux_arm64_SHA256SUM=5fbb4c9f1eb3d8f6e9f870bfb48160842b9b541ce644d602282ef86578fedc1c
+operator-sdk_darwin_amd64_SHA256SUM=0293b988886b5a2a82b6c141c46293915f0c67cae43cabdb36a0ffdf8af042b6
+operator-sdk_darwin_arm64_SHA256SUM=8f7c19e35ce6ad4069502fcb66ea89548d0173ff8a02b253b0be4ad4909eeaf6
.PRECIOUS: $(DOWNLOAD_DIR)/tools/operator-sdk@$(OPERATOR-SDK_VERSION)_$(HOST_OS)_$(HOST_ARCH)
$(DOWNLOAD_DIR)/tools/operator-sdk@$(OPERATOR-SDK_VERSION)_$(HOST_OS)_$(HOST_ARCH): | $(DOWNLOAD_DIR)/tools
diff --git a/make/ark/00_mod.mk b/make/ark/00_mod.mk
index 8df9a152..e5b722eb 100644
--- a/make/ark/00_mod.mk
+++ b/make/ark/00_mod.mk
@@ -26,8 +26,5 @@ oci_ark_build_args := \
define ark_helm_values_mutation_function
-$(YQ) \
- '( .image.repository = "$(oci_ark_image_name)" ) | \
- ( .image.tag = "$(oci_ark_image_tag)" )' \
- $1 --inplace
+echo "no mutations defined for this chart"
endef
diff --git a/make/ark/02_mod.mk b/make/ark/02_mod.mk
index 96e66206..73b7d5e7 100644
--- a/make/ark/02_mod.mk
+++ b/make/ark/02_mod.mk
@@ -47,11 +47,11 @@ ark-test-e2e: $(NEEDS_KIND) $(NEEDS_KUBECTL) $(NEEDS_HELM)
## Verify the Helm chart
## @category CyberArk Discovery and Context
ark-verify:
- $(MAKE) verify-helm-lint verify-helm-values verify-pod-security-standards verify-helm-kubeconform \
+ INSTALL_OPTIONS="--set acceptTerms=true" $(MAKE) verify-helm-lint verify-helm-values verify-pod-security-standards verify-helm-kubeconform verify-helm-unittest \
helm_chart_source_dir=deploy/charts/disco-agent \
helm_chart_image_name=$(ARK_CHART)
-shared_verify_targets += ark-verify
+shared_verify_targets_dirty += ark-verify
.PHONY: ark-generate
## Generate Helm chart documentation and schema
diff --git a/make/extra_tools.mk b/make/extra_tools.mk
index 232985b5..78ad5e36 100644
--- a/make/extra_tools.mk
+++ b/make/extra_tools.mk
@@ -1,10 +1,6 @@
ADDITIONAL_TOOLS :=
ADDITIONAL_GO_DEPENDENCIES :=
-# https://pkg.go.dev/github.com/helm-unittest/helm-unittest?tab=versions
-ADDITIONAL_TOOLS += helm-unittest=v0.8.2
-ADDITIONAL_GO_DEPENDENCIES += helm-unittest=github.com/helm-unittest/helm-unittest/cmd/helm-unittest
-
ADDITIONAL_TOOLS += venctl=1.27.0
ADDITIONAL_TOOLS += step=0.28.2
diff --git a/make/ngts/00_mod.mk b/make/ngts/00_mod.mk
new file mode 100644
index 00000000..67c1935b
--- /dev/null
+++ b/make/ngts/00_mod.mk
@@ -0,0 +1,30 @@
+build_names += ngts
+go_ngts_main_dir := ./cmd/ark
+go_ngts_mod_dir := .
+go_ngts_ldflags := \
+ -X $(gomodule_name)/pkg/version.PreflightVersion=$(VERSION) \
+ -X $(gomodule_name)/pkg/version.Commit=$(GITCOMMIT) \
+ -X $(gomodule_name)/pkg/version.BuildDate=$(shell date "+%F-%T-%Z")
+
+oci_ngts_base_image_flavor := static
+oci_ngts_image_name := quay.io/jetstack/discovery-agent
+oci_ngts_image_tag := $(VERSION)
+oci_ngts_image_name_development := jetstack.local/discovery-agent
+
+# Annotations are the standardised set of annotations we set on every component we publish
+oci_ngts_build_args := \
+ --image-annotation="org.opencontainers.image.source"="https://github.com/jetstack/jetstack-secure" \
+ --image-annotation="org.opencontainers.image.vendor"="Palo Alto Networks" \
+ --image-annotation="org.opencontainers.image.licenses"="Apache-2.0" \
+ --image-annotation="org.opencontainers.image.authors"="Palo Alto Networks" \
+ --image-annotation="org.opencontainers.image.title"="Discovery Agent for NGTS" \
+ --image-annotation="org.opencontainers.image.description"="Gathers machine identity data from Kubernetes clusters for NGTS." \
+ --image-annotation="org.opencontainers.image.url"="https://www.paloaltonetworks.com/" \
+ --image-annotation="org.opencontainers.image.documentation"="https://docs.paloaltonetworks.com/" \
+ --image-annotation="org.opencontainers.image.version"="$(VERSION)" \
+ --image-annotation="org.opencontainers.image.revision"="$(GITCOMMIT)"
+
+
+define ngts_helm_values_mutation_function
+echo "no mutations defined for this chart"
+endef
diff --git a/make/ngts/02_mod.mk b/make/ngts/02_mod.mk
new file mode 100644
index 00000000..9f8124f9
--- /dev/null
+++ b/make/ngts/02_mod.mk
@@ -0,0 +1,79 @@
+# Makefile targets for NGTS Discovery Agent
+
+# The base OCI repository for all NGTS Discovery Agent artifacts
+NGTS_OCI_BASE ?= quay.io/jetstack
+
+# The OCI repository (without tag) for the NGTS Discovery Agent Docker image
+# Can be overridden when calling `make ngts-release` to push to a different repository.
+NGTS_IMAGE ?= $(NGTS_OCI_BASE)/discovery-agent
+
+# The OCI repository (without tag) for the NGTS Discovery Agent Helm chart
+# Can be overridden when calling `make ngts-release` to push to a different repository.
+NGTS_CHART ?= $(NGTS_OCI_BASE)/charts/discovery-agent
+
+# Used to output variables when running in GitHub Actions
+GITHUB_OUTPUT ?= /dev/stderr
+
+.PHONY: ngts-release
+## Publish all release artifacts (image + helm chart)
+## @category NGTS Discovery Agent
+ngts-release: oci_ngts_image_digest_path := $(bin_dir)/scratch/image/oci-layout-ngts.digests
+ngts-release: helm_digest_path := $(bin_dir)/scratch/helm/discovery-agent-$(helm_chart_version).digests
+ngts-release:
+ $(MAKE) oci-push-ngts helm-chart-oci-push \
+ oci_ngts_image_name="$(NGTS_IMAGE)" \
+ helm_image_name="$(NGTS_IMAGE)" \
+ helm_image_tag="$(oci_ngts_image_tag)" \
+ helm_chart_source_dir=deploy/charts/discovery-agent \
+ helm_chart_image_name="$(NGTS_CHART)"
+
+ @echo "NGTS_IMAGE=$(NGTS_IMAGE)" >> "$(GITHUB_OUTPUT)"
+ @echo "NGTS_IMAGE_TAG=$(oci_ngts_image_tag)" >> "$(GITHUB_OUTPUT)"
+ @echo "NGTS_IMAGE_DIGEST=$$(head -1 $(oci_ngts_image_digest_path))" >> "$(GITHUB_OUTPUT)"
+ @echo "NGTS_CHART=$(NGTS_CHART)" >> "$(GITHUB_OUTPUT)"
+ @echo "NGTS_CHART_TAG=$(helm_chart_version)" >> "$(GITHUB_OUTPUT)"
+ @echo "NGTS_CHART_DIGEST=$$(head -1 $(helm_digest_path))" >> "$(GITHUB_OUTPUT)"
+
+ @echo "Release complete!"
+
+.PHONY: ngts-test-e2e
+## Run a basic E2E test on a Kind cluster
+## See `hack/ngts/e2e.sh` for the full test script.
+## @category NGTS Discovery Agent
+ngts-test-e2e: $(NEEDS_KIND) $(NEEDS_KUBECTL) $(NEEDS_HELM) $(NEEDS_YQ)
+ PATH="$(bin_dir)/tools:${PATH}" ./hack/ngts/test-e2e.sh
+
+.PHONY: ngts-verify
+## Verify the Helm chart
+## @category NGTS Discovery Agent
+ngts-verify:
+ INSTALL_OPTIONS="--set-string config.tsgID=1234123412 --set config.clusterName=foo" $(MAKE) verify-helm-lint verify-helm-values verify-pod-security-standards verify-helm-kubeconform verify-helm-unittest \
+ helm_chart_source_dir=deploy/charts/discovery-agent \
+ helm_chart_image_name=$(NGTS_CHART)
+
+shared_verify_targets_dirty += ngts-verify
+
+.PHONY: ngts-generate
+## Generate Helm chart documentation, schema and the VenafiConnection CRD
+## @category NGTS Discovery Agent
+ngts-generate:
+ $(MAKE) generate-helm-docs generate-helm-schema generate-crds-venconn \
+ helm_chart_source_dir=deploy/charts/discovery-agent
+
+shared_generate_targets_dirty += ngts-generate
+
+.PHONY: list-discovery-resources
+## Dump all discovery-agent k8s-dynamic resource types to a markdown list
+## @category NGTS Discovery Agent
+list-discovery-resources: $(NEEDS_HELM) $(NEEDS_YQ)
+ @# First, template out the chart using dummy values for the required values
+ @# Then extract config.yaml, and extract all "k8s-dynamic" data gatherers
+ @# Then print "- [group/]version resource" with the first letter of resource captialised in awk
+ @$(HELM) template discovery-agent deploy/charts/discovery-agent \
+ --set-string config.tsgID=1234123412 \
+ --set config.clusterName=foo | \
+ $(YQ) '.data."config.yaml"' | \
+ $(YQ) '.data-gatherers[] | select(.kind == "k8s-dynamic") | .config.resource-type' -o json | \
+ jq -r 'if .group then "\(.group)/\(.version) \(.resource)" else "\(.version) \(.resource)" end' | \
+ awk '{$$NF = toupper(substr($$NF,1,1)) substr($$NF,2); print "- " $$0}' | \
+ sort
diff --git a/pkg/agent/config.go b/pkg/agent/config.go
index ed72afe4..1c8e7335 100644
--- a/pkg/agent/config.go
+++ b/pkg/agent/config.go
@@ -22,6 +22,7 @@ import (
"github.com/jetstack/preflight/pkg/datagatherer/k8sdiscovery"
"github.com/jetstack/preflight/pkg/datagatherer/k8sdynamic"
"github.com/jetstack/preflight/pkg/datagatherer/local"
+ "github.com/jetstack/preflight/pkg/datagatherer/oidc"
"github.com/jetstack/preflight/pkg/kubeconfig"
"github.com/jetstack/preflight/pkg/logs"
"github.com/jetstack/preflight/pkg/version"
@@ -40,8 +41,9 @@ type Config struct {
// Server is the base URL for the Preflight server. It defaults to
// https://preflight.jetstack.io in Jetstack Secure OAuth and Jetstack
// Secure API Token modes, and https://api.venafi.cloud in Venafi Cloud Key
- // Pair Service Account mode. It is ignored in Venafi Cloud VenafiConnection
- // mode and in MachineHub mode.
+ // Pair Service Account mode. It is ignored in VenafiConnection mode (the
+ // backend URL is taken from the VenafiConnection resource), in NGTS mode
+ // (use --tsg-id or --ngts-server-url instead) and in MachineHub mode.
Server string `yaml:"server"`
// OrganizationID is only used in Jetstack Secure OAuth and Jetstack Secure
@@ -54,9 +56,13 @@ type Config struct {
ClusterName string `yaml:"cluster_name"`
// ClusterDescription is a short description of the Kubernetes cluster where the
// agent is running.
- ClusterDescription string `yaml:"cluster_description"`
- DataGatherers []DataGatherer `yaml:"data-gatherers"`
- VenafiCloud *VenafiCloudConfig `yaml:"venafi-cloud,omitempty"`
+ ClusterDescription string `yaml:"cluster_description"`
+ // ClaimableCerts controls whether discovered certs can be claimed by other tenants.
+ // true = certs are left unassigned, available for any tenant to claim.
+ // false (default) = certs are owned by this cluster's tenant.
+ ClaimableCerts bool `yaml:"claimable_certs"`
+ DataGatherers []DataGatherer `yaml:"data-gatherers"`
+ VenafiCloud *VenafiCloudConfig `yaml:"venafi-cloud,omitempty"`
// For testing purposes.
InputPath string `yaml:"input-path"`
@@ -155,7 +161,9 @@ type AgentCmdFlags struct {
APIToken string
// VenConnName (--venafi-connection) is the name of the VenafiConnection
- // resource to use. Using this flag will enable Venafi Connection mode.
+ // resource to use. Using this flag will enable VenafiConnection mode. The
+ // upload backend (Venafi Cloud or NGTS) is selected by the spec of the
+ // referenced VenafiConnection resource.
VenConnName string
// VenConnNS (--venafi-connection-namespace) is the namespace of the
@@ -177,6 +185,25 @@ type AgentCmdFlags struct {
// Prometheus (--enable-metrics) enables the Prometheus metrics server.
Prometheus bool
+
+ // NGTSMode (--ngts) turns on the NGTS keypair mode. The agent will
+ // authenticate to the NGTS endpoint using an NGTS built-in service account
+ // key pair (--client-id and --private-key-path).
+ //
+ // To authenticate to NGTS through a VenafiConnection resource instead, use
+ // --venafi-connection (without --ngts) and point it at a VenafiConnection
+ // whose spec selects the NGTS backend.
+ NGTSMode bool
+
+ // TSGID (--tsg-id) is the TSG (Tenant Service Group) ID for NGTS mode.
+ // The production NGTS server URL is derived from this value. Mutually
+ // exclusive with --ngts-server-url.
+ TSGID string
+
+ // NGTSServerURL (--ngts-server-url) is a hidden flag for developers to
+ // point the agent at a custom NGTS server URL for testing purposes.
+ // Mutually exclusive with --tsg-id.
+ NGTSServerURL string
}
func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) {
@@ -272,8 +299,10 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) {
&cfg.VenConnName,
"venafi-connection",
"",
- "Turns on the "+string(VenafiCloudVenafiConnection)+" mode. "+
- "This flag configures the name of the VenafiConnection to be used.",
+ "Turns on the "+string(VenafiConnection)+" mode. The upload backend (Venafi Cloud "+
+ "or NGTS) is selected by the spec of the referenced VenafiConnection "+
+ "resource. This flag configures the name of the VenafiConnection to "+
+ "be used.",
)
c.PersistentFlags().StringVar(
&cfg.VenConnNS,
@@ -329,6 +358,38 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) {
panic(err)
}
+ c.PersistentFlags().BoolVar(
+ &cfg.NGTSMode,
+ "ngts",
+ false,
+ "Enables NGTS keypair mode. The agent will authenticate to NGTS using an NGTS built-in service account key pair. "+
+ "Must be used with --private-key-path and exactly one of --tsg-id or --ngts-server-url. "+
+ "--client-id is optional if provided in the credentials secret. "+
+ "To authenticate to NGTS through a VenafiConnection resource instead, use --venafi-connection without this flag.",
+ )
+ c.PersistentFlags().StringVar(
+ &cfg.TSGID,
+ "tsg-id",
+ "",
+ "The TSG (Tenant Service Group) ID for NGTS mode. The production NGTS server URL is derived from this value. "+
+ "Mutually exclusive with --ngts-server-url; exactly one must be provided when using --ngts.",
+ )
+
+ ngtsServerURLFlag := "ngts-server-url"
+
+ c.PersistentFlags().StringVar(
+ &cfg.NGTSServerURL,
+ ngtsServerURLFlag,
+ "",
+ "Override the NGTS server URL for testing purposes. This flag is intended for agent development and should not need to be set. "+
+ "Mutually exclusive with --tsg-id.",
+ )
+
+ // ngts-server-url is intended only for developers, so hide it from help
+ if err := c.PersistentFlags().MarkHidden(ngtsServerURLFlag); err != nil {
+ panic(err)
+ }
+
}
// OutputMode controls how the collected data is published.
@@ -336,12 +397,13 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) {
type OutputMode string
const (
- JetstackSecureOAuth OutputMode = "Jetstack Secure OAuth"
- JetstackSecureAPIToken OutputMode = "Jetstack Secure API Token"
- VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account"
- VenafiCloudVenafiConnection OutputMode = "Venafi Cloud VenafiConnection"
- LocalFile OutputMode = "Local File"
- MachineHub OutputMode = "MachineHub"
+ JetstackSecureOAuth OutputMode = "Jetstack Secure OAuth"
+ JetstackSecureAPIToken OutputMode = "Jetstack Secure API Token"
+ VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account"
+ VenafiConnection OutputMode = "VenafiConnection"
+ LocalFile OutputMode = "Local File"
+ MachineHub OutputMode = "MachineHub"
+ NGTS OutputMode = "NGTS"
)
// The command-line flags and the config file and some environment variables are
@@ -360,7 +422,9 @@ type CombinedConfig struct {
ClusterID string
// Used by JetstackSecureOAuth, JetstackSecureAPIToken, and
- // VenafiCloudKeypair. Ignored in VenafiCloudVenafiConnection mode.
+ // VenafiCloudKeypair. Ignored in VenafiConnection mode (the
+ // backend URL is taken from the VenafiConnection resource) and in NGTS
+ // mode (set via --tsg-id or --ngts-server-url instead).
Server string
// JetstackSecureOAuth and JetstackSecureAPIToken modes only.
@@ -378,14 +442,23 @@ type CombinedConfig struct {
// the agent is running.
ClusterDescription string
- // VenafiCloudVenafiConnection mode only.
+ // ClaimableCerts controls whether discovered certs can be claimed by other tenants.
+ // true = certs are left unassigned, available for any tenant to claim.
+ // false (default) = certs are owned by this cluster's tenant.
+ ClaimableCerts bool
+
+ // VenafiConnection mode only.
VenConnName string
VenConnNS string
- // VenafiCloudKeypair and VenafiCloudVenafiConnection modes only.
+ // Applied to all data gatherers regardless of OutputMode.
ExcludeAnnotationKeysRegex []*regexp.Regexp
ExcludeLabelKeysRegex []*regexp.Regexp
+ // NGTS mode only.
+ TSGID string
+ NGTSServerURL string
+
// Only used for testing purposes.
OutputPath string
InputPath string
@@ -410,6 +483,10 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
keysAndValues []any
)
switch {
+ case flags.NGTSMode:
+ mode = NGTS
+ reason = "--ngts was specified"
+ keysAndValues = []any{"ngts", true}
case flags.VenafiCloudMode && flags.CredentialsPath != "":
mode = VenafiCloudKeypair
reason = "--venafi-cloud and --credentials-path were specified"
@@ -426,7 +503,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
reason = "--client-id and --private-key-path were specified"
keysAndValues = []any{"clientID", flags.ClientID, "privateKeyPath", flags.PrivateKeyPath}
case flags.VenConnName != "":
- mode = VenafiCloudVenafiConnection
+ mode = VenafiConnection
reason = "--venafi-connection was specified"
keysAndValues = []any{"venConnName", flags.VenConnName}
case flags.APIToken != "":
@@ -447,8 +524,9 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
default:
return CombinedConfig{}, nil, fmt.Errorf("no output mode specified. " +
"To enable one of the output modes, you can:\n" +
+ " - Use --ngts with --private-key-path and exactly one of --tsg-id or --ngts-server-url to use the " + string(NGTS) + " mode (--client-id is optional if provided in the credentials secret).\n" +
" - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the " + string(VenafiCloudKeypair) + " mode.\n" +
- " - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" +
+ " - Use --venafi-connection for the " + string(VenafiConnection) + " mode (the upload backend - Venafi Cloud or NGTS - is selected by the VenafiConnection resource).\n" +
" - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" +
" - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" +
" - Use --machine-hub if you want to use the " + string(MachineHub) + " mode.\n" +
@@ -462,6 +540,60 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
var errs error
+ // Validation of NGTS mode requirements.
+ if res.OutputMode == NGTS {
+ switch {
+ case flags.TSGID != "" && flags.NGTSServerURL != "":
+ errs = multierror.Append(errs, fmt.Errorf("--tsg-id and --ngts-server-url are mutually exclusive; exactly one must be provided when using --ngts"))
+ case flags.TSGID == "" && flags.NGTSServerURL == "":
+ errs = multierror.Append(errs, fmt.Errorf("either --tsg-id or --ngts-server-url is required when using --ngts"))
+ }
+ if flags.PrivateKeyPath == "" {
+ errs = multierror.Append(errs, fmt.Errorf("--private-key-path is required when using --ngts"))
+ }
+
+ // Error if MachineHub mode is also enabled
+ if flags.MachineHubMode {
+ errs = multierror.Append(errs, fmt.Errorf("--machine-hub cannot be used with --ngts. These are mutually exclusive modes."))
+ }
+
+ // Error if VenafiConnection mode flags are used. The --ngts flag
+ // selects NGTS keypair auth; to authenticate to NGTS through a
+ // VenafiConnection resource, drop --ngts and use --venafi-connection
+ // on its own (the agent picks the NGTS backend from the
+ // VenafiConnection's spec).
+ if flags.VenConnName != "" {
+ errs = multierror.Append(errs, fmt.Errorf("--venafi-connection cannot be used with --ngts. Either drop --ngts to authenticate to NGTS through a VenafiConnection resource, or drop --venafi-connection and use --client-id and --private-key-path for NGTS keypair auth."))
+ }
+
+ // Error if Jetstack Secure OAuth mode flags are used
+ if !flags.VenafiCloudMode && flags.CredentialsPath != "" {
+ errs = multierror.Append(errs, fmt.Errorf("--credentials-file (for Jetstack Secure OAuth) cannot be used with --ngts. Use --client-id and --private-key-path instead."))
+ }
+
+ // Error if API Token mode is used
+ if flags.APIToken != "" {
+ errs = multierror.Append(errs, fmt.Errorf("--api-token cannot be used with --ngts. Use --client-id and --private-key-path instead."))
+ }
+
+ // Error if --venafi-cloud is used with --ngts
+ if flags.VenafiCloudMode {
+ errs = multierror.Append(errs, fmt.Errorf("--venafi-cloud cannot be used with --ngts. These are different deployment targets."))
+ }
+
+ // Error if organization_id or cluster_id are set in config (these are for Jetstack Secure / CM-SaaS)
+ if cfg.OrganizationID != "" {
+ errs = multierror.Append(errs, fmt.Errorf("organization_id in config file is not supported in NGTS mode. This field is only for Jetstack Secure."))
+ }
+
+ if cfg.ClusterID != "" {
+ errs = multierror.Append(errs, fmt.Errorf("cluster_id in config file is not supported in NGTS mode. Use cluster_name instead."))
+ }
+
+ res.TSGID = flags.TSGID
+ res.NGTSServerURL = flags.NGTSServerURL
+ }
+
// Validation and defaulting of `server` and the deprecated `endpoint.path`.
{
// Only relevant if using TLSPK backends
@@ -487,18 +619,44 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
case !hasServerField && !hasEndpointField:
server = "https://preflight.jetstack.io"
if res.OutputMode == VenafiCloudKeypair {
- // The VenafiCloudVenafiConnection mode doesn't need a server.
+ // VenafiConnection mode (VCP or NGTS) takes its server from
+ // the VenafiConnection resource and doesn't need one here.
server = client.VenafiCloudProdURL
}
+ if res.OutputMode == NGTS {
+ // In NGTS keypair mode, use NGTSServerURL if provided, otherwise
+ // we'll use a default (derived from --tsg-id at client construction
+ // time).
+ server = res.NGTSServerURL
+ }
}
+
+ // In NGTS mode: ignore the config-file server field entirely; use only
+ // --ngts-server-url when provided (default URL is derived from TSG ID
+ // at client construction time).
+ if res.OutputMode == NGTS {
+ if res.NGTSServerURL != "" {
+ log.Info("Using custom NGTS server URL (for testing)", "url", res.NGTSServerURL)
+ }
+
+ // config-file server field has no impact in NGTS mode so warn about it
+ if cfg.Server != "" {
+ log.Info(fmt.Sprintf("ignoring the server field in the config file. In %s mode, use --ngts-server-url for testing.", NGTS))
+ }
+
+ server = res.NGTSServerURL
+ }
+
url, urlErr := url.Parse(server)
- if urlErr != nil || url.Hostname() == "" {
+ if server != "" && (urlErr != nil || url.Hostname() == "") {
errs = multierror.Append(errs, fmt.Errorf("server %q is not a valid URL", server))
}
- if res.OutputMode == VenafiCloudVenafiConnection && server != "" {
- log.Info(fmt.Sprintf("ignoring the server field specified in the config file. In %s mode, this field is not needed.", VenafiCloudVenafiConnection))
+
+ if res.OutputMode == VenafiConnection && server != "" {
+ log.Info(fmt.Sprintf("ignoring the server field specified in the config file. In %s mode, this field is not needed.", VenafiConnection))
server = ""
}
+
res.Server = server
res.EndpointPath = endpointPath
}
@@ -519,7 +677,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
}
uploadPath = cfg.VenafiCloud.UploadPath
- case VenafiCloudVenafiConnection:
+ case VenafiConnection:
// The venafi-cloud.upload_path was initially meant to let users
// configure HTTP proxies, but it has never been used since HTTP
// proxies don't rewrite paths. Thus, we've disabled the ability to
@@ -529,6 +687,12 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.OutputMode))
}
uploadPath = ""
+ case NGTS:
+ // NGTS mode doesn't use the upload_path field
+ if cfg.VenafiCloud != nil && cfg.VenafiCloud.UploadPath != "" {
+ log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.OutputMode))
+ }
+ uploadPath = ""
}
res.UploadPath = uploadPath
}
@@ -554,7 +718,14 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
var clusterID string // Required by the old jetstack-secure mode deprecated for venafi cloud modes.
var organizationID string // Only used by the old jetstack-secure mode.
switch res.OutputMode { // nolint:exhaustive
- case VenafiCloudKeypair, VenafiCloudVenafiConnection:
+ case NGTS:
+ // NGTS mode requires cluster_name
+ if cfg.ClusterName == "" {
+ errs = multierror.Append(errs, fmt.Errorf("cluster_name is required in %s mode", res.OutputMode))
+ }
+ clusterName = cfg.ClusterName
+ // cluster_id and organization_id were already validated to not be present in NGTS mode
+ case VenafiCloudKeypair, VenafiConnection:
// For backwards compatibility, use the agent config's `cluster_id` as
// ClusterName if `cluster_name` is not set.
if cfg.ClusterName == "" && cfg.ClusterID == "" {
@@ -599,6 +770,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
res.ClusterID = clusterID
res.ClusterName = clusterName
res.ClusterDescription = cfg.ClusterDescription
+ res.ClaimableCerts = cfg.ClaimableCerts
}
// Validation of `data-gatherers`.
@@ -642,7 +814,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
var err error
installNS, err = getInClusterNamespace()
if err != nil {
- if res.OutputMode == VenafiCloudVenafiConnection {
+ if res.OutputMode == VenafiConnection {
errs = multierror.Append(errs, fmt.Errorf("could not guess which namespace the agent is running in: %w", err))
}
}
@@ -651,7 +823,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
}
// Validation of --venafi-connection and --venafi-connection-namespace.
- if res.OutputMode == VenafiCloudVenafiConnection {
+ if res.OutputMode == VenafiConnection {
res.VenConnName = flags.VenConnName
venConnNS := flags.VenConnNS
if flags.VenConnNS == "" {
@@ -780,7 +952,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
// arbitrary value.
uploaderID := "no"
- // We don't do this for the VenafiCloudVenafiConnection mode because
+ // We don't do this for the VenafiConnection mode because
// the upload_path field is ignored in that mode.
log.Info("Loading upload_path from \"venafi-cloud\" configuration.")
@@ -789,7 +961,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
if err != nil {
errs = multierror.Append(errs, err)
}
- case VenafiCloudVenafiConnection:
+ case VenafiConnection:
var restCfg *rest.Config
restCfg, err := kubeconfig.LoadRESTConfig("")
if err != nil {
@@ -819,6 +991,27 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
if err != nil {
errs = multierror.Append(errs, err)
}
+ case NGTS:
+ var creds *client.NGTSServiceAccountCredentials
+
+ if flagPrivateKeyPath == "" {
+ errs = multierror.Append(errs, fmt.Errorf("--private-key-path is required for NGTS mode"))
+ break
+ }
+
+ creds = &client.NGTSServiceAccountCredentials{
+ ClientID: flagClientID,
+ PrivateKeyFile: flagPrivateKeyPath,
+ }
+
+ // rootCAs can be used in future to support custom CA certs, but for now will remain empty
+ var rootCAs *x509.CertPool
+
+ var err error
+ outputClient, err = client.NewNGTSClient(metadata, creds, cfg.Server, cfg.TSGID, rootCAs)
+ if err != nil {
+ errs = multierror.Append(errs, err)
+ }
default:
panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode))
}
@@ -901,6 +1094,8 @@ func (dg *DataGatherer) UnmarshalYAML(unmarshal func(any) error) error {
cfg = &k8sdynamic.ConfigDynamic{}
case "k8s-discovery":
cfg = &k8sdiscovery.ConfigDiscovery{}
+ case "oidc":
+ cfg = &oidc.OIDCDiscovery{}
case "local":
cfg = &local.Config{}
// dummy dataGatherer is just used for testing
diff --git a/pkg/agent/config_test.go b/pkg/agent/config_test.go
index dee90d71..d4460251 100644
--- a/pkg/agent/config_test.go
+++ b/pkg/agent/config_test.go
@@ -195,8 +195,9 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
)
assert.EqualError(t, err, testutil.Undent(`
no output mode specified. To enable one of the output modes, you can:
+ - Use --ngts with --private-key-path and exactly one of --tsg-id or --ngts-server-url to use the NGTS mode (--client-id is optional if provided in the credentials secret).
- Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the Venafi Cloud Key Pair Service Account mode.
- - Use --venafi-connection for the Venafi Cloud VenafiConnection mode.
+ - Use --venafi-connection for the VenafiConnection mode (the upload backend - Venafi Cloud or NGTS - is selected by the VenafiConnection resource).
- Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode.
- Use --api-token if you want to use the Jetstack Secure API Token mode.
- Use --machine-hub if you want to use the MachineHub mode.
@@ -305,6 +306,23 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
assert.IsType(t, &client.VenafiCloudClient{}, cl)
})
+ t.Run("venafi-cloud-keypair-auth: it is possible to use --client-id with --venafi-cloud", func(t *testing.T) {
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ got, cl, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ server: "http://localhost:8080"
+ period: 1h
+ cluster_id: "the cluster name"
+ venafi-cloud:
+ upload_path: "/foo/bar"
+ `)),
+ withCmdLineFlags("--client-id", "5bc7d07c-45da-11ef-a878-523f1e1d7de1", "--private-key-path", privKeyPath, "--venafi-cloud"),
+ )
+ require.NoError(t, err)
+ assert.Equal(t, VenafiCloudKeypair, got.OutputMode)
+ assert.IsType(t, &client.VenafiCloudClient{}, cl)
+ })
+
t.Run("jetstack-secure-oauth-auth: fail if organization_id or cluster_id is missing and --venafi-cloud not enabled", func(t *testing.T) {
t.Setenv("POD_NAMESPACE", "venafi")
credsPath := withFile(t, `{"user_id":"fpp2624799349@affectionate-hertz6.platform.jetstack.io","user_secret":"foo","client_id": "k3TrDbfLhCgnpAbOiiT2kIE1AbovKzjo","client_secret": "f39w_3KT9Vp0VhzcPzvh-uVbudzqCFmHER3Huj0dvHgJwVrjxsoOQPIw_1SDiCfa","auth_server_domain":"auth.jetstack.io"}`)
@@ -567,15 +585,15 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
withCmdLineFlags("--venafi-connection", "venafi-components"))
require.NoError(t, err)
assert.Equal(t, testutil.Undent(`
- INFO Output mode selected venConnName="venafi-components" mode="Venafi Cloud VenafiConnection" reason="--venafi-connection was specified"
- INFO ignoring the server field specified in the config file. In Venafi Cloud VenafiConnection mode, this field is not needed.
+ INFO Output mode selected venConnName="venafi-components" mode="VenafiConnection" reason="--venafi-connection was specified"
+ INFO ignoring the server field specified in the config file. In VenafiConnection mode, this field is not needed.
INFO Using cluster_id as cluster_name for backwards compatibility clusterID="legacy cluster_id as cluster name"
INFO Using period from config period="1h0m0s"
`), gotLogs.String())
assert.Equal(t, CombinedConfig{
Period: 1 * time.Hour,
ClusterName: "legacy cluster_id as cluster name",
- OutputMode: VenafiCloudVenafiConnection,
+ OutputMode: VenafiConnection,
VenConnName: "venafi-components",
VenConnNS: "venafi",
InstallNS: "venafi",
@@ -602,14 +620,14 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
)
require.NoError(t, err)
assert.Equal(t, testutil.Undent(`
- INFO Output mode selected venConnName="venafi-components" mode="Venafi Cloud VenafiConnection" reason="--venafi-connection was specified"
- INFO ignoring the server field specified in the config file. In Venafi Cloud VenafiConnection mode, this field is not needed.
- INFO ignoring the venafi-cloud.upload_path field in the config file. In Venafi Cloud VenafiConnection mode, this field is not needed.
- INFO ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in Venafi Cloud VenafiConnection mode.
- INFO Ignoring the cluster_id field in the config file. This field is not needed in Venafi Cloud VenafiConnection mode.
+ INFO Output mode selected venConnName="venafi-components" mode="VenafiConnection" reason="--venafi-connection was specified"
+ INFO ignoring the server field specified in the config file. In VenafiConnection mode, this field is not needed.
+ INFO ignoring the venafi-cloud.upload_path field in the config file. In VenafiConnection mode, this field is not needed.
+ INFO ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in VenafiConnection mode.
+ INFO Ignoring the cluster_id field in the config file. This field is not needed in VenafiConnection mode.
INFO Using period from config period="1h0m0s"
`), gotLogs.String())
- assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode)
+ assert.Equal(t, VenafiConnection, got.OutputMode)
assert.IsType(t, &client.VenConnClient{}, gotCl)
})
@@ -624,7 +642,20 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
`)),
withCmdLineFlags("--venafi-connection", "venafi-components"))
require.NoError(t, err)
- assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode)
+ assert.Equal(t, VenafiConnection, got.OutputMode)
+ })
+
+ t.Run("venafi-cloud-workload-identity-auth: --venafi-cloud is tolerated alongside --venafi-connection for backwards compatibility with older rendered charts", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
+ got, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: cluster-1
+ `)),
+ withCmdLineFlags("--venafi-connection", "venafi-components", "--venafi-cloud"))
+ require.NoError(t, err)
+ assert.Equal(t, VenafiConnection, got.OutputMode)
})
const arkUsername = "cluster-1-region-1-cloud-1@cyberark.cloud.123456"
@@ -846,7 +877,7 @@ func Test_ValidateAndCombineConfig_VenafiConnection(t *testing.T) {
_, _, err := ValidateAndCombineConfig(discardLogs(),
Config{Server: "http://should-be-ignored", Period: 1 * time.Hour},
AgentCmdFlags{VenConnName: "venafi-components", InstallNS: "venafi"})
- assert.EqualError(t, err, "1 error occurred:\n\t* cluster_name or cluster_id is required in Venafi Cloud VenafiConnection mode\n\n")
+ assert.EqualError(t, err, "1 error occurred:\n\t* cluster_name or cluster_id is required in VenafiConnection mode\n\n")
})
t.Run("the server field is ignored when VenafiConnection is used", func(t *testing.T) {
@@ -1064,6 +1095,214 @@ users:
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBcVl4eEozSmV3VkV4VVNydVU4amNUWlE3cFJtL3BvMGlxY0pjR2haQmEyaFR0YnVOClUxUERoSTFXdVBXc2M3aDcxK2VvbVN0QVFad2NvVmw4WGllREFUdUZic3V2anJHanhtQjlhZWg2WTMwTktWSnoKL05ZTHZOOW1kbVhXWlBIM3dWS0NYTnZLSnk1KzdUdzFML0JsVGxaOEdCdGZBMmdsMEhFM2NGZFRHa0NYZnBlSQo2TWJmU2djNGZHRUIySTZ5NzhzUmNNSURwdHh6VFJRcThIOCttZnVwLzhwY200dE80M0JsZjROZVJvbU01TmpHCkp3NVQxejdqM091TldiZVlBS1dxU1U1aHBZcU1tNlBaaEpuM1JKOExZZGdpT2tyeTg4c3FNdG03MXcxU2pCdkcKRHRmZTUzUkJzeXpzam1Gemt5MmduY1dXRkFiMG1ieTBmbWJOSXdJREFRQUJBb0lCQUY2dHkzNWdzcU0zYU5mUApwbmpwSUlTOTh6UzJGVHkzY1pUa3NUUHNHNm9UL3pMcndmYTNQdVpsV3ZrOFQ0bnJpbFM5eTN1RkdJUEszbjRICmo1aXdiY3FoWjFqQXE0OStpVnM5Qkt2QW81K3M5RTJQK3E5RkJCYjdsYWNtSlR3SGx2ZkEwSVYwUXdYd1EvYk0KZVZNRTVqMkJ0Qmh1S0hlcGovdy9UTnNTR0pqK2NlNmN2aXVVb2NXWGsxWDl2c1RDaUdtMVdnVkZGQVphVGpMTgpDcEU1dHFpdnpvbEZVbXZIbmVYNTZTOEdFWk01NFA5MFk1enJ3NHBGa0Vud1VMRlBLa1U0cUU0eWVPNVFsWUhCClQ0NklIOVNPcUU5T0pLL3JCSGVzQU45TWNrMTdKblF6Sy95bXh6eHhhcGdPMnk0bVBTcjJaaGk0SENMRHRQV2QKc0ZtRzc2RUNnWUVBeHhQTTJYVFV2bXV5ckZmUVgxblJTSW9jMGhxZFY0MnFaRFlkMzZWVWc1UUVMM0Y4S01aUwptSkNsWlJXYW9IY0NFVUdXakFTWEJaMW9hOHlOMVhSNURTV3ZJMmV5TjE1dnh3NFg1SjV5QzUvY0F4ZW00dUk3CnkzM0VWWktXZXpFQTVVeUFtNlF6ei9lR1R6QkZyNUlxYkJDUitTUldudHRXUHdJTUhkK0VoeEVDZ1lFQTJnY3QKT2h1U0xJeDZZbTFTRHVVT0pSdmtFZFlCazJPQWxRbk5kOVJoaWIxdVlVbjhPTkhYdHBsY2FHZEl3bFdkaEJlcwo4M1F4dXA4MEFydEFtM2FHMXZ6RlZ6Q05KeHA4ZGFxWlFsZk94YlJReUQ0cjdtT2Z5aENFY2VibHAxMkZKRTBQCmNhOFl2TkFuTTdkbnlTSFd0aUo2THFQWDVuMXlRSC9JY1NIaEdQTUNnWUVBa0ZDZFBzSy8rcTZ1SHR1bDFZbVIKK3FrTWpZNzNvdUd5dE9TNk1VZDBCZEtHV2pKRmxIVjRxTnFxMjZXV3ExNjZZL0lOQmNIS0RTcjM2TFduMkNhUQpIbVRFR3NGd1kwMFZjTktacFlUckhkd3NMUjIzUUdCS2dwRFFoRXc0eEdOWXgrRDJsbDJwcGNoRldDQ2hVODU4CjdFdnkxZzV1c01oR05IVHlmYkZzTEZFQ2dZRUF6QXJOVzhVenZuZFZqY25MY3Q4UXBzLzhXR2pVbnJBUFJPdWcKbTlWcDF2TXVXdVJYcElGV0JMQnYxOUZaT1czUWRTK0hEMndkb2c2ZUtUUS9HWDhLWUNhOU5JVGVoTXIzMFZMdwpEVE9KOG1KMiszK2JzNFVPcEpkaXJBb3Z3THI0QUdvUjJ3M0g4K1JGMjlOMzBMYlhieXJDOStVa0I3UTgrWG5kCkIydHljdHNDZ1lCZkxqUTNRUnpQN1Z5Y1VGNkFTYUNYVTJkcE5lckVUbGFpdldIb1FFWVo3NHEyMkFTeFcrMlEKWmtZTEM1RVNGMnZwUU5kZUZhZlRyRm9zR3pLQ1dwYXBUL2QwUC9qaG83TEF1TTJQZEcxSXFoNElRU3FUM3VqNwp4Sm9WUzhIbEg1Ri9sQzZzczZQSm1GWlpsanhFL1FVTDlucDNLYTVCRjFXdXZiZVp0Q2I5Mnc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
`
+func Test_ValidateAndCombineConfig_NGTS(t *testing.T) {
+ t.Run("ngts: valid configuration with all required flags", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ got, cl, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ cluster_description: Test NGTS cluster
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.NoError(t, err)
+ assert.Equal(t, NGTS, got.OutputMode)
+ assert.Equal(t, "test-tsg-123", got.TSGID)
+ assert.Equal(t, "test-cluster", got.ClusterName)
+ assert.Equal(t, "Test NGTS cluster", got.ClusterDescription)
+ assert.Equal(t, false, got.ClaimableCerts)
+ assert.IsType(t, &client.NGTSClient{}, cl)
+ })
+
+ t.Run("ngts: claimable_certs flows from config into CombinedConfig", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ got, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ claimable_certs: true
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.NoError(t, err)
+ assert.Equal(t, true, got.ClaimableCerts)
+ })
+
+ t.Run("ngts: valid configuration with custom server URL", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ got, cl, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--client-id", "test-client-id", "--private-key-path", privKeyPath, "--ngts-server-url", "https://ngts.test.example.com"))
+ require.NoError(t, err)
+ assert.Equal(t, NGTS, got.OutputMode)
+ assert.Equal(t, "", got.TSGID)
+ assert.Equal(t, "https://ngts.test.example.com", got.NGTSServerURL)
+ assert.IsType(t, &client.NGTSClient{}, cl)
+ })
+
+ t.Run("ngts: --tsg-id and --ngts-server-url are mutually exclusive", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath, "--ngts-server-url", "https://ngts.test.example.com"))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--tsg-id and --ngts-server-url are mutually exclusive")
+ })
+
+ t.Run("ngts: missing both --tsg-id and --ngts-server-url should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "either --tsg-id or --ngts-server-url is required when using --ngts")
+ })
+
+ t.Run("ngts: missing --ngts flag should not trigger NGTS mode", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ // Should select VenafiCloudKeypair mode instead when --ngts is not specified
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "venafi-cloud.upload_path")
+ })
+
+ t.Run("ngts: missing --client-id should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "client_id cannot be empty")
+ })
+
+ t.Run("ngts: missing --private-key-path should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id"))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--private-key-path is required when using --ngts")
+ })
+
+ t.Run("ngts: missing cluster_name should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "cluster_name is required")
+ })
+
+ t.Run("ngts: cannot be used with --machine-hub", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--machine-hub", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--machine-hub cannot be used with --ngts")
+ })
+
+ t.Run("ngts: cannot be used with --venafi-connection", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--venafi-connection", "my-conn", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--venafi-connection cannot be used with --ngts")
+ })
+
+ t.Run("ngts: cannot be used with --venafi-cloud", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--venafi-cloud", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--venafi-cloud cannot be used with --ngts")
+ })
+
+ t.Run("ngts: cannot be used with --api-token", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ `)),
+ withCmdLineFlags("--ngts", "--api-token", "test-token", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "--api-token cannot be used with --ngts")
+ })
+
+ t.Run("ngts: organization_id in config should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ organization_id: my-org
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "organization_id in config file is not supported in NGTS mode")
+ })
+
+ t.Run("ngts: cluster_id in config should error", func(t *testing.T) {
+ t.Setenv("POD_NAMESPACE", "venafi")
+ privKeyPath := withFile(t, fakePrivKeyPEM)
+ _, _, err := ValidateAndCombineConfig(discardLogs(),
+ withConfig(testutil.Undent(`
+ period: 1h
+ cluster_name: test-cluster
+ cluster_id: my-cluster-id
+ `)),
+ withCmdLineFlags("--ngts", "--tsg-id", "test-tsg-123", "--client-id", "test-client-id", "--private-key-path", privKeyPath))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "cluster_id in config file is not supported in NGTS mode")
+ })
+}
+
const fakePrivKeyPEM = `-----BEGIN PRIVATE KEY-----
MHcCAQEEIFptpPXOvEWDrYkiMhyEH1+FB1GwtwX2tyXH4KtBO6g7oAoGCCqGSM49
AwEHoUQDQgAE/BsIwagYc4YUjSSFyqcStj2qliAkdVGlMoJbMuXupzQ9Qs4TX5Pl
diff --git a/pkg/agent/dummy_data_gatherer.go b/pkg/agent/dummy_data_gatherer.go
index 997c50eb..8d224d27 100644
--- a/pkg/agent/dummy_data_gatherer.go
+++ b/pkg/agent/dummy_data_gatherer.go
@@ -39,7 +39,7 @@ func (g *dummyDataGatherer) WaitForCacheSync(ctx context.Context) error {
return nil
}
-func (c *dummyDataGatherer) Fetch() (any, int, error) {
+func (c *dummyDataGatherer) Fetch(ctx context.Context) (any, int, error) {
var err error
if c.attemptNumber < c.FailedAttempts {
err = fmt.Errorf("First %d attempts will fail", c.FailedAttempts)
diff --git a/pkg/agent/run.go b/pkg/agent/run.go
index 609f5c4d..461cd03a 100644
--- a/pkg/agent/run.go
+++ b/pkg/agent/run.go
@@ -31,6 +31,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"github.com/jetstack/preflight/api"
+ "github.com/jetstack/preflight/internal/envelope"
+ "github.com/jetstack/preflight/internal/envelope/keyfetch"
+ "github.com/jetstack/preflight/internal/envelope/rsa"
"github.com/jetstack/preflight/pkg/client"
"github.com/jetstack/preflight/pkg/datagatherer"
"github.com/jetstack/preflight/pkg/datagatherer/k8sdynamic"
@@ -51,9 +54,9 @@ const schemaVersion string = "v2.0.0"
// Run starts the agent process
func Run(cmd *cobra.Command, args []string) (returnErr error) {
- ctx, cancel := context.WithCancel(cmd.Context())
+ baseCtx, cancel := context.WithCancel(cmd.Context())
defer cancel()
- log := klog.FromContext(ctx).WithName("Run")
+ log := klog.FromContext(baseCtx).WithName("Run")
log.Info("Starting", "version", version.PreflightVersion, "commit", version.Commit)
@@ -78,7 +81,7 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
return fmt.Errorf("While evaluating configuration: %v", err)
}
- group, gctx := errgroup.WithContext(ctx)
+ group, gctx := errgroup.WithContext(baseCtx)
defer func() {
cancel()
if groupErr := group.Wait(); groupErr != nil {
@@ -123,13 +126,14 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
})
group.Go(func() error {
+ listenCtx := klog.NewContext(gctx, log)
err := listenAndServe(
- klog.NewContext(gctx, log),
+ listenCtx,
&http.Server{
Addr: serverAddress,
Handler: server,
BaseContext: func(_ net.Listener) context.Context {
- return gctx
+ return listenCtx
},
},
)
@@ -161,6 +165,20 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
return fmt.Errorf("failed to create event recorder: %v", err)
}
+ // Check if secret encryption is enabled via environment variable
+ // When enabled, secret data will be kept for encryption instead of being redacted
+ encryptSecrets := strings.ToLower(os.Getenv("ARK_SEND_SECRET_VALUES")) == "true"
+
+ var encryptor envelope.Encryptor
+
+ if encryptSecrets {
+ encryptor, err = loadEncryptor(gctx, preflightClient)
+ if err != nil {
+ log.Error(err, "Failed to set up encryptor for secrets, secret data will not be sent")
+ encryptSecrets = false
+ }
+ }
+
dataGatherers := map[string]datagatherer.DataGatherer{}
// load datagatherer config and boot each one
@@ -178,8 +196,20 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
dynDg, isDynamicGatherer := newDg.(*k8sdynamic.DataGathererDynamic)
if isDynamicGatherer {
- dynDg.ExcludeAnnotKeys = config.ExcludeAnnotationKeysRegex
- dynDg.ExcludeLabelKeys = config.ExcludeLabelKeysRegex
+ dynDg.ExcludeAnnotKeys = append(dynDg.ExcludeAnnotKeys, config.ExcludeAnnotationKeysRegex...)
+ dynDg.ExcludeLabelKeys = append(dynDg.ExcludeLabelKeys, config.ExcludeLabelKeysRegex...)
+
+ gvr := dynDg.GVR()
+
+ if encryptSecrets && gvr.Resource == "secrets" && gvr.Group == "" {
+ log.Info("Secret encryption enabled for datagatherer")
+ dynDg.Encryptor = encryptor
+ }
+
+ _, isCyberArk := preflightClient.(*client.CyberArkClient)
+ if isCyberArk && gvr.Resource == "secrets" && gvr.Group == "" {
+ dynDg.IncludeLastModifiedTime = true
+ }
}
log.V(logs.Debug).Info("Starting DataGatherer", "name", dgConfig.Name)
@@ -239,7 +269,7 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
// be cancelled, which will cause this blocking loop to exit
// instead of waiting for the time period.
for {
- if err := gatherAndOutputData(klog.NewContext(ctx, log), eventf, config, preflightClient, dataGatherers); err != nil {
+ if err := gatherAndOutputData(gctx, eventf, config, preflightClient, dataGatherers); err != nil {
return err
}
@@ -256,6 +286,31 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
return nil
}
+// loadEncryptor sets up an encryptor for encrypting secrets. For now, it just loads a hardcoded public key
+func loadEncryptor(ctx context.Context, preflightClient client.Client) (envelope.Encryptor, error) {
+ cyberarkClient, ok := preflightClient.(*client.CyberArkClient)
+ if !ok {
+ return nil, fmt.Errorf("secret encryption is only supported for CyberArk clients")
+ }
+
+ cfg, err := cyberarkClient.Config()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get CyberArk client config: %w", err)
+ }
+
+ fetcher, err := keyfetch.NewClient(ctx, cyberarkClient.DiscoveryClient(), cfg, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create key fetcher for secret encryption: %w", err)
+ }
+
+ encryptor, err := rsa.NewEncryptor(fetcher)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create encryptor for secret encryption: %w", err)
+ }
+
+ return encryptor, nil
+}
+
// Creates an event recorder for the agent's Pod object. Expects the env var
// POD_NAME to contain the pod name. Note that the RBAC rule allowing sending
// events is attached to the pod's service account, not the impersonated service
@@ -316,7 +371,7 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf
}
} else {
var err error
- readings, err = gatherData(klog.NewContext(ctx, log), config, dataGatherers)
+ readings, err = gatherData(ctx, config, dataGatherers)
if err != nil {
return err
}
@@ -338,7 +393,7 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf
postCtx, cancel := context.WithTimeout(ctx, config.BackoffMaxTime)
defer cancel()
- return struct{}{}, postData(klog.NewContext(postCtx, log), config, preflightClient, readings)
+ return struct{}{}, postData(postCtx, config, preflightClient, readings)
}
group.Go(func() error {
@@ -361,7 +416,7 @@ func gatherData(ctx context.Context, config CombinedConfig, dataGatherers map[st
var dgError *multierror.Error
for k, dg := range dataGatherers {
- dgData, count, err := dg.Fetch()
+ dgData, count, err := dg.Fetch(ctx)
if err != nil {
dgError = multierror.Append(dgError, fmt.Errorf("error in datagatherer %s: %w", k, err))
@@ -406,10 +461,10 @@ func gatherData(ctx context.Context, config CombinedConfig, dataGatherers map[st
func postData(ctx context.Context, config CombinedConfig, preflightClient client.Client, readings []*api.DataReading) error {
log := klog.FromContext(ctx).WithName("postData")
- ctx = klog.NewContext(ctx, log)
err := preflightClient.PostDataReadingsWithOptions(ctx, readings, client.Options{
ClusterName: config.ClusterName,
ClusterDescription: config.ClusterDescription,
+ ClaimableCerts: config.ClaimableCerts,
// orgID and clusterID are not required for Venafi Cloud auth
OrgID: config.OrganizationID,
ClusterID: config.ClusterID,
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 210243f0..6c9fd16f 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -23,6 +23,11 @@ type (
// Used for Venafi Cloud and MachineHub mode.
ClusterDescription string
+
+ // ClaimableCerts controls whether discovered certs can be claimed by other tenants.
+ // true = certs are left unassigned, available for any tenant to claim.
+ // false (default) = certs are owned by this cluster's tenant.
+ ClaimableCerts bool
}
// The Client interface describes types that perform requests against the Jetstack Secure backend.
diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go
index c9310265..3232e7c6 100644
--- a/pkg/client/client_cyberark.go
+++ b/pkg/client/client_cyberark.go
@@ -19,6 +19,7 @@ import (
"github.com/jetstack/preflight/api"
"github.com/jetstack/preflight/internal/cyberark"
"github.com/jetstack/preflight/internal/cyberark/dataupload"
+ "github.com/jetstack/preflight/internal/cyberark/servicediscovery"
"github.com/jetstack/preflight/pkg/logs"
"github.com/jetstack/preflight/pkg/version"
)
@@ -27,6 +28,8 @@ import (
type CyberArkClient struct {
configLoader cyberark.ClientConfigLoader
httpClient *http.Client
+
+ discoveryClient *servicediscovery.Client
}
var _ Client = &CyberArkClient{}
@@ -34,16 +37,21 @@ var _ Client = &CyberArkClient{}
// NewCyberArk initializes a CyberArk client using configuration from environment variables.
// It requires an HTTP client to be provided, which will be used for making requests.
// The environment variables ARK_SUBDOMAIN, ARK_USERNAME, and ARK_SECRET must be set for authentication.
+// Sending secrets is controlled by the ARK_SEND_SECRETS environment variable (defaults to "false").
+// If sending secrets is enabled, the hardcoded public key will be loaded and an encryptor will be created.
// If the configuration is invalid or missing, an error is returned.
func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
configLoader := cyberark.LoadClientConfigFromEnvironment
- _, err := configLoader()
+
+ cfg, err := configLoader()
if err != nil {
return nil, err
}
+
return &CyberArkClient{
- configLoader: configLoader,
- httpClient: httpClient,
+ configLoader: configLoader,
+ httpClient: httpClient,
+ discoveryClient: servicediscovery.New(httpClient, cfg.Subdomain),
}, nil
}
@@ -56,6 +64,17 @@ func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
// The supplied Options are not used by this publisher.
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, opts Options) error {
log := klog.FromContext(ctx)
+
+ cfg, err := o.configLoader()
+ if err != nil {
+ return fmt.Errorf("failed to load config: %w", err)
+ }
+
+ serviceMap, tenantUUID, err := o.discoveryClient.DiscoverServices(ctx)
+ if err != nil {
+ return err
+ }
+
snapshot := baseSnapshotFromOptions(opts)
if err := convertDataReadings(defaultExtractorFunctions, readings, &snapshot); err != nil {
@@ -65,11 +84,7 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
// Minimize the snapshot to reduce size and improve privacy
minimizeSnapshot(log.V(logs.Debug), &snapshot)
- cfg, err := o.configLoader()
- if err != nil {
- return err
- }
- datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, cfg)
+ datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, serviceMap, tenantUUID, cfg)
if err != nil {
return fmt.Errorf("while initializing data upload client: %s", err)
}
@@ -81,6 +96,14 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
return nil
}
+func (o *CyberArkClient) DiscoveryClient() *servicediscovery.Client {
+ return o.discoveryClient
+}
+
+func (o *CyberArkClient) Config() (cyberark.ClientConfig, error) {
+ return o.configLoader()
+}
+
// baseSnapshotFromOptions creates a base snapshot with common fields from the provided options.
// This includes the cluster name, description, and agent version.
// Other fields like ClusterID and K8SVersion need to be populated separately.
@@ -92,6 +115,25 @@ func baseSnapshotFromOptions(opts Options) dataupload.Snapshot {
}
}
+// extractOIDCFromReading converts the opaque data from a OIDCDiscoveryData
+// data reading to allow access to the OIDC fields within.
+func extractOIDCFromReading(reading *api.DataReading, target *dataupload.Snapshot) error {
+ if reading == nil {
+ return fmt.Errorf("programmer mistake: the DataReading must not be nil")
+ }
+ data, ok := reading.Data.(*api.OIDCDiscoveryData)
+ if !ok {
+ return fmt.Errorf(
+ "programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. "+
+ "This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data)
+ }
+ target.OIDCConfig = data.OIDCConfig
+ target.OIDCConfigError = data.OIDCConfigError
+ target.JWKS = data.JWKS
+ target.JWKSError = data.JWKSError
+ return nil
+}
+
// extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData
// data reading to allow access to the Kubernetes version fields within.
func extractClusterIDAndServerVersionFromReading(reading *api.DataReading, target *dataupload.Snapshot) error {
@@ -149,6 +191,7 @@ func extractResourceListFromReading(reading *api.DataReading, target *[]runtime.
// and populates the relevant field(s) of the Snapshot based on the DataReading's data.
// Deleted resources are excluded from the snapshot because they are not needed by CyberArk.
var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/oidc": extractOIDCFromReading,
"ark/discovery": extractClusterIDAndServerVersionFromReading,
"ark/secrets": func(r *api.DataReading, s *dataupload.Snapshot) error {
return extractResourceListFromReading(r, &s.Secrets)
@@ -186,6 +229,21 @@ var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Sn
"ark/pods": func(r *api.DataReading, s *dataupload.Snapshot) error {
return extractResourceListFromReading(r, &s.Pods)
},
+ "ark/configmaps": func(r *api.DataReading, s *dataupload.Snapshot) error {
+ return extractResourceListFromReading(r, &s.ConfigMaps)
+ },
+ "ark/esoexternalsecrets": func(r *api.DataReading, s *dataupload.Snapshot) error {
+ return extractResourceListFromReading(r, &s.ExternalSecrets)
+ },
+ "ark/esosecretstores": func(r *api.DataReading, s *dataupload.Snapshot) error {
+ return extractResourceListFromReading(r, &s.SecretStores)
+ },
+ "ark/esoclusterexternalsecrets": func(r *api.DataReading, s *dataupload.Snapshot) error {
+ return extractResourceListFromReading(r, &s.ClusterExternalSecrets)
+ },
+ "ark/esoclustersecretstores": func(r *api.DataReading, s *dataupload.Snapshot) error {
+ return extractResourceListFromReading(r, &s.ClusterSecretStores)
+ },
}
// convertDataReadings processes a list of DataReadings using the provided
diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go
index 4fc33198..293629c9 100644
--- a/pkg/client/client_cyberark_convertdatareadings_test.go
+++ b/pkg/client/client_cyberark_convertdatareadings_test.go
@@ -126,6 +126,70 @@ func TestExtractServerVersionFromReading(t *testing.T) {
}
}
+// TestExtractOIDCFromReading tests the extractOIDCFromReading function.
+func TestExtractOIDCFromReading(t *testing.T) {
+ type testCase struct {
+ name string
+ reading *api.DataReading
+ expectedSnapshot dataupload.Snapshot
+ expectError string
+ }
+ tests := []testCase{
+ {
+ name: "nil reading",
+ expectError: `programmer mistake: the DataReading must not be nil`,
+ },
+ {
+ name: "nil data",
+ reading: &api.DataReading{
+ DataGatherer: "ark/oidc",
+ Data: nil,
+ },
+ expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type `,
+ },
+ {
+ name: "wrong data type",
+ reading: &api.DataReading{
+ DataGatherer: "ark/oidc",
+ Data: &api.DiscoveryData{},
+ },
+ expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type *api.DiscoveryData`,
+ },
+ {
+ name: "happy path",
+ reading: &api.DataReading{
+ DataGatherer: "ark/oidc",
+ Data: &api.OIDCDiscoveryData{
+ OIDCConfig: map[string]any{"issuer": "https://example.com"},
+ OIDCConfigError: "oidc-err",
+ JWKS: map[string]any{"keys": []any{}},
+ JWKSError: "jwks-err",
+ },
+ },
+ expectedSnapshot: dataupload.Snapshot{
+ OIDCConfig: map[string]any{"issuer": "https://example.com"},
+ OIDCConfigError: "oidc-err",
+ JWKS: map[string]any{"keys": []any{}},
+ JWKSError: "jwks-err",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var snapshot dataupload.Snapshot
+ err := extractOIDCFromReading(test.reading, &snapshot)
+ if test.expectError != "" {
+ assert.EqualError(t, err, test.expectError)
+ assert.Equal(t, dataupload.Snapshot{}, snapshot)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.expectedSnapshot, snapshot)
+ })
+ }
+}
+
// TestExtractResourceListFromReading tests the extractResourceListFromReading function.
func TestExtractResourceListFromReading(t *testing.T) {
type testCase struct {
@@ -253,6 +317,759 @@ func TestExtractResourceListFromReading(t *testing.T) {
}
}
+// TestConvertDataReadings_ConfigMaps tests that configmaps are correctly converted.
+func TestConvertDataReadings_ConfigMaps(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/configmaps": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ConfigMaps)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.21.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/configmaps",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "conjur-connect",
+ "namespace": "conjur",
+ "labels": map[string]any{
+ "conjur.org/name": "conjur-connect-configmap",
+ },
+ },
+ "data": map[string]any{
+ "config.yaml": "some-config-data",
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "another-configmap",
+ "namespace": "default",
+ "labels": map[string]any{
+ "conjur.org/name": "conjur-connect-configmap",
+ },
+ },
+ "data": map[string]any{
+ "setting": "value",
+ },
+ },
+ },
+ },
+ // Deleted configmap should be ignored
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "deleted-configmap",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify the snapshot contains the expected data
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.21.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ConfigMaps, 2, "should have 2 configmaps (deleted one should be excluded)")
+
+ // Verify the first configmap
+ cm1, ok := snapshot.ConfigMaps[0].(*unstructured.Unstructured)
+ require.True(t, ok, "configmap should be unstructured")
+ assert.Equal(t, "ConfigMap", cm1.GetKind())
+ assert.Equal(t, "conjur-connect", cm1.GetName())
+ assert.Equal(t, "conjur", cm1.GetNamespace())
+
+ // Verify the second configmap
+ cm2, ok := snapshot.ConfigMaps[1].(*unstructured.Unstructured)
+ require.True(t, ok, "configmap should be unstructured")
+ assert.Equal(t, "ConfigMap", cm2.GetKind())
+ assert.Equal(t, "another-configmap", cm2.GetName())
+ assert.Equal(t, "default", cm2.GetNamespace())
+}
+
+// TestConvertDataReadings_ExternalSecrets tests that externalsecrets are correctly converted.
+func TestConvertDataReadings_ExternalSecrets(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/esoexternalsecrets": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ExternalSecrets)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.21.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/esoexternalsecrets",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ExternalSecret",
+ "metadata": map[string]any{
+ "name": "my-external-secret",
+ "namespace": "default",
+ },
+ "spec": map[string]any{
+ "refreshInterval": "1h",
+ "secretStoreRef": map[string]any{
+ "name": "my-secret-store",
+ "kind": "SecretStore",
+ },
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ExternalSecret",
+ "metadata": map[string]any{
+ "name": "another-external-secret",
+ "namespace": "production",
+ },
+ "spec": map[string]any{
+ "refreshInterval": "30m",
+ },
+ },
+ },
+ },
+ // Deleted externalsecret should be ignored
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ExternalSecret",
+ "metadata": map[string]any{
+ "name": "deleted-external-secret",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify the snapshot contains the expected data
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.21.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ExternalSecrets, 2, "should have 2 externalsecrets (deleted one should be excluded)")
+
+ // Verify the first externalsecret
+ es1, ok := snapshot.ExternalSecrets[0].(*unstructured.Unstructured)
+ require.True(t, ok, "externalsecret should be unstructured")
+ assert.Equal(t, "ExternalSecret", es1.GetKind())
+ assert.Equal(t, "my-external-secret", es1.GetName())
+ assert.Equal(t, "default", es1.GetNamespace())
+
+ // Verify the second externalsecret
+ es2, ok := snapshot.ExternalSecrets[1].(*unstructured.Unstructured)
+ require.True(t, ok, "externalsecret should be unstructured")
+ assert.Equal(t, "ExternalSecret", es2.GetKind())
+ assert.Equal(t, "another-external-secret", es2.GetName())
+ assert.Equal(t, "production", es2.GetNamespace())
+}
+
+// TestConvertDataReadings_SecretStores tests that secretstores are correctly converted.
+func TestConvertDataReadings_SecretStores(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/esosecretstores": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.SecretStores)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.21.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/esosecretstores",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "SecretStore",
+ "metadata": map[string]any{
+ "name": "my-secret-store",
+ "namespace": "default",
+ },
+ "spec": map[string]any{
+ "provider": map[string]any{
+ "fake": map[string]any{},
+ },
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "SecretStore",
+ "metadata": map[string]any{
+ "name": "aws-secret-store",
+ "namespace": "production",
+ },
+ "spec": map[string]any{
+ "provider": map[string]any{
+ "aws": map[string]any{},
+ },
+ },
+ },
+ },
+ },
+ // Deleted secretstore should be ignored
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "SecretStore",
+ "metadata": map[string]any{
+ "name": "deleted-secret-store",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify the snapshot contains the expected data
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.21.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.SecretStores, 2, "should have 2 secretstores (deleted one should be excluded)")
+
+ // Verify the first secretstore
+ ss1, ok := snapshot.SecretStores[0].(*unstructured.Unstructured)
+ require.True(t, ok, "secretstore should be unstructured")
+ assert.Equal(t, "SecretStore", ss1.GetKind())
+ assert.Equal(t, "my-secret-store", ss1.GetName())
+ assert.Equal(t, "default", ss1.GetNamespace())
+
+ // Verify the second secretstore
+ ss2, ok := snapshot.SecretStores[1].(*unstructured.Unstructured)
+ require.True(t, ok, "secretstore should be unstructured")
+ assert.Equal(t, "SecretStore", ss2.GetKind())
+ assert.Equal(t, "aws-secret-store", ss2.GetName())
+ assert.Equal(t, "production", ss2.GetNamespace())
+}
+
+// TestConvertDataReadings_ClusterExternalSecrets tests that clusterexternalsecrets are correctly converted.
+func TestConvertDataReadings_ClusterExternalSecrets(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/esoclusterexternalsecrets": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ClusterExternalSecrets)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.21.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/esoclusterexternalsecrets",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterExternalSecret",
+ "metadata": map[string]any{
+ "name": "my-cluster-external-secret",
+ },
+ "spec": map[string]any{
+ "externalSecretSpec": map[string]any{
+ "secretStoreRef": map[string]any{
+ "name": "my-cluster-secret-store",
+ "kind": "ClusterSecretStore",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterExternalSecret",
+ "metadata": map[string]any{
+ "name": "aws-cluster-external-secret",
+ },
+ "spec": map[string]any{
+ "externalSecretSpec": map[string]any{
+ "secretStoreRef": map[string]any{
+ "name": "aws-cluster-secret-store",
+ "kind": "ClusterSecretStore",
+ },
+ },
+ },
+ },
+ },
+ },
+ // Deleted clusterexternalsecret should be ignored
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterExternalSecret",
+ "metadata": map[string]any{
+ "name": "deleted-cluster-external-secret",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify the snapshot contains the expected data
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.21.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ClusterExternalSecrets, 2, "should have 2 clusterexternalsecrets (deleted one should be excluded)")
+
+ // Verify the first clusterexternalsecret
+ ces1, ok := snapshot.ClusterExternalSecrets[0].(*unstructured.Unstructured)
+ require.True(t, ok, "clusterexternalsecret should be unstructured")
+ assert.Equal(t, "ClusterExternalSecret", ces1.GetKind())
+ assert.Equal(t, "my-cluster-external-secret", ces1.GetName())
+
+ // Verify the second clusterexternalsecret
+ ces2, ok := snapshot.ClusterExternalSecrets[1].(*unstructured.Unstructured)
+ require.True(t, ok, "clusterexternalsecret should be unstructured")
+ assert.Equal(t, "ClusterExternalSecret", ces2.GetKind())
+ assert.Equal(t, "aws-cluster-external-secret", ces2.GetName())
+}
+
+// TestConvertDataReadings_ClusterSecretStores tests that clustersecretstores are correctly converted.
+func TestConvertDataReadings_ClusterSecretStores(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/esoclustersecretstores": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ClusterSecretStores)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.21.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/esoclustersecretstores",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterSecretStore",
+ "metadata": map[string]any{
+ "name": "my-cluster-secret-store",
+ },
+ "spec": map[string]any{
+ "provider": map[string]any{
+ "fake": map[string]any{},
+ },
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterSecretStore",
+ "metadata": map[string]any{
+ "name": "aws-cluster-secret-store",
+ },
+ "spec": map[string]any{
+ "provider": map[string]any{
+ "aws": map[string]any{},
+ },
+ },
+ },
+ },
+ },
+ // Deleted clustersecretstore should be ignored
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "external-secrets.io/v1",
+ "kind": "ClusterSecretStore",
+ "metadata": map[string]any{
+ "name": "deleted-cluster-secret-store",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify the snapshot contains the expected data
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.21.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ClusterSecretStores, 2, "should have 2 clustersecretstores (deleted one should be excluded)")
+
+ // Verify the first clustersecretstore
+ css1, ok := snapshot.ClusterSecretStores[0].(*unstructured.Unstructured)
+ require.True(t, ok, "clustersecretstore should be unstructured")
+ assert.Equal(t, "ClusterSecretStore", css1.GetKind())
+ assert.Equal(t, "my-cluster-secret-store", css1.GetName())
+
+ // Verify the second clustersecretstore
+ css2, ok := snapshot.ClusterSecretStores[1].(*unstructured.Unstructured)
+ require.True(t, ok, "clustersecretstore should be unstructured")
+ assert.Equal(t, "ClusterSecretStore", css2.GetKind())
+ assert.Equal(t, "aws-cluster-secret-store", css2.GetName())
+}
+
+// TestConvertDataReadings_ServiceAccounts tests that serviceaccounts are correctly converted.
+func TestConvertDataReadings_ServiceAccounts(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/serviceaccounts": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ServiceAccounts)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "test-cluster-id",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.22.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/serviceaccounts",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ServiceAccount",
+ "metadata": map[string]any{
+ "name": "default",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ServiceAccount",
+ "metadata": map[string]any{
+ "name": "app-sa",
+ "namespace": "production",
+ "labels": map[string]any{
+ "app": "myapp",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ assert.Equal(t, "test-cluster-id", snapshot.ClusterID)
+ assert.Equal(t, "v1.22.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ServiceAccounts, 2)
+
+ sa1, ok := snapshot.ServiceAccounts[0].(*unstructured.Unstructured)
+ require.True(t, ok)
+ assert.Equal(t, "ServiceAccount", sa1.GetKind())
+ assert.Equal(t, "default", sa1.GetName())
+}
+
+// TestConvertDataReadings_Roles tests that roles are correctly converted.
+func TestConvertDataReadings_Roles(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/roles": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.Roles)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "rbac-cluster",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.23.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/roles",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "rbac.authorization.k8s.io/v1",
+ "kind": "Role",
+ "metadata": map[string]any{
+ "name": "pod-reader",
+ "namespace": "default",
+ "labels": map[string]any{
+ "rbac.authorization.k8s.io/aggregate-to-view": "true",
+ },
+ },
+ "rules": []any{
+ map[string]any{
+ "apiGroups": []any{""},
+ "resources": []any{"pods"},
+ "verbs": []any{"get", "list"},
+ },
+ },
+ },
+ },
+ },
+ // Deleted role should be excluded
+ {
+ DeletedAt: api.Time{Time: time.Now()},
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "rbac.authorization.k8s.io/v1",
+ "kind": "Role",
+ "metadata": map[string]any{
+ "name": "deleted-role",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ assert.Equal(t, "rbac-cluster", snapshot.ClusterID)
+ require.Len(t, snapshot.Roles, 1, "deleted role should be excluded")
+
+ role, ok := snapshot.Roles[0].(*unstructured.Unstructured)
+ require.True(t, ok)
+ assert.Equal(t, "Role", role.GetKind())
+ assert.Equal(t, "pod-reader", role.GetName())
+}
+
+// TestConvertDataReadings_MultipleResources tests conversion with multiple resource types.
+func TestConvertDataReadings_MultipleResources(t *testing.T) {
+ extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
+ "ark/discovery": extractClusterIDAndServerVersionFromReading,
+ "ark/configmaps": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ConfigMaps)
+ },
+ "ark/serviceaccounts": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.ServiceAccounts)
+ },
+ "ark/deployments": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error {
+ return extractResourceListFromReading(reading, &snapshot.Deployments)
+ },
+ }
+
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "ark/discovery",
+ Data: &api.DiscoveryData{
+ ClusterID: "multi-resource-cluster",
+ ServerVersion: &version.Info{
+ GitVersion: "v1.24.0",
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/configmaps",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "app-config",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/serviceaccounts",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ServiceAccount",
+ "metadata": map[string]any{
+ "name": "app-sa",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ DataGatherer: "ark/deployments",
+ Data: &api.DynamicData{
+ Items: []*api.GatheredResource{
+ {
+ Resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "web-app",
+ "namespace": "default",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var snapshot dataupload.Snapshot
+ err := convertDataReadings(extractorFunctions, readings, &snapshot)
+ require.NoError(t, err)
+
+ // Verify all resources are present
+ assert.Equal(t, "multi-resource-cluster", snapshot.ClusterID)
+ assert.Equal(t, "v1.24.0", snapshot.K8SVersion)
+ require.Len(t, snapshot.ConfigMaps, 1)
+ require.Len(t, snapshot.ServiceAccounts, 1)
+ require.Len(t, snapshot.Deployments, 1)
+
+ // Verify each resource type
+ cm, ok := snapshot.ConfigMaps[0].(*unstructured.Unstructured)
+ require.True(t, ok)
+ assert.Equal(t, "app-config", cm.GetName())
+
+ sa, ok := snapshot.ServiceAccounts[0].(*unstructured.Unstructured)
+ require.True(t, ok)
+ assert.Equal(t, "app-sa", sa.GetName())
+
+ deploy, ok := snapshot.Deployments[0].(*unstructured.Unstructured)
+ require.True(t, ok)
+ assert.Equal(t, "web-app", deploy.GetName())
+}
+
// TestConvertDataReadings tests the convertDataReadings function.
func TestConvertDataReadings(t *testing.T) {
simpleExtractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{
diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go
index f0df5c64..4931c376 100644
--- a/pkg/client/client_cyberark_test.go
+++ b/pkg/client/client_cyberark_test.go
@@ -3,6 +3,8 @@ package client_test
import (
"crypto/x509"
"errors"
+ "os"
+ "strings"
"testing"
"github.com/jetstack/venafi-connection-lib/http_client"
@@ -52,6 +54,11 @@ func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) {
// go test ./internal/cyberark/dataupload/... \
// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6
func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
+ if strings.ToLower(os.Getenv("ARK_LIVE_TEST")) != "true" {
+ t.Skip("set ARK_LIVE_TEST=true to run this test against the live service")
+ return
+ }
+
t.Run("success", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)
@@ -79,6 +86,11 @@ func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
var defaultDynamicDatagathererNames = []string{
"ark/secrets",
"ark/serviceaccounts",
+ "ark/configmaps",
+ "ark/esoexternalsecrets",
+ "ark/esosecretstores",
+ "ark/esoclusterexternalsecrets",
+ "ark/esoclustersecretstores",
"ark/roles",
"ark/clusterroles",
"ark/rolebindings",
@@ -104,10 +116,17 @@ func fakeReadings() []*api.DataReading {
}
return append([]*api.DataReading{
+ {
+ DataGatherer: "ark/oidc",
+ Data: &api.OIDCDiscoveryData{
+ OIDCConfigError: "Failed to fetch /.well-known/openid-configuration: 404 Not Found",
+ JWKSError: "Failed to fetch /openid/v1/jwks: 404 Not Found",
+ },
+ },
{
DataGatherer: "ark/discovery",
Data: &api.DiscoveryData{
- ClusterID: "success-cluster-id",
+ ClusterID: "ffffffff-ffff-ffff-ffff-ffffffffffff",
ServerVersion: &k8sversion.Info{
GitVersion: "v1.21.0",
},
diff --git a/pkg/client/client_ngts.go b/pkg/client/client_ngts.go
new file mode 100644
index 00000000..69cca123
--- /dev/null
+++ b/pkg/client/client_ngts.go
@@ -0,0 +1,414 @@
+package client
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/google/uuid"
+ "github.com/microcosm-cc/bluemonday"
+ "k8s.io/client-go/transport"
+ "k8s.io/klog/v2"
+
+ "github.com/jetstack/preflight/api"
+ "github.com/jetstack/preflight/pkg/version"
+)
+
+// NGTSClient is a Client implementation for uploading data readings to NGTS
+// using service account keypair authentication. It follows the Private Key JWT
+// authentication pattern (RFC 7521 + RFC 7523).
+type NGTSClient struct {
+ credentials *NGTSServiceAccountCredentials
+ accessToken *ngtsAccessToken
+ baseURL *url.URL
+ agentMetadata *api.AgentMetadata
+
+ privateKey crypto.PrivateKey
+ jwtSigningAlg jwt.SigningMethod
+ lock sync.RWMutex
+
+ // Made public for testing purposes.
+ Client *http.Client
+}
+
+// NGTSServiceAccountCredentials holds the service account authentication credentials for NGTS.
+type NGTSServiceAccountCredentials struct {
+ // ClientID is the service account client ID
+ ClientID string `json:"client_id,omitempty"`
+ // PrivateKeyFile is the path to the private key file paired to
+ // the public key in the service account
+ PrivateKeyFile string `json:"private_key_file,omitempty"`
+}
+
+// ngtsAccessToken stores an NGTS access token and its expiration time.
+type ngtsAccessToken struct {
+ accessToken string
+ expirationTime time.Time
+}
+
+// ngtsAccessTokenResponse represents the JSON response from the NGTS token endpoint.
+type ngtsAccessTokenResponse struct {
+ AccessToken string `json:"access_token"` // base 64 encoded token
+ Type string `json:"token_type"` // always "bearer"
+ ExpiresIn int64 `json:"expires_in"` // number of seconds after which the access token will expire
+}
+
+const (
+ // ngtsProdURLFormat is the format used for constructing a URL for the production environment.
+ // The TSG ID is part of the URL.
+ ngtsProdURLFormat = "https://%s.ngts.paloaltonetworks.com"
+
+ // ngtsUploadEndpoint matches the "new" CM-SaaS upload endpoint
+ // Note that "no" is always passed to this endpoint in other paths (e.g. in the venafi-connection client and in the venafi-kubernetes-agent chart)
+ // so we copy that behavior here.
+ ngtsUploadEndpoint = "v1/tlspk/upload/clusterdata/no"
+
+ // ngtsAccessTokenEndpoint matches the CM-SaaS token endpoint
+ ngtsAccessTokenEndpoint = accessTokenEndpoint
+
+ // ngtsRequiredGrantType matches the CM-SaaS required grant type for JWTs
+ ngtsRequiredGrantType = requiredGrantType
+)
+
+// NewNGTSClient creates a new NGTS client that authenticates using keypair authentication
+// and uploads data to NGTS endpoints. Exactly one of tsgID or baseURL must be provided:
+// tsgID derives the production NGTS URL; baseURL sets a custom URL for testing.
+func NewNGTSClient(agentMetadata *api.AgentMetadata, credentials *NGTSServiceAccountCredentials, baseURL string, tsgID string, rootCAs *x509.CertPool) (*NGTSClient, error) {
+ // Load ClientID from file if not provided directly
+ if err := credentials.LoadClientIDIfNeeded(); err != nil {
+ return nil, fmt.Errorf("cannot create NGTSClient: %w", err)
+ }
+
+ if err := credentials.Validate(); err != nil {
+ return nil, fmt.Errorf("cannot create NGTSClient: %w", err)
+ }
+
+ // NB: There may be more validation which can be done here, e.g. see
+ // https://pan.dev/scm/api/tenancy/delete-tenancy-v-1-tenant-service-groups-tsg-id/
+ // > Possible values: >= 10 characters and <= 10 characters, Value must match regular expression ^1[0-9]+$
+ // For now, leaving this check simple
+ switch {
+ case tsgID != "" && baseURL != "":
+ return nil, fmt.Errorf("cannot create NGTSClient: tsgID and baseURL are mutually exclusive; exactly one must be provided")
+ case tsgID == "" && baseURL == "":
+ return nil, fmt.Errorf("cannot create NGTSClient: either tsgID or baseURL must be provided")
+ }
+
+ privateKey, jwtSigningAlg, err := parsePrivateKeyAndExtractSigningMethod(credentials.PrivateKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("while parsing private key file: %w", err)
+ }
+
+ actualBaseURL := baseURL
+ if actualBaseURL == "" {
+ actualBaseURL = fmt.Sprintf(ngtsProdURLFormat, tsgID)
+ }
+
+ parsedBaseURL, err := url.Parse(actualBaseURL)
+ if err != nil {
+ extra := ""
+
+ // A possible failure mode would be an incorrectly formatted TSG ID, so warn about that specifically
+ // if we tried to create a prod URL
+ if baseURL == "" {
+ extra = fmt.Sprintf(" (possibly malformed TSG ID %q?)", tsgID)
+ }
+
+ return nil, fmt.Errorf("invalid NGTS base URL %q: %s%s", baseURL, err, extra)
+ }
+
+ // Create HTTP transport that honors proxy settings and custom CA certs
+ tr := http.DefaultTransport.(*http.Transport).Clone()
+ if rootCAs != nil {
+ if tr.TLSClientConfig == nil {
+ tr.TLSClientConfig = &tls.Config{}
+ }
+ tr.TLSClientConfig.RootCAs = rootCAs
+ }
+
+ return &NGTSClient{
+ agentMetadata: agentMetadata,
+ credentials: credentials,
+ baseURL: parsedBaseURL,
+ accessToken: &ngtsAccessToken{},
+ Client: &http.Client{
+ Timeout: time.Minute,
+ Transport: transport.DebugWrappers(tr),
+ },
+ privateKey: privateKey,
+ jwtSigningAlg: jwtSigningAlg,
+ }, nil
+}
+
+// LoadClientIDIfNeeded attempts to load the ClientID from a file if it is not already set.
+// It looks for a "clientID" file in the same directory as the PrivateKeyFile.
+// For compatibility with the venafi-kubernetes-agent chart, it also supports "clientId" (lowercase 'd').
+// If both files exist, "clientID" takes precedence.
+// This allows the ClientID to be provided either as a direct value or via a Kubernetes secret.
+func (c *NGTSServiceAccountCredentials) LoadClientIDIfNeeded() error {
+ if c == nil {
+ return fmt.Errorf("credentials are nil")
+ }
+
+ // If ClientID is already set via helm values / CLI args, nothing to do
+ if c.ClientID != "" {
+ klog.V(2).Info("Using clientID from config.clientID helm value")
+ return nil
+ }
+
+ // We'd preferably have NGTSServiceAccountCredentials.CredentialPath but we didn't want to make another change
+ // to existing CLI flags; so we depend on PrivateKeyFile and assume clientID is in the same directory.
+
+ // If PrivateKeyFile is not set, we can't determine where to look for the clientID file
+ if c.PrivateKeyFile == "" {
+ return nil // This is actually a fatal error but will be caught by Validate() later
+ }
+
+ baseDir := path.Dir(c.PrivateKeyFile)
+
+ // Try to load ClientID from a file in the same directory as the private key
+ // Try "clientID" first (takes precedence), then "clientId" for backward compatibility
+ clientIDPath := baseDir + "/clientID"
+ clientIDBytes, err := os.ReadFile(clientIDPath)
+ if err != nil {
+ // Try the alternative "clientId" (lowercase 'd') for compatibility with venafi-kubernetes-agent
+ clientIDPath = baseDir + "/clientId"
+ clientIDBytes, err = os.ReadFile(clientIDPath)
+ if err != nil {
+ // If neither file exists, that's okay - we'll let Validate() catch the empty ClientID error later
+ klog.V(2).Info("Could not read clientID from file", "path", clientIDPath, "error", err)
+ return nil
+ }
+ }
+
+ // Trim whitespace from the clientID
+ c.ClientID = strings.TrimSpace(string(clientIDBytes))
+ klog.V(2).Info("Loaded clientID from file", "path", clientIDPath)
+
+ return nil
+}
+
+// Validate checks that the NGTS service account credentials are valid.
+func (c *NGTSServiceAccountCredentials) Validate() error {
+ if c == nil {
+ return fmt.Errorf("credentials are nil")
+ }
+
+ if c.ClientID == "" {
+ return fmt.Errorf("client_id cannot be empty")
+ }
+
+ if c.PrivateKeyFile == "" {
+ return fmt.Errorf("NGTS private key file location cannot be empty")
+ }
+
+ return nil
+}
+
+// PostDataReadingsWithOptions uploads data readings to the NGTS backend.
+// The TSG ID is included in the upload path to identify the tenant service group.
+func (c *NGTSClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, opts Options) error {
+ payload := api.DataReadingsPost{
+ AgentMetadata: c.agentMetadata,
+ DataGatherTime: time.Now().UTC(),
+ DataReadings: readings,
+ }
+ data, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+
+ uploadURL := c.baseURL.JoinPath(ngtsUploadEndpoint)
+
+ // Add cluster name and description as query parameters
+ query := uploadURL.Query()
+ stripHTML := bluemonday.StrictPolicy()
+ if opts.ClusterName != "" {
+ query.Add("name", stripHTML.Sanitize(opts.ClusterName))
+ }
+
+ if opts.ClusterDescription != "" {
+ query.Add("description", base64.RawURLEncoding.EncodeToString([]byte(stripHTML.Sanitize(opts.ClusterDescription))))
+ }
+
+ if opts.ClaimableCerts {
+ // The TLSPK backend reads "certOwnership=unassigned" — this is the backend contract.
+ query.Add("certOwnership", "unassigned")
+ }
+
+ uploadURL.RawQuery = query.Encode()
+
+ klog.FromContext(ctx).V(2).Info(
+ "uploading data readings to NGTS",
+ "url", uploadURL.String(),
+ "cluster_name", opts.ClusterName,
+ "data_readings_count", len(readings),
+ "data_size_bytes", len(data),
+ )
+
+ res, err := c.post(ctx, uploadURL.String(), bytes.NewBuffer(data))
+ if err != nil {
+ return fmt.Errorf("failed to upload data to NGTS: %w", err)
+ }
+ defer res.Body.Close()
+
+ if code := res.StatusCode; code < 200 || code >= 300 {
+ errorContent := ""
+ body, err := io.ReadAll(res.Body)
+ if err == nil {
+ errorContent = string(body)
+ }
+ return fmt.Errorf("NGTS upload failed with status code %d. Body: [%s]", code, errorContent)
+ }
+
+ return nil
+}
+
+// post performs an HTTP POST request to NGTS with authentication.
+func (c *NGTSClient) post(ctx context.Context, url string, body io.Reader) (*http.Response, error) {
+ token, err := c.getValidAccessToken(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+ version.SetUserAgent(req)
+
+ if len(token.accessToken) > 0 {
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.accessToken))
+ }
+
+ return c.Client.Do(req)
+}
+
+// getValidAccessToken returns a valid access token. It will fetch a new access
+// token from the auth server if the current token does not exist or has expired.
+func (c *NGTSClient) getValidAccessToken(ctx context.Context) (*ngtsAccessToken, error) {
+ c.lock.RLock()
+ needsUpdate := c.accessToken == nil || time.Now().Add(time.Minute).After(c.accessToken.expirationTime)
+ c.lock.RUnlock()
+
+ if needsUpdate {
+ err := c.updateAccessToken(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ c.lock.RLock()
+ token := c.accessToken
+ c.lock.RUnlock()
+
+ return token, nil
+}
+
+// updateAccessToken fetches a new access token from the NGTS auth server using JWT authentication.
+func (c *NGTSClient) updateAccessToken(ctx context.Context) error {
+ jwtToken, err := c.generateAndSignJwtToken()
+ if err != nil {
+ return fmt.Errorf("failed to generate JWT token for NGTS authentication: %w", err)
+ }
+
+ values := url.Values{}
+ values.Set("grant_type", ngtsRequiredGrantType)
+ values.Set("assertion", jwtToken)
+
+ tokenURL := c.baseURL.JoinPath(ngtsAccessTokenEndpoint).String()
+
+ encoded := values.Encode()
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(encoded))
+ if err != nil {
+ return err
+ }
+
+ request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ request.Header.Add("Content-Length", strconv.Itoa(len(encoded)))
+ version.SetUserAgent(request)
+
+ now := time.Now()
+ accessToken := ngtsAccessTokenResponse{}
+ err = c.sendHTTPRequest(request, &accessToken)
+ if err != nil {
+ return fmt.Errorf("failed to obtain NGTS access token: %w", err)
+ }
+
+ c.lock.Lock()
+ c.accessToken = &ngtsAccessToken{
+ accessToken: accessToken.AccessToken,
+ expirationTime: now.Add(time.Duration(accessToken.ExpiresIn) * time.Second),
+ }
+ c.lock.Unlock()
+ return nil
+}
+
+// sendHTTPRequest executes an HTTP request and unmarshals the JSON response.
+func (c *NGTSClient) sendHTTPRequest(request *http.Request, responseObject any) error {
+ response, err := c.Client.Do(request)
+ if err != nil {
+ return err
+ }
+
+ defer response.Body.Close()
+
+ if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(response.Body)
+ return fmt.Errorf("NGTS API request failed. Request %s, status code: %d, body: [%s]", request.URL, response.StatusCode, body)
+ }
+
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return err
+ }
+
+ if err = json.Unmarshal(body, responseObject); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// generateAndSignJwtToken creates a JWT token signed with the service account's private key
+// for authenticating to NGTS.
+func (c *NGTSClient) generateAndSignJwtToken() (string, error) {
+ // backend still expects "api.venafi.cloud/v1/oauth/token/serviceaccount" for audience, so force that for now
+ venafiCloudProdURL, err := url.Parse(VenafiCloudProdURL)
+ if err != nil {
+ return "", err
+ }
+
+ claims := make(jwt.MapClaims)
+ claims["sub"] = c.credentials.ClientID
+ claims["iss"] = c.credentials.ClientID
+ claims["iat"] = time.Now().Unix()
+ claims["exp"] = time.Now().Add(time.Minute).Unix()
+ claims["aud"] = path.Join(venafiCloudProdURL.Host, ngtsAccessTokenEndpoint)
+ claims["jti"] = uuid.New().String()
+
+ token, err := jwt.NewWithClaims(c.jwtSigningAlg, claims).SignedString(c.privateKey)
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}
diff --git a/pkg/client/client_ngts_test.go b/pkg/client/client_ngts_test.go
new file mode 100644
index 00000000..5f869106
--- /dev/null
+++ b/pkg/client/client_ngts_test.go
@@ -0,0 +1,477 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/jetstack/preflight/api"
+)
+
+const fakePrivKeyPEM = `-----BEGIN PRIVATE KEY-----
+MHcCAQEEIFptpPXOvEWDrYkiMhyEH1+FB1GwtwX2tyXH4KtBO6g7oAoGCCqGSM49
+AwEHoUQDQgAE/BsIwagYc4YUjSSFyqcStj2qliAkdVGlMoJbMuXupzQ9Qs4TX5Pl
+dFjz6J/j6Gu4fLPqXmM61Hj6kiuRHx5eHQ==
+-----END PRIVATE KEY-----
+`
+
+func withFile(t testing.TB, content string) string {
+ t.Helper()
+
+ f, err := os.CreateTemp(t.TempDir(), "file")
+ if err != nil {
+ t.Fatalf("failed to create temporary file: %v", err)
+ }
+ defer f.Close()
+
+ _, err = f.WriteString(content)
+ if err != nil {
+ t.Fatalf("failed to write to temporary file: %v", err)
+ }
+
+ return f.Name()
+}
+
+func TestNewNGTSClient(t *testing.T) {
+ // Create a temporary key file
+ keyFile := withFile(t, fakePrivKeyPEM)
+
+ tests := []struct {
+ name string
+ credentials *NGTSServiceAccountCredentials
+ baseURL string
+ tsgID string
+ wantErr bool
+ errContains string
+ }{
+ {
+ name: "valid credentials with baseURL only",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ },
+ baseURL: "https://test.ngts.example.com",
+ tsgID: "",
+ wantErr: false,
+ },
+ {
+ name: "valid credentials with tsgID only",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ },
+ baseURL: "",
+ tsgID: "test-tsg-id",
+ wantErr: false,
+ },
+ {
+ name: "tsgID and baseURL are mutually exclusive",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ },
+ baseURL: "https://test.ngts.example.com",
+ tsgID: "test-tsg-id",
+ wantErr: true,
+ errContains: "tsgID and baseURL are mutually exclusive",
+ },
+ {
+ name: "missing both tsgID and baseURL",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ },
+ baseURL: "",
+ tsgID: "",
+ wantErr: true,
+ errContains: "either tsgID or baseURL must be provided",
+ },
+ {
+ name: "missing clientID without file",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "",
+ PrivateKeyFile: keyFile,
+ },
+ baseURL: "https://test.ngts.example.com",
+ tsgID: "",
+ wantErr: true,
+ errContains: "client_id cannot be empty",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ metadata := &api.AgentMetadata{
+ Version: "test-version",
+ ClusterID: "test-cluster",
+ }
+
+ client, err := NewNGTSClient(metadata, tt.credentials, tt.baseURL, tt.tsgID, nil)
+
+ if tt.wantErr {
+ require.Error(t, err)
+ if tt.errContains != "" {
+ assert.Contains(t, err.Error(), tt.errContains)
+ }
+ assert.Nil(t, client)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.NotNil(t, client)
+ if tt.baseURL != "" {
+ assert.Equal(t, tt.baseURL, client.baseURL.String())
+ return
+ }
+
+ assert.Equal(t, fmt.Sprintf(ngtsProdURLFormat, tt.tsgID), client.baseURL.String())
+ })
+ }
+}
+
+func TestNGTSClient_LoadClientIDFromFile(t *testing.T) {
+ // Create a temporary directory for the secret files
+ tmpDir := t.TempDir()
+
+ // Create the private key file
+ keyFile := tmpDir + "/privatekey.pem"
+ err := os.WriteFile(keyFile, []byte(fakePrivKeyPEM), 0o600)
+ require.NoError(t, err)
+
+ // Create the clientID file in the same directory
+ clientIDFile := tmpDir + "/clientID"
+ err = os.WriteFile(clientIDFile, []byte("test-client-from-file\n"), 0o600)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ credentials *NGTSServiceAccountCredentials
+ wantErr bool
+ wantClient string
+ }{
+ {
+ name: "load clientID from file",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "", // Empty - should be loaded from file
+ PrivateKeyFile: keyFile,
+ },
+ wantErr: false,
+ wantClient: "test-client-from-file",
+ },
+ {
+ name: "explicit clientID takes precedence",
+ credentials: &NGTSServiceAccountCredentials{
+ ClientID: "explicit-client-id",
+ PrivateKeyFile: keyFile,
+ },
+ wantErr: false,
+ wantClient: "explicit-client-id",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ metadata := &api.AgentMetadata{
+ Version: "test-version",
+ ClusterID: "test-cluster",
+ }
+
+ client, err := NewNGTSClient(metadata, tt.credentials, "https://test.example.com", "", nil)
+
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.NotNil(t, client)
+ assert.Equal(t, tt.wantClient, client.credentials.ClientID)
+ })
+ }
+}
+
+func TestNGTSClient_LoadClientIDFromFileAlternativeNames(t *testing.T) {
+ tests := []struct {
+ name string
+ setupFiles func(tmpDir string) string // returns keyFile path
+ wantClientID string
+ wantErr bool
+ wantErrContain string
+ }{
+ {
+ // Note: venafi-kubernetes-agent didn't support storing the client ID in the secret, but
+ // we don't want users moving to discovery-agent to be caught out by such a trivial mistake.
+ name: "load from clientId (lowercase d) for venafi-kubernetes-agent compatibility",
+ setupFiles: func(tmpDir string) string {
+ keyFile := tmpDir + "/privatekey.pem"
+ err := os.WriteFile(keyFile, []byte(fakePrivKeyPEM), 0o600)
+ require.NoError(t, err)
+ // Create clientId file (lowercase 'd')
+ clientIdFile := tmpDir + "/clientId"
+ err = os.WriteFile(clientIdFile, []byte("test-client-from-clientId\n"), 0o600)
+ require.NoError(t, err)
+ return keyFile
+ },
+ wantClientID: "test-client-from-clientId",
+ wantErr: false,
+ },
+ {
+ name: "load from clientID (uppercase D)",
+ setupFiles: func(tmpDir string) string {
+ keyFile := tmpDir + "/privatekey.pem"
+ err := os.WriteFile(keyFile, []byte(fakePrivKeyPEM), 0o600)
+ require.NoError(t, err)
+ // Create only clientID file (uppercase 'D')
+ clientIDFile := tmpDir + "/clientID"
+ err = os.WriteFile(clientIDFile, []byte("from-clientID"), 0o600)
+ require.NoError(t, err)
+ return keyFile
+ },
+ wantClientID: "from-clientID",
+ wantErr: false,
+ },
+ {
+ name: "error when no clientID file exists",
+ setupFiles: func(tmpDir string) string {
+ keyFile := tmpDir + "/privatekey.pem"
+ err := os.WriteFile(keyFile, []byte(fakePrivKeyPEM), 0o600)
+ require.NoError(t, err)
+ // Don't create any clientID file
+ return keyFile
+ },
+ wantErr: true,
+ wantErrContain: "client_id cannot be empty",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpDir := t.TempDir()
+ keyFile := tt.setupFiles(tmpDir)
+
+ credentials := &NGTSServiceAccountCredentials{
+ ClientID: "", // Empty - should be loaded from file
+ PrivateKeyFile: keyFile,
+ }
+
+ metadata := &api.AgentMetadata{
+ Version: "test-version",
+ ClusterID: "test-cluster",
+ }
+
+ client, err := NewNGTSClient(metadata, credentials, "https://test.example.com", "", nil)
+
+ if tt.wantErr {
+ require.Error(t, err)
+ if tt.wantErrContain != "" {
+ assert.Contains(t, err.Error(), tt.wantErrContain)
+ }
+ return
+ }
+
+ require.NoError(t, err)
+ assert.NotNil(t, client)
+ assert.Equal(t, tt.wantClientID, client.credentials.ClientID)
+ })
+ }
+}
+
+func TestNGTSClient_PostDataReadingsWithOptions(t *testing.T) {
+ keyFile := withFile(t, fakePrivKeyPEM)
+
+ // Create a test server that simulates NGTS backend
+ var receivedRequest *http.Request
+ var receivedBody []byte
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ receivedRequest = r
+
+ // First request is for access token
+ if r.URL.Path == ngtsAccessTokenEndpoint {
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(ngtsAccessTokenResponse{
+ AccessToken: "test-access-token",
+ Type: "bearer",
+ ExpiresIn: 3600,
+ })
+ return
+ }
+
+ // Second request is for data upload
+ body := make([]byte, r.ContentLength)
+ _, _ = r.Body.Read(body)
+ receivedBody = body
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"status": "success"}`))
+ }))
+ defer server.Close()
+
+ credentials := &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ }
+
+ metadata := &api.AgentMetadata{
+ Version: "test-version",
+ ClusterID: "test-cluster",
+ }
+
+ client, err := NewNGTSClient(metadata, credentials, server.URL, "", nil)
+ require.NoError(t, err)
+
+ // Test data upload
+ readings := []*api.DataReading{
+ {
+ DataGatherer: "test-gatherer",
+ Timestamp: api.Time{},
+ Data: &api.DynamicData{},
+ },
+ }
+
+ opts := Options{
+ ClusterName: "test-cluster",
+ ClusterDescription: "Test cluster description",
+ }
+
+ err = client.PostDataReadingsWithOptions(t.Context(), readings, opts)
+ require.NoError(t, err)
+
+ // Verify the upload request
+ assert.NotNil(t, receivedRequest)
+ assert.Equal(t, "/"+ngtsUploadEndpoint, receivedRequest.URL.Path)
+ assert.Contains(t, receivedRequest.URL.RawQuery, "name=test-cluster")
+ assert.Equal(t, "Bearer test-access-token", receivedRequest.Header.Get("Authorization"))
+ // certOwnership not set — must NOT appear in query
+ assert.NotContains(t, receivedRequest.URL.RawQuery, "certOwnership")
+
+ // Verify the payload
+ var payload api.DataReadingsPost
+ err = json.Unmarshal(receivedBody, &payload)
+ require.NoError(t, err)
+ assert.Equal(t, 1, len(payload.DataReadings))
+
+ // Verify claimableCerts=true is included when set
+ t.Run("claimableCerts: true sends certOwnership=unassigned to backend", func(t *testing.T) {
+ optsUnassigned := Options{
+ ClusterName: "test-cluster",
+ ClaimableCerts: true,
+ }
+ err = client.PostDataReadingsWithOptions(t.Context(), readings, optsUnassigned)
+ require.NoError(t, err)
+ assert.Contains(t, receivedRequest.URL.RawQuery, "certOwnership=unassigned")
+ })
+}
+
+func TestNGTSClient_AuthenticationFlow(t *testing.T) {
+ keyFile := withFile(t, fakePrivKeyPEM)
+
+ authCallCount := 0
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == ngtsAccessTokenEndpoint {
+ authCallCount++
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(ngtsAccessTokenResponse{
+ AccessToken: "test-access-token",
+ Type: "bearer",
+ ExpiresIn: 3600,
+ })
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ credentials := &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ }
+
+ metadata := &api.AgentMetadata{
+ Version: "test-version",
+ ClusterID: "test-cluster",
+ }
+
+ client, err := NewNGTSClient(metadata, credentials, server.URL, "", nil)
+ require.NoError(t, err)
+
+ // Make multiple requests - should only authenticate once
+ readings := []*api.DataReading{{DataGatherer: "test", Data: &api.DynamicData{}}}
+ opts := Options{ClusterName: "test"}
+
+ for range 3 {
+ err = client.PostDataReadingsWithOptions(t.Context(), readings, opts)
+ require.NoError(t, err)
+ }
+
+ // Should only authenticate once since token is cached
+ assert.Equal(t, 1, authCallCount)
+}
+
+func TestNGTSClient_ErrorHandling(t *testing.T) {
+ keyFile := withFile(t, fakePrivKeyPEM)
+
+ tests := []struct {
+ name string
+ serverHandler http.HandlerFunc
+ expectedErrMsg string
+ }{
+ {
+ name: "authentication failure",
+ serverHandler: func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == ngtsAccessTokenEndpoint {
+ w.WriteHeader(http.StatusUnauthorized)
+ _, _ = w.Write([]byte(`{"error": "invalid_client"}`))
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ },
+ expectedErrMsg: "failed to obtain NGTS access token",
+ },
+ {
+ name: "upload failure",
+ serverHandler: func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == ngtsAccessTokenEndpoint {
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(ngtsAccessTokenResponse{
+ AccessToken: "test-token",
+ Type: "bearer",
+ ExpiresIn: 3600,
+ })
+ return
+ }
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(`{"error": "internal server error"}`))
+ },
+ expectedErrMsg: "NGTS upload failed with status code 500",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(tt.serverHandler)
+ defer server.Close()
+
+ credentials := &NGTSServiceAccountCredentials{
+ ClientID: "test-client-id",
+ PrivateKeyFile: keyFile,
+ }
+
+ metadata := &api.AgentMetadata{Version: "test", ClusterID: "test"}
+ client, err := NewNGTSClient(metadata, credentials, server.URL, "", nil)
+ require.NoError(t, err)
+
+ readings := []*api.DataReading{{DataGatherer: "test", Data: &api.DynamicData{}}}
+ opts := Options{ClusterName: "test"}
+
+ err = client.PostDataReadingsWithOptions(t.Context(), readings, opts)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.expectedErrMsg)
+ })
+ }
+}
diff --git a/pkg/client/client_oauth.go b/pkg/client/client_oauth.go
index 5cc1e64f..b7ad93ea 100644
--- a/pkg/client/client_oauth.go
+++ b/pkg/client/client_oauth.go
@@ -159,8 +159,8 @@ func (c *OAuthClient) post(ctx context.Context, path string, body io.Reader) (*h
return nil, err
}
+ req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
-
version.SetUserAgent(req)
if len(token.bearer) > 0 {
diff --git a/pkg/client/client_venafi_cloud.go b/pkg/client/client_venafi_cloud.go
index 6b5da890..4579cdc9 100644
--- a/pkg/client/client_venafi_cloud.go
+++ b/pkg/client/client_venafi_cloud.go
@@ -4,18 +4,12 @@ import (
"bytes"
"context"
"crypto"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
- "crypto/x509"
"encoding/base64"
"encoding/json"
- "encoding/pem"
"fmt"
"io"
"net/http"
"net/url"
- "os"
"path"
"path/filepath"
"strconv"
@@ -259,14 +253,22 @@ func (c *VenafiCloudClient) post(ctx context.Context, path string, body io.Reade
// token from the auth server in case the current access token does not exist
// or it is expired.
func (c *VenafiCloudClient) getValidAccessToken(ctx context.Context) (*venafiCloudAccessToken, error) {
- if c.accessToken == nil || time.Now().Add(time.Minute).After(c.accessToken.expirationTime) {
+ c.lock.RLock()
+ needsUpdate := c.accessToken == nil || time.Now().Add(time.Minute).After(c.accessToken.expirationTime)
+ c.lock.RUnlock()
+
+ if needsUpdate {
err := c.updateAccessToken(ctx)
if err != nil {
return nil, err
}
}
- return c.accessToken, nil
+ c.lock.RLock()
+ token := c.accessToken
+ c.lock.RUnlock()
+
+ return token, nil
}
func (c *VenafiCloudClient) updateAccessToken(ctx context.Context) error {
@@ -353,73 +355,3 @@ func (c *VenafiCloudClient) generateAndSignJwtToken() (string, error) {
return token, nil
}
-
-func parsePrivateKeyFromPemFile(privateKeyFilePath string) (crypto.PrivateKey, error) {
- pkBytes, err := os.ReadFile(privateKeyFilePath)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch Venafi Cloud authentication private key %q: %s",
- privateKeyFilePath, err)
- }
-
- der, _ := pem.Decode(pkBytes)
- if der == nil {
- return nil, fmt.Errorf("while decoding the PEM-encoded private key %v, its content were: %s", privateKeyFilePath, string(pkBytes))
- }
-
- if key, err := x509.ParsePKCS1PrivateKey(der.Bytes); err == nil {
- return key, nil
- }
- if key, err := x509.ParsePKCS8PrivateKey(der.Bytes); err == nil {
- switch key := key.(type) {
- case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
- return key, nil
- default:
- return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
- }
- }
- if key, err := x509.ParseECPrivateKey(der.Bytes); err == nil {
- return key, nil
- }
- return nil, fmt.Errorf("while parsing EC private: %w", err)
-}
-
-func parsePrivateKeyAndExtractSigningMethod(privateKeyFile string) (crypto.PrivateKey, jwt.SigningMethod, error) {
-
- privateKey, err := parsePrivateKeyFromPemFile(privateKeyFile)
- if err != nil {
- return nil, nil, err
- }
-
- var signingMethod jwt.SigningMethod
- switch key := privateKey.(type) {
- case *rsa.PrivateKey:
- bitLen := key.N.BitLen()
- switch bitLen {
- case 2048:
- signingMethod = jwt.SigningMethodRS256
- case 3072:
- signingMethod = jwt.SigningMethodRS384
- case 4096:
- signingMethod = jwt.SigningMethodRS512
- default:
- signingMethod = jwt.SigningMethodRS256
- }
- case *ecdsa.PrivateKey:
- bitLen := key.Curve.Params().BitSize
- switch bitLen {
- case 256:
- signingMethod = jwt.SigningMethodES256
- case 384:
- signingMethod = jwt.SigningMethodES384
- case 521:
- signingMethod = jwt.SigningMethodES512
- default:
- signingMethod = jwt.SigningMethodES256
- }
- case ed25519.PrivateKey:
- signingMethod = jwt.SigningMethodEdDSA
- default:
- err = fmt.Errorf("unsupported private key type")
- }
- return privateKey, signingMethod, err
-}
diff --git a/pkg/client/client_venconn.go b/pkg/client/client_venconn.go
index 9ce0c86b..9a6f28e0 100644
--- a/pkg/client/client_venconn.go
+++ b/pkg/client/client_venconn.go
@@ -13,8 +13,10 @@ import (
"time"
venapi "github.com/jetstack/venafi-connection-lib/api/v1alpha1"
- "github.com/jetstack/venafi-connection-lib/chain/sources/venafi"
+ "github.com/jetstack/venafi-connection-lib/connection_details"
+ "github.com/jetstack/venafi-connection-lib/sources/venafi"
"github.com/jetstack/venafi-connection-lib/venafi_client"
+ "github.com/microcosm-cc/bluemonday"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
@@ -137,17 +139,25 @@ func (c *VenConnClient) PostDataReadingsWithOptions(ctx context.Context, reading
if err != nil {
return fmt.Errorf("while loading the VenafiConnection %s/%s: %w", c.venConnNS, c.venConnName, err)
}
- if details.TPP != nil {
- return fmt.Errorf(`VenafiConnection %s/%s: the agent cannot be used with TPP`, c.venConnNS, c.venConnName)
- }
- if details.VCP != nil && details.VCP.APIKey != "" {
+ server := details.ServerDetails()
+ switch server.BackendAuth {
+ case connection_details.VCPAccessToken, connection_details.NGTSAccessToken:
+ // Supported.
+ case connection_details.TPPAccessToken:
+ return fmt.Errorf(`VenafiConnection %s/%s: the agent does not support the TPP backend`, c.venConnNS, c.venConnName)
+ case connection_details.DistributedIssuerAccessToken:
+ return fmt.Errorf(`VenafiConnection %s/%s: the agent does not support the Distributed Issuer backend`, c.venConnNS, c.venConnName)
+ case connection_details.VCPAPIKey:
// Although it is technically possible to use an API key, we have
// decided to not allow it as it isn't recommended and will eventually
// be phased out.
- return fmt.Errorf(`VenafiConnection %s/%s: the agent cannot be used with an API key`, c.venConnNS, c.venConnName)
+ return fmt.Errorf(`VenafiConnection %s/%s: the agent does not support API key authentication with the VCP backend`, c.venConnNS, c.venConnName)
+ default:
+ return fmt.Errorf(`VenafiConnection %s/%s: the agent does not support backend auth %q`, c.venConnNS, c.venConnName, server.BackendAuth)
}
- if details.VCP == nil || details.VCP.AccessToken == "" {
- return fmt.Errorf(`programmer mistake: VenafiConnection %s/%s: TPPAccessToken is empty in the token returned by connHandler.Get: %v`, c.venConnNS, c.venConnName, details)
+ token := details.Credential()
+ if token == "" {
+ return fmt.Errorf(`programmer mistake: VenafiConnection %s/%s: access token is empty in the connection details returned by connHandler.Get`, c.venConnNS, c.venConnName)
}
payload := api.DataReadingsPost{
@@ -160,9 +170,10 @@ func (c *VenConnClient) PostDataReadingsWithOptions(ctx context.Context, reading
return err
}
+ uploadURL := fullURL(server.BaseURL, "/v1/tlspk/upload/clusterdata/no")
klog.FromContext(ctx).V(2).Info(
"uploading data readings",
- "url", fullURL(details.VCP.URL, "/v1/tlspk/upload/clusterdata/no"),
+ "url", uploadURL,
"cluster_name", opts.ClusterName,
"data_readings_count", len(readings),
"data_size_bytes", len(data),
@@ -171,21 +182,30 @@ func (c *VenConnClient) PostDataReadingsWithOptions(ctx context.Context, reading
// The path parameter "no" is a dummy parameter to make the Venafi Cloud
// backend happy. This parameter, named `uploaderID` in the backend, is not
// actually used by the backend.
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL(details.VCP.URL, "/v1/tlspk/upload/clusterdata/no"), bytes.NewReader(data))
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, bytes.NewReader(data))
if err != nil {
return err
}
+ req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", details.VCP.AccessToken))
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
version.SetUserAgent(req)
- q := req.URL.Query()
- q.Set("name", opts.ClusterName)
+ query := req.URL.Query()
+ stripHTML := bluemonday.StrictPolicy()
+ if opts.ClusterName != "" {
+ query.Add("name", stripHTML.Sanitize(opts.ClusterName))
+ }
if opts.ClusterDescription != "" {
- q.Set("description", base64.RawURLEncoding.EncodeToString([]byte(opts.ClusterDescription)))
+ query.Add("description", base64.RawURLEncoding.EncodeToString([]byte(stripHTML.Sanitize(opts.ClusterDescription))))
}
- req.URL.RawQuery = q.Encode()
+ if isNGTS := server.BackendAuth == connection_details.NGTSAccessToken; isNGTS && opts.ClaimableCerts {
+ // The TLSPK backend reads "certOwnership=unassigned" — this is the backend contract.
+ query.Add("certOwnership", "unassigned")
+ }
+
+ req.URL.RawQuery = query.Encode()
res, err := c.Client.Do(req)
if err != nil {
diff --git a/pkg/client/client_venconn_test.go b/pkg/client/client_venconn_test.go
index 6b9e14a4..adfb633a 100644
--- a/pkg/client/client_venconn_test.go
+++ b/pkg/client/client_venconn_test.go
@@ -156,7 +156,64 @@ func TestVenConnClient_PostDataReadingsWithOptions(t *testing.T) {
// PostDataReadingsWithOptions failed, but Get succeeded; that's why the
// condition says the VenafiConnection is ready.
expectReadyCondMsg: "Generated a new token",
- expectErr: "VenafiConnection error-when-the-apikey-field-is-used/venafi-components: the agent cannot be used with an API key",
+ expectErr: "VenafiConnection error-when-the-apikey-field-is-used/venafi-components: the agent does not support API key authentication with the VCP backend",
+ }))
+ t.Run("error when the distributedIssuer field is used", run_TestVenConnClient_PostDataReadingsWithOptions(ctx, restconf, kclient, testcase{
+ // The Firefly / Distributed Issuer backend reaches the
+ // `connection_details.DistributedIssuerAccessToken` branch, which the
+ // agent rejects since it only supports VCP and NGTS upload backends.
+ given: testutil.Undent(`
+ apiVersion: jetstack.io/v1alpha1
+ kind: VenafiConnection
+ metadata:
+ name: venafi-components
+ namespace: TEST_NAMESPACE
+ spec:
+ distributedIssuer:
+ url: FAKE_TPP_URL
+ accessToken:
+ - secret:
+ name: accesstoken
+ fields: [accesstoken]
+ allowReferencesFrom:
+ matchExpressions:
+ - {key: kubernetes.io/metadata.name, operator: In, values: [venafi]}
+ ---
+ apiVersion: v1
+ kind: Secret
+ metadata:
+ name: accesstoken
+ namespace: TEST_NAMESPACE
+ stringData:
+ accesstoken: VALID_ACCESS_TOKEN
+ ---
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: Role
+ metadata:
+ name: venafi-connection-accesstoken-reader
+ namespace: TEST_NAMESPACE
+ rules:
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get"]
+ resourceNames: ["accesstoken"]
+ ---
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: RoleBinding
+ metadata:
+ name: venafi-connection-accesstoken-reader
+ namespace: TEST_NAMESPACE
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: venafi-connection-accesstoken-reader
+ subjects:
+ - kind: ServiceAccount
+ name: venafi-connection
+ namespace: venafi
+ `),
+ expectReadyCondMsg: "Generated a new token",
+ expectErr: "VenafiConnection error-when-the-distributedissuer-field-is-used/venafi-components: the agent does not support the Distributed Issuer backend",
}))
t.Run("error when the tpp field is used", run_TestVenConnClient_PostDataReadingsWithOptions(ctx, restconf, kclient, testcase{
// IMPORTANT: The user may think they can use 'tpp', spend time
@@ -214,7 +271,7 @@ func TestVenConnClient_PostDataReadingsWithOptions(t *testing.T) {
namespace: venafi
`),
expectReadyCondMsg: "Generated a new token",
- expectErr: "VenafiConnection error-when-the-tpp-field-is-used/venafi-components: the agent cannot be used with TPP",
+ expectErr: "VenafiConnection error-when-the-tpp-field-is-used/venafi-components: the agent does not support the TPP backend",
}))
}
diff --git a/pkg/client/util.go b/pkg/client/util.go
new file mode 100644
index 00000000..fc655c7c
--- /dev/null
+++ b/pkg/client/util.go
@@ -0,0 +1,89 @@
+package client
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "os"
+
+ "github.com/golang-jwt/jwt/v4"
+)
+
+// parsePrivateKeyFromPEMFile reads and parses a PEM-encoded private key file.
+func parsePrivateKeyFromPEMFile(privateKeyFilePath string) (crypto.PrivateKey, error) {
+ pkBytes, err := os.ReadFile(privateKeyFilePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch private key %q: %s",
+ privateKeyFilePath, err)
+ }
+
+ der, _ := pem.Decode(pkBytes)
+ if der == nil {
+ return nil, fmt.Errorf("while decoding the PEM-encoded private key %v, its content were: %s", privateKeyFilePath, string(pkBytes))
+ }
+
+ if key, err := x509.ParsePKCS1PrivateKey(der.Bytes); err == nil {
+ return key, nil
+ }
+ if key, err := x509.ParsePKCS8PrivateKey(der.Bytes); err == nil {
+ switch key := key.(type) {
+ case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
+ return key, nil
+ default:
+ return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
+ }
+ }
+ if key, err := x509.ParseECPrivateKey(der.Bytes); err == nil {
+ return key, nil
+ }
+ return nil, fmt.Errorf("while parsing EC private: %w", err)
+}
+
+// parsePrivateKeyAndExtractSigningMethod parses a private key file and determines
+// the appropriate JWT signing method based on the key type and size.
+func parsePrivateKeyAndExtractSigningMethod(privateKeyFile string) (crypto.PrivateKey, jwt.SigningMethod, error) {
+ privateKey, err := parsePrivateKeyFromPEMFile(privateKeyFile)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var signingMethod jwt.SigningMethod
+ switch key := privateKey.(type) {
+ case *rsa.PrivateKey:
+ bitLen := key.N.BitLen()
+ switch bitLen {
+ case 2048:
+ signingMethod = jwt.SigningMethodRS256
+ case 3072:
+ signingMethod = jwt.SigningMethodRS384
+ case 4096:
+ signingMethod = jwt.SigningMethodRS512
+ default:
+ signingMethod = jwt.SigningMethodRS256
+ }
+
+ case *ecdsa.PrivateKey:
+ bitLen := key.Curve.Params().BitSize
+ switch bitLen {
+ case 256:
+ signingMethod = jwt.SigningMethodES256
+ case 384:
+ signingMethod = jwt.SigningMethodES384
+ case 521:
+ signingMethod = jwt.SigningMethodES512
+ default:
+ signingMethod = jwt.SigningMethodES256
+ }
+
+ case ed25519.PrivateKey:
+ signingMethod = jwt.SigningMethodEdDSA
+
+ default:
+ err = fmt.Errorf("unsupported private key type")
+ }
+ return privateKey, signingMethod, err
+}
diff --git a/pkg/datagatherer/datagatherer.go b/pkg/datagatherer/datagatherer.go
index 0baab09d..afea88ec 100644
--- a/pkg/datagatherer/datagatherer.go
+++ b/pkg/datagatherer/datagatherer.go
@@ -14,7 +14,7 @@ type DataGatherer interface {
// Fetch retrieves data.
// count is the number of items that were discovered. A negative count means the number
// of items was indeterminate.
- Fetch() (data any, count int, err error)
+ Fetch(ctx context.Context) (data any, count int, err error)
// Run starts the data gatherer's informers for resource collection.
// Returns error if the data gatherer informer wasn't initialized
Run(ctx context.Context) error
diff --git a/pkg/datagatherer/k8sdiscovery/discovery.go b/pkg/datagatherer/k8sdiscovery/discovery.go
index e4ce54b5..3033c85f 100644
--- a/pkg/datagatherer/k8sdiscovery/discovery.go
+++ b/pkg/datagatherer/k8sdiscovery/discovery.go
@@ -76,7 +76,7 @@ func (g *DataGathererDiscovery) WaitForCacheSync(ctx context.Context) error {
}
// Fetch will fetch discovery data from the apiserver, or return an error
-func (g *DataGathererDiscovery) Fetch() (any, int, error) {
+func (g *DataGathererDiscovery) Fetch(ctx context.Context) (any, int, error) {
data, err := g.cl.ServerVersion()
if err != nil {
return nil, -1, fmt.Errorf("failed to get server version: %v", err)
diff --git a/pkg/datagatherer/k8sdynamic/dynamic.go b/pkg/datagatherer/k8sdynamic/dynamic.go
index 7a6349be..6eed8db2 100644
--- a/pkg/datagatherer/k8sdynamic/dynamic.go
+++ b/pkg/datagatherer/k8sdynamic/dynamic.go
@@ -34,6 +34,7 @@ package k8sdynamic
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"regexp"
@@ -49,6 +50,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
+ "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
@@ -60,6 +62,7 @@ import (
"k8s.io/klog/v2"
"github.com/jetstack/preflight/api"
+ "github.com/jetstack/preflight/internal/envelope"
"github.com/jetstack/preflight/pkg/datagatherer"
"github.com/jetstack/preflight/pkg/kubeconfig"
"github.com/jetstack/preflight/pkg/logs"
@@ -77,6 +80,12 @@ type ConfigDynamic struct {
IncludeNamespaces []string `yaml:"include-namespaces"`
// FieldSelectors is a list of field selectors to use when listing this resource
FieldSelectors []string `yaml:"field-selectors"`
+ // LabelSelectors is a list of label selectors to use when listing this resource
+ LabelSelectors []string `yaml:"label-selectors"`
+ // ExcludeAnnotationKeysRegex is a list of regular expressions to exclude.
+ ExcludeAnnotationKeysRegex []string `yaml:"excludeAnnotationKeysRegex"`
+ // ExcludeLabelKeysRegex is a list of regular expressions to exclude.
+ ExcludeLabelKeysRegex []string `yaml:"excludeLabelKeysRegex"`
}
// UnmarshalYAML unmarshals the ConfigDynamic resolving GroupVersionResource.
@@ -88,9 +97,12 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error {
Version string `yaml:"version"`
Resource string `yaml:"resource"`
} `yaml:"resource-type"`
- ExcludeNamespaces []string `yaml:"exclude-namespaces"`
- IncludeNamespaces []string `yaml:"include-namespaces"`
- FieldSelectors []string `yaml:"field-selectors"`
+ ExcludeNamespaces []string `yaml:"exclude-namespaces"`
+ IncludeNamespaces []string `yaml:"include-namespaces"`
+ FieldSelectors []string `yaml:"field-selectors"`
+ LabelSelectors []string `yaml:"label-selectors"`
+ ExcludeAnnotationKeysRegex []string `yaml:"excludeAnnotationKeysRegex"`
+ ExcludeLabelKeysRegex []string `yaml:"excludeLabelKeysRegex"`
}{}
err := unmarshal(&aux)
if err != nil {
@@ -104,6 +116,9 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error {
c.ExcludeNamespaces = aux.ExcludeNamespaces
c.IncludeNamespaces = aux.IncludeNamespaces
c.FieldSelectors = aux.FieldSelectors
+ c.LabelSelectors = aux.LabelSelectors
+ c.ExcludeAnnotationKeysRegex = aux.ExcludeAnnotationKeysRegex
+ c.ExcludeLabelKeysRegex = aux.ExcludeLabelKeysRegex
return nil
}
@@ -119,16 +134,38 @@ func (c *ConfigDynamic) validate() error {
errs = append(errs, "invalid configuration: GroupVersionResource.Resource cannot be empty")
}
- for i, selectorString := range c.FieldSelectors {
- if selectorString == "" {
+ for i, fieldSelectorString := range c.FieldSelectors {
+ if fieldSelectorString == "" {
errs = append(errs, fmt.Sprintf("invalid field selector %d: must not be empty", i))
}
- _, err := fields.ParseSelector(selectorString)
+ _, err := fields.ParseSelector(fieldSelectorString)
if err != nil {
errs = append(errs, fmt.Sprintf("invalid field selector %d: %s", i, err))
}
}
+ for i, labelSelectorString := range c.LabelSelectors {
+ if labelSelectorString == "" {
+ errs = append(errs, fmt.Sprintf("invalid label selector %d: must not be empty", i))
+ }
+ _, err := labels.Parse(labelSelectorString)
+ if err != nil {
+ errs = append(errs, fmt.Sprintf("invalid label selector %d: %s", i, err))
+ }
+ }
+
+ for i, r := range c.ExcludeAnnotationKeysRegex {
+ if _, err := regexp.Compile(r); err != nil {
+ errs = append(errs, fmt.Sprintf("invalid excludeAnnotationKeysRegex[%d]: %s", i, err))
+ }
+ }
+
+ for i, r := range c.ExcludeLabelKeysRegex {
+ if _, err := regexp.Compile(r); err != nil {
+ errs = append(errs, fmt.Sprintf("invalid excludeLabelKeysRegex[%d]: %s", i, err))
+ }
+ }
+
if len(errs) > 0 {
return errors.New(strings.Join(errs, ", "))
}
@@ -151,6 +188,9 @@ var kubernetesNativeResources = map[schema.GroupVersionResource]sharedInformerFu
corev1.SchemeGroupVersion.WithResource("services"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer {
return sharedFactory.Core().V1().Services().Informer()
},
+ corev1.SchemeGroupVersion.WithResource("configmaps"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer {
+ return sharedFactory.Core().V1().ConfigMaps().Informer()
+ },
appsv1.SchemeGroupVersion.WithResource("deployments"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer {
return sharedFactory.Apps().V1().Deployments().Informer()
},
@@ -207,8 +247,22 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
// Add any custom field selectors to the excluded namespaces selector
// The selectors have already been validated, so it is safe to use
// ParseSelectorOrDie here.
- for _, selectorString := range c.FieldSelectors {
- fieldSelector = fields.AndSelectors(fieldSelector, fields.ParseSelectorOrDie(selectorString))
+ for _, fieldSelectorString := range c.FieldSelectors {
+ fieldSelector = fields.AndSelectors(fieldSelector, fields.ParseSelectorOrDie(fieldSelectorString))
+ }
+
+ // Add any custom label selectors
+ // The selectors have already been validated, so Parse is expected to
+ // succeed; any parse error is treated as a programming error.
+ labelSelector := labels.Everything()
+ for _, labelSelectorString := range c.LabelSelectors {
+ selector, err := labels.Parse(labelSelectorString)
+ if err != nil {
+ panic(fmt.Sprintf("PROGRAMMING ERROR: should have been caught in validation: "+
+ "failed to parse validated label selector %q: %v", labelSelectorString, err))
+ }
+ reqs, _ := selector.Requirements()
+ labelSelector = labelSelector.Add(reqs...)
}
// init cache to store gathered resources
@@ -217,6 +271,7 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
newDataGatherer := &DataGathererDynamic{
groupVersionResource: c.GroupVersionResource,
fieldSelector: fieldSelector.String(),
+ labelSelector: labelSelector.String(),
namespaces: c.IncludeNamespaces,
cache: dgCache,
}
@@ -237,6 +292,7 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
informers.WithNamespace(metav1.NamespaceAll),
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.FieldSelector = fieldSelector.String()
+ options.LabelSelector = labelSelector.String()
}),
)
newDataGatherer.informer = informerFunc(factory)
@@ -249,6 +305,7 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
metav1.NamespaceAll,
func(options *metav1.ListOptions) {
options.FieldSelector = fieldSelector.String()
+ options.LabelSelector = labelSelector.String()
},
)
newDataGatherer.informer = factory.ForResource(c.GroupVersionResource).Informer()
@@ -272,6 +329,21 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
}
newDataGatherer.registration = registration
+ for _, r := range c.ExcludeAnnotationKeysRegex {
+ compiled, err := regexp.Compile(r)
+ if err != nil {
+ return nil, fmt.Errorf("invalid excludeAnnotationKeysRegex %q: %w", r, err)
+ }
+ newDataGatherer.ExcludeAnnotKeys = append(newDataGatherer.ExcludeAnnotKeys, compiled)
+ }
+ for _, r := range c.ExcludeLabelKeysRegex {
+ compiled, err := regexp.Compile(r)
+ if err != nil {
+ return nil, fmt.Errorf("invalid excludeLabelKeysRegex %q: %w", r, err)
+ }
+ newDataGatherer.ExcludeLabelKeys = append(newDataGatherer.ExcludeLabelKeys, compiled)
+ }
+
return newDataGatherer, nil
}
@@ -293,6 +365,9 @@ type DataGathererDynamic struct {
// returned by the Kubernetes API.
// https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/
fieldSelector string
+ // labelSelector is a label selector string used to filter resources
+ // returned by the Kubernetes API.
+ labelSelector string
// cache holds all resources watched by the data gatherer, default object expiry time 5 minutes
// 30 seconds purge time https://pkg.go.dev/github.com/patrickmn/go-cache
cache *cache.Cache
@@ -302,6 +377,18 @@ type DataGathererDynamic struct {
ExcludeAnnotKeys []*regexp.Regexp
ExcludeLabelKeys []*regexp.Regexp
+
+ // Encryptor, if non-nil, will be used to envelope encrypt Secret data.
+ // If nil, Secret data will be redacted.
+ Encryptor envelope.Encryptor
+
+ // IncludeLastModifiedTime, if true, extracts the most recent time from
+ // metadata.managedFields and includes it as _lastModifiedTime on Secrets.
+ IncludeLastModifiedTime bool
+}
+
+func (g *DataGathererDynamic) GVR() schema.GroupVersionResource {
+ return g.groupVersionResource
}
// Run starts the dynamic data gatherer's informers for resource collection.
@@ -347,7 +434,7 @@ func (g *DataGathererDynamic) WaitForCacheSync(ctx context.Context) error {
// Fetch will fetch the requested data from the apiserver, or return an error
// if fetching the data fails.
-func (g *DataGathererDynamic) Fetch() (any, int, error) {
+func (g *DataGathererDynamic) Fetch(ctx context.Context) (any, int, error) {
if g.groupVersionResource.String() == "" {
return nil, -1, fmt.Errorf("resource type must be specified")
}
@@ -375,8 +462,10 @@ func (g *DataGathererDynamic) Fetch() (any, int, error) {
return nil, -1, fmt.Errorf("failed to parse cached resource")
}
- // Redact Secret data
- err := redactList(items, g.ExcludeAnnotKeys, g.ExcludeLabelKeys)
+ items = g.excludeResources(items)
+
+ // Redact Secret data (which may include encrypting it if enabled)
+ err := g.redactList(ctx, items)
if err != nil {
return nil, -1, err
}
@@ -386,6 +475,42 @@ func (g *DataGathererDynamic) Fetch() (any, int, error) {
}, len(items), nil
}
+// excludeResources drops any resource whose annotation or label keys match the
+// configured exclusion patterns. This is distinct from redactList, which strips
+// matching keys from kept resources.
+//
+// Note: the work done here scales with the number of resources, annotations,
+// labels, and exclusion rules, as well as the complexity of each regex.
+// A user configuring many complex patterns against a large cluster may see
+// a meaningful CPU cost.
+func (g *DataGathererDynamic) excludeResources(list []*api.GatheredResource) []*api.GatheredResource {
+ if len(g.ExcludeAnnotKeys) == 0 && len(g.ExcludeLabelKeys) == 0 {
+ return list
+ }
+ return slices.DeleteFunc(list, g.resourceMatchesExclusionKeys)
+}
+
+func (g *DataGathererDynamic) resourceMatchesExclusionKeys(item *api.GatheredResource) bool {
+ res, ok := item.Resource.(*unstructured.Unstructured)
+ if !ok {
+ return false
+ }
+
+ return anyKeyMatches(res.GetAnnotations(), g.ExcludeAnnotKeys) ||
+ anyKeyMatches(res.GetLabels(), g.ExcludeLabelKeys)
+}
+
+func anyKeyMatches(m map[string]string, patterns []*regexp.Regexp) bool {
+ for key := range m {
+ for _, p := range patterns {
+ if p.MatchString(key) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
// redactList removes sensitive and superfluous data from the supplied resource list.
// All resources have superfluous managed-data fields removed.
// All resources have sensitive labels and annotations removed.
@@ -393,12 +518,20 @@ func (g *DataGathererDynamic) Fetch() (any, int, error) {
// resources there is an allow-list of fields that should be retained.
// For Secret resources, the `data` is redacted, to prevent private keys or sensitive
// data being collected; only the tls.crt and ca.crt data keys are retained.
+// However, if keepSecretData is true (i.e., encryption is enabled), secret data is NOT redacted
+// so it can be encrypted later in the upload pipeline.
// For Route resources, only the fields related to CA certificate and policy are retained.
// TODO(wallrj): A short coming of the current allow-list implementation is that
// you have to specify absolute fields paths. It is not currently possible to
// select all metadata with: `{metadata}`. This means that the metadata for
// Secret and Route has fewer fields than the metadata for all other resources.
-func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys []*regexp.Regexp) error {
+func (g *DataGathererDynamic) redactList(ctx context.Context, list []*api.GatheredResource) error {
+ secretSelectedFields := slices.Clone(SecretSelectedFields)
+
+ if g.Encryptor != nil {
+ secretSelectedFields = append(secretSelectedFields, FieldPath{"_encryptedData"})
+ }
+
for i := range list {
if item, ok := list[i].Resource.(*unstructured.Unstructured); ok {
// Determine the kind of items in case this is a generic 'mixed' list.
@@ -413,12 +546,29 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
for _, gvk := range gvks {
// secret object
if gvk.Kind == "Secret" && (gvk.Group == "core" || gvk.Group == "") {
- if err := Select(SecretSelectedFields, resource); err != nil {
- return err
+ // Note: We must redact data field in all cases!
+ // If encryption is enabled, we encrypt the data and preserve it, but we still need to redact later.
+ // If encryption is enabled and _fails_, we MUST still redact the data field to avoid leaking sensitive information.
+ if g.Encryptor != nil {
+ err := g.encryptDataField(ctx, resource)
+ if err != nil {
+ // WARNING: We CAN NOT return an error here, as that would leak the secret data
+ log := klog.FromContext(ctx).WithName("encryptDataField")
+ log.Error(err, "failed to encrypt secret data field; no encrypted secret data will be sent for object", "secretName", resource.GetName())
+
+ }
}
- // route object
+ if g.IncludeLastModifiedTime {
+ setLastModifiedTime(resource)
+ }
+
+ // Redact to only selected fields
+ if err := Select(secretSelectedFields, resource); err != nil {
+ return err
+ }
} else if gvk.Kind == "Route" && gvk.Group == "route.openshift.io" {
+ // route object
if err := Select(RouteSelectedFields, resource); err != nil {
return err
}
@@ -428,8 +578,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
// remove managedFields from all resources
Redact(RedactFields, resource)
- RemoveUnstructuredKeys(excludeAnnotKeys, resource, "metadata", "annotations")
- RemoveUnstructuredKeys(excludeLabelKeys, resource, "metadata", "labels")
+ RemoveUnstructuredKeys(g.ExcludeAnnotKeys, resource, "metadata", "annotations")
+ RemoveUnstructuredKeys(g.ExcludeLabelKeys, resource, "metadata", "labels")
continue
}
@@ -443,8 +593,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
item.GetObjectMeta().SetManagedFields(nil)
delete(item.GetObjectMeta().GetAnnotations(), "kubectl.kubernetes.io/last-applied-configuration")
- RemoveTypedKeys(excludeAnnotKeys, item.GetObjectMeta().GetAnnotations())
- RemoveTypedKeys(excludeLabelKeys, item.GetObjectMeta().GetLabels())
+ RemoveTypedKeys(g.ExcludeAnnotKeys, item.GetObjectMeta().GetAnnotations())
+ RemoveTypedKeys(g.ExcludeLabelKeys, item.GetObjectMeta().GetLabels())
resource := item.(runtime.Object)
gvks, _, err := scheme.Scheme.ObjectKinds(resource)
@@ -471,6 +621,91 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
return nil
}
+const encryptedDataFieldName = "_encryptedData"
+
+var encryptedDataField = FieldPath{encryptedDataFieldName}
+
+const lastModifiedTimeFieldName = "_lastModifiedTime"
+
+// setLastModifiedTime extracts the most recent time from metadata.managedFields
+// and sets it as a top-level synthetic field on the resource.
+// This must be called before Select(), which removes managedFields.
+func setLastModifiedTime(resource *unstructured.Unstructured) {
+ managedFieldsRaw, found, err := unstructured.NestedSlice(resource.Object, "metadata", "managedFields")
+ if err != nil || !found || len(managedFieldsRaw) == 0 {
+ return
+ }
+
+ var latestTime time.Time
+ var latestTimeStr string
+ for _, entry := range managedFieldsRaw {
+ entryMap, ok := entry.(map[string]any)
+ if !ok {
+ continue
+ }
+ timeVal, ok := entryMap["time"].(string)
+ if !ok || timeVal == "" {
+ continue
+ }
+ parsed, err := time.Parse(time.RFC3339, timeVal)
+ if err != nil {
+ continue
+ }
+ if parsed.After(latestTime) {
+ latestTime = parsed
+ latestTimeStr = timeVal
+ }
+ }
+
+ if latestTimeStr == "" {
+ return
+ }
+
+ _ = unstructured.SetNestedField(resource.Object, latestTimeStr, lastModifiedTimeFieldName)
+}
+
+// encryptDataField encrypts the `data` field of the given secret and stores the encrypted data
+// in a new field with the name of [encryptedDataFieldName]. The original `data` field is left unchanged, on the
+// assumption that it will be redacted after the encryption step.
+// This function does not check that the given resource is actually a Secret; that is the caller's responsibility.
+func (g *DataGathererDynamic) encryptDataField(ctx context.Context, secret *unstructured.Unstructured) error {
+ if g.Encryptor == nil {
+ return nil
+ }
+
+ plaintextDataRaw, found, err := unstructured.NestedFieldNoCopy(secret.Object, "data")
+ if err != nil {
+ return fmt.Errorf("error retrieving secret data field during redaction for encryption: %w", err)
+ }
+
+ if !found {
+ return fmt.Errorf("no data field found on secret")
+ }
+
+ plaintextDataTyped, ok := plaintextDataRaw.(map[string]any)
+ if !ok {
+ return fmt.Errorf("secret data field is not of expected map type for encryption")
+ }
+
+ // we want to encrypt the JSON representation of the data field
+ plaintextData, err := json.Marshal(plaintextDataTyped)
+ if err != nil {
+ return fmt.Errorf("failed to marshal secret data field for encryption: %w", err)
+ }
+
+ encryptedData, err := g.Encryptor.Encrypt(ctx, plaintextData)
+ if err != nil {
+ return fmt.Errorf("failed to encrypt secret data during redaction: %w", err)
+ }
+
+ err = unstructured.SetNestedField(secret.Object, encryptedData.ToMap(), encryptedDataField...)
+ if err != nil {
+ return fmt.Errorf("failed to set %s field on secret resource during redaction: %w", encryptedDataFieldName, err)
+ }
+
+ return nil
+}
+
// Meant for typed clientset objects.
func RemoveTypedKeys(excludeAnnotKeys []*regexp.Regexp, m map[string]string) {
for key := range m {
diff --git a/pkg/datagatherer/k8sdynamic/dynamic_test.go b/pkg/datagatherer/k8sdynamic/dynamic_test.go
index 5745f254..e724730f 100644
--- a/pkg/datagatherer/k8sdynamic/dynamic_test.go
+++ b/pkg/datagatherer/k8sdynamic/dynamic_test.go
@@ -1,16 +1,22 @@
package k8sdynamic
import (
+ "context"
+ "crypto/rand"
+ stdrsa "crypto/rsa"
+ "encoding/base64"
"encoding/json"
"fmt"
"reflect"
"regexp"
- "sort"
+ "slices"
"strings"
"sync"
"testing"
"time"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
@@ -26,6 +32,9 @@ import (
k8scache "k8s.io/client-go/tools/cache"
"github.com/jetstack/preflight/api"
+ "github.com/jetstack/preflight/internal/envelope"
+ "github.com/jetstack/preflight/internal/envelope/keyfetch"
+ "github.com/jetstack/preflight/internal/envelope/rsa"
)
func getObject(version, kind, name, namespace string, withManagedFields bool) *unstructured.Unstructured {
@@ -91,32 +100,36 @@ func getSecret(name, namespace string, data map[string]any, isTLS bool, withLast
return object
}
-func sortGatheredResources(list []*api.GatheredResource) {
- if len(list) > 1 {
- sort.SliceStable(list, func(i, j int) bool {
- var itemA, itemB string
- // unstructured
- if item, ok := list[i].Resource.(*unstructured.Unstructured); ok {
- itemA = item.GetName()
- }
- if item, ok := list[j].Resource.(*unstructured.Unstructured); ok {
- itemB = item.GetName()
- }
+func sortResourcesByName(list []*unstructured.Unstructured) {
+ slices.SortStableFunc(list, func(a, b *unstructured.Unstructured) int {
+ return strings.Compare(a.GetName(), b.GetName())
+ })
+}
- // pods
- if item, ok := list[i].Resource.(*corev1.Pod); ok {
- itemA = item.GetName()
- }
- if item, ok := list[j].Resource.(*corev1.Pod); ok {
- itemB = item.GetName()
- }
- return itemA < itemB
- })
+func sortGatheredResources(list []*api.GatheredResource) {
+ type namer interface {
+ GetName() string
}
+
+ slices.SortStableFunc(list, func(a, b *api.GatheredResource) int {
+ aNamer, ok := a.Resource.(namer)
+ if !ok {
+ panic("got unexpected resource type")
+ }
+
+ bNamer, ok := b.Resource.(namer)
+ if !ok {
+ panic("got unexpected resource type")
+ }
+
+ return strings.Compare(aNamer.GetName(), bNamer.GetName())
+ })
+
}
func TestNewDataGathererWithClientAndDynamicInformer(t *testing.T) {
ctx := t.Context()
+
config := ConfigDynamic{
ExcludeNamespaces: []string{"kube-system"},
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
@@ -124,6 +137,10 @@ func TestNewDataGathererWithClientAndDynamicInformer(t *testing.T) {
"type!=kubernetes.io/service-account-token",
"type!=kubernetes.io/dockercfg",
},
+ LabelSelectors: []string{
+ "conjur.org/name=conjur-connect-configmap",
+ "app=my-app",
+ },
}
cl := fake.NewSimpleDynamicClient(runtime.NewScheme())
dg, err := config.newDataGathererWithClient(ctx, cl, nil)
@@ -138,6 +155,7 @@ func TestNewDataGathererWithClientAndDynamicInformer(t *testing.T) {
// during initialization
namespaces: config.IncludeNamespaces,
fieldSelector: "metadata.namespace!=kube-system,type!=kubernetes.io/service-account-token,type!=kubernetes.io/dockercfg",
+ labelSelector: "app=my-app,conjur.org/name=conjur-connect-configmap",
}
gatherer := dg.(*DataGathererDynamic)
@@ -160,6 +178,9 @@ func TestNewDataGathererWithClientAndDynamicInformer(t *testing.T) {
if !reflect.DeepEqual(gatherer.fieldSelector, expected.fieldSelector) {
t.Errorf("expected %v, got %v", expected.fieldSelector, gatherer.fieldSelector)
}
+ if !reflect.DeepEqual(gatherer.labelSelector, expected.labelSelector) {
+ t.Errorf("expected %v, got %v", expected.labelSelector, gatherer.labelSelector)
+ }
}
func TestNewDataGathererWithClientAndSharedIndexInformer(t *testing.T) {
@@ -167,6 +188,10 @@ func TestNewDataGathererWithClientAndSharedIndexInformer(t *testing.T) {
config := ConfigDynamic{
IncludeNamespaces: []string{"a"},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
+ LabelSelectors: []string{
+ "app=my-app",
+ "version=v1",
+ },
}
clientset := fakeclientset.NewSimpleClientset()
dg, err := config.newDataGathererWithClient(ctx, nil, clientset)
@@ -178,7 +203,8 @@ func TestNewDataGathererWithClientAndSharedIndexInformer(t *testing.T) {
groupVersionResource: config.GroupVersionResource,
// it's important that the namespaces are set as the IncludeNamespaces
// during initialization
- namespaces: config.IncludeNamespaces,
+ namespaces: config.IncludeNamespaces,
+ labelSelector: "app=my-app,version=v1",
}
gatherer := dg.(*DataGathererDynamic)
@@ -198,6 +224,9 @@ func TestNewDataGathererWithClientAndSharedIndexInformer(t *testing.T) {
if gatherer.registration == nil {
t.Errorf("unexpected event handler registration value: %v", nil)
}
+ if !reflect.DeepEqual(gatherer.labelSelector, expected.labelSelector) {
+ t.Errorf("expected %v, got %v", expected.labelSelector, gatherer.labelSelector)
+ }
}
func TestUnmarshalDynamicConfig(t *testing.T) {
@@ -217,6 +246,9 @@ include-namespaces:
- default
field-selectors:
- type!=kubernetes.io/service-account-token
+label-selectors:
+- conjur.org/name=conjur-connect-configmap
+- app=my-app
`
expectedGVR := schema.GroupVersionResource{
@@ -236,6 +268,11 @@ field-selectors:
"type!=kubernetes.io/service-account-token",
}
+ expectedLabelSelectors := []string{
+ "conjur.org/name=conjur-connect-configmap",
+ "app=my-app",
+ }
+
cfg := ConfigDynamic{}
err := yaml.Unmarshal([]byte(textCfg), &cfg)
if err != nil {
@@ -259,6 +296,37 @@ field-selectors:
if got, want := cfg.FieldSelectors, expectedFieldSelectors; !reflect.DeepEqual(got, want) {
t.Errorf("FieldSelectors does not match: got=%+v want=%+v", got, want)
}
+ if got, want := cfg.LabelSelectors, expectedLabelSelectors; !reflect.DeepEqual(got, want) {
+ t.Errorf("LabelSelectors does not match: got=%+v want=%+v", got, want)
+ }
+}
+func TestUnmarshalDynamicConfig_ExclusionRegex(t *testing.T) {
+ // Verify that the per-gatherer excludeAnnotationKeysRegex and
+ // excludeLabelKeysRegex fields are parsed from YAML.
+ textCfg := `
+resource-type:
+ version: v1
+ resource: secrets
+excludeAnnotationKeysRegex:
+ - '^openshift\.io.*$'
+ - '^kapp\.k14s\.io/.*$'
+excludeLabelKeysRegex:
+ - '^company\.com/employee-id$'
+`
+ cfg := ConfigDynamic{}
+ if err := yaml.Unmarshal([]byte(textCfg), &cfg); err != nil {
+ t.Fatalf("unexpected error: %+v", err)
+ }
+
+ expectedAnnot := []string{`^openshift\.io.*$`, `^kapp\.k14s\.io/.*$`}
+ expectedLabel := []string{`^company\.com/employee-id$`}
+
+ if got, expected := cfg.ExcludeAnnotationKeysRegex, expectedAnnot; !reflect.DeepEqual(got, expected) {
+ t.Errorf("ExcludeAnnotationKeysRegex: got=%v want=%v", got, expected)
+ }
+ if got, expected := cfg.ExcludeLabelKeysRegex, expectedLabel; !reflect.DeepEqual(got, expected) {
+ t.Errorf("ExcludeLabelKeysRegex: got=%v want=%v", got, expected)
+ }
}
func TestConfigDynamicValidate(t *testing.T) {
@@ -305,6 +373,20 @@ func TestConfigDynamicValidate(t *testing.T) {
},
ExpectedError: "invalid field selector 0: invalid selector: 'foo'; can't understand 'foo'",
},
+ {
+ Config: ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
+ ExcludeAnnotationKeysRegex: []string{`^[0-9$`},
+ },
+ ExpectedError: "invalid excludeAnnotationKeysRegex[0]",
+ },
+ {
+ Config: ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
+ ExcludeLabelKeysRegex: []string{`^[0-9$`},
+ },
+ ExpectedError: "invalid excludeLabelKeysRegex[0]",
+ },
}
for _, test := range tests {
@@ -365,7 +447,24 @@ func init() {
clock = &fakeTime{}
}
+type failEncryptor struct{}
+
+func (fe *failEncryptor) Encrypt(_ context.Context, plaintext []byte) (*envelope.EncryptedData, error) {
+ return nil, fmt.Errorf("encryption failed")
+}
+
func TestDynamicGatherer_Fetch(t *testing.T) {
+ privKey, err := stdrsa.GenerateKey(rand.Reader, 2048)
+ require.NoError(t, err)
+
+ keyID := "test-key-id"
+
+ fetcher := keyfetch.NewFakeClientWithKey(keyID, privKey.Public().(*stdrsa.PublicKey))
+ encryptor, err := rsa.NewEncryptor(fetcher)
+ if err != nil {
+ t.Fatalf("failed to create encryptor: %v", err)
+ }
+
// start a k8s client
// init the datagatherer's informer with the client
// add/delete resources watched by the data gatherer
@@ -374,14 +473,18 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
config ConfigDynamic
excludeAnnotsKeys []string
excludeLabelKeys []string
- addObjects []runtime.Object
+ addObjects []*unstructured.Unstructured
deleteObjects map[string]string
updateObjects map[string]runtime.Object
expected []*api.GatheredResource
- err bool
+
+ encryptor envelope.Encryptor
+ expectEncryptionFailure bool
+
+ err bool
}{
"fetches the default namespace": {
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("v1", "Namespace", "default", "", false),
},
config: ConfigDynamic{
@@ -404,7 +507,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
},
},
"only a Foo should be returned if GVR selects foos": {
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo", "testns", false),
getObject("v1", "Service", "testservice", "testns", false),
getObject("foobar/v1", "NotFoo", "notfoo", "testns", false),
@@ -420,7 +523,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
},
},
"delete a Foo resource from the testns, the cache should have a Foo with deletedAt set to now()": {
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo", "testns", false),
getObject("v1", "Service", "testservice", "testns", false),
getObject("foobar/v1", "NotFoo", "notfoo", "testns", false),
@@ -444,7 +547,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
IncludeNamespaces: []string{"testns"},
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo", "testns", false),
getObject("foobar/v1", "Foo", "testfoo", "nottestns", false),
},
@@ -459,7 +562,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
IncludeNamespaces: []string{""},
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo1", "testns1", false),
getObject("foobar/v1", "Foo", "testfoo2", "testns2", false),
},
@@ -477,7 +580,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
IncludeNamespaces: []string{""},
GroupVersionResource: schema.GroupVersionResource{Group: "foobar", Version: "v1", Resource: "foos"},
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo1", "testns1", false),
getObject("foobar/v1", "Foo", "testfoo2", "testns2", false),
},
@@ -499,7 +602,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
"testns1": "testfoo1",
"testns2": "testfoo2",
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo1", "testns1", false),
getObject("foobar/v1", "Foo", "testfoo2", "testns2", false),
},
@@ -523,7 +626,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
"testns1": getObject("foobar/v1", "Foo", "testfoo1", "testns1", false),
"testns2": getObject("foobar/v1", "Foo", "testfoo2", "testns2", false),
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getObject("foobar/v1", "Foo", "testfoo1", "testns1", false),
getObject("foobar/v1", "Foo", "testfoo2", "testns2", false),
},
@@ -541,7 +644,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
IncludeNamespaces: []string{""},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getSecret("testsecret", "testns1", map[string]any{
"secretKey": "secretValue",
}, false, true),
@@ -563,7 +666,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
IncludeNamespaces: []string{""},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
},
- addObjects: []runtime.Object{
+ addObjects: []*unstructured.Unstructured{
getSecret("testsecret", "testns1", map[string]any{
"tls.key": "secretValue",
"tls.crt": "value",
@@ -588,6 +691,76 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
},
},
},
+ "Secret resources should have encrypted data when encryption is enabled": {
+ config: ConfigDynamic{
+ IncludeNamespaces: []string{""},
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ },
+ addObjects: []*unstructured.Unstructured{
+ getSecret("testsecret", "testns1", map[string]any{
+ "secretKey": "secretValue",
+ }, false, true),
+ getSecret("anothertestsecret", "testns2", map[string]any{
+ "secretNumber": "12345",
+ }, false, true),
+ },
+ encryptor: encryptor,
+ expected: []*api.GatheredResource{
+ {
+ Resource: getSecret("testsecret", "testns1", nil, false, false),
+ },
+ {
+ Resource: getSecret("anothertestsecret", "testns2", nil, false, false),
+ },
+ },
+ },
+ "Secret resources should have encrypted data when encryption is enabled with some data fields preserved": {
+ config: ConfigDynamic{
+ IncludeNamespaces: []string{""},
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ },
+ addObjects: []*unstructured.Unstructured{
+ getSecret("testsecret-notpreserved", "testns1", map[string]any{
+ "secretKey": "secretValue",
+ }, false, true),
+ getSecret("testsecret-preserved", "testns1", map[string]any{
+ "tls.key": "secretValue",
+ "tls.crt": "value",
+ "ca.crt": "value",
+ }, true, true),
+ },
+ encryptor: encryptor,
+ expected: []*api.GatheredResource{
+ {
+ // only tls.crt and ca.cert remain, although tls.key will be present in encrypted data
+ Resource: getSecret("testsecret-preserved", "testns1", map[string]any{
+ "tls.crt": "value",
+ "ca.crt": "value",
+ }, true, false),
+ },
+ {
+ Resource: getSecret("testsecret-notpreserved", "testns1", nil, false, false),
+ },
+ },
+ },
+ "Secret resources should still be redacted if encryption fails": {
+ config: ConfigDynamic{
+ IncludeNamespaces: []string{""},
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ },
+ addObjects: []*unstructured.Unstructured{
+ getSecret("testsecret", "testns1", map[string]any{
+ "secretKey": "secretValue",
+ }, false, true),
+ },
+ encryptor: &failEncryptor{},
+ expectEncryptionFailure: true,
+ expected: []*api.GatheredResource{
+ {
+ Resource: getSecret("testsecret", "testns1", nil, false, false),
+ },
+ },
+ },
"excluded annotations are removed for unstructured-based gatherers such as secrets": {
config: ConfigDynamic{GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}},
@@ -623,7 +796,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
// The regular expression would then be:
excludeLabelKeys: []string{`^company\.com/employee-id$`},
- addObjects: []runtime.Object{getObjectAnnot("v1", "Secret", "s0", "n1",
+ addObjects: []*unstructured.Unstructured{getObjectAnnot("v1", "Secret", "s0", "n1",
map[string]any{"kapp.k14s.io/original": "foo", "kapp.k14s.io/original-diff": "bar", "normal": "true"},
map[string]any{`company.com/employee-id`: "12345", "prod": "true"},
)},
@@ -632,19 +805,68 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
map[string]any{"prod": "true"},
)}},
},
+ "per-gatherer excludeAnnotationKeysRegex excludes matching resources entirely": {
+ // Resources annotated with openshift.io/* should not appear in the
+ // output at all, not just have those keys stripped.
+ config: ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ ExcludeAnnotationKeysRegex: []string{`^openshift\.io.*$`},
+ },
+ addObjects: []*unstructured.Unstructured{
+ getObjectAnnot("v1", "Secret", "excluded", "ns",
+ map[string]any{"openshift.io/discovery": "ignore", "other": "kept"},
+ map[string]any{},
+ ),
+ getObjectAnnot("v1", "Secret", "included", "ns",
+ map[string]any{"other": "kept"},
+ map[string]any{},
+ ),
+ },
+ expected: []*api.GatheredResource{{Resource: getObjectAnnot("v1", "Secret", "included", "ns",
+ map[string]any{"other": "kept"},
+ map[string]any{},
+ )}},
+ },
+ "per-gatherer excludeLabelKeysRegex excludes matching resources entirely": {
+ config: ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ ExcludeLabelKeysRegex: []string{`^discovery\.venafi\.com/exclude$`},
+ },
+ addObjects: []*unstructured.Unstructured{
+ getObjectAnnot("v1", "Secret", "excluded", "ns",
+ map[string]any{},
+ map[string]any{"discovery.venafi.com/exclude": "true", "other": "kept"},
+ ),
+ getObjectAnnot("v1", "Secret", "included", "ns",
+ map[string]any{},
+ map[string]any{"other": "kept"},
+ ),
+ },
+ expected: []*api.GatheredResource{{Resource: getObjectAnnot("v1", "Secret", "included", "ns",
+ map[string]any{},
+ map[string]any{"other": "kept"},
+ )}},
+ },
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var wg sync.WaitGroup
ctx := t.Context()
+
gvrToListKind := map[schema.GroupVersionResource]string{
{Group: "foobar", Version: "v1", Resource: "foos"}: "UnstructuredList",
{Group: "apps", Version: "v1", Resource: "deployments"}: "UnstructuredList",
{Group: "", Version: "v1", Resource: "secrets"}: "UnstructuredList",
{Group: "", Version: "v1", Resource: "namespaces"}: "UnstructuredList",
}
- cl := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, tc.addObjects...)
+
+ addObjs := make([]runtime.Object, len(tc.addObjects))
+ for i, obj := range tc.addObjects {
+ addObjs[i] = obj
+ }
+ cl := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, addObjs...)
+
// init the datagatherer's informer with the client
dg, err := tc.config.newDataGathererWithClient(ctx, cl, nil)
if err != nil {
@@ -681,6 +903,10 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
dgd.ExcludeLabelKeys = append(dgd.ExcludeLabelKeys, regexp.MustCompile(key))
}
+ if tc.encryptor != nil {
+ dgd.Encryptor = tc.encryptor
+ }
+
// start data gatherer informer
dynamiDg := dg
go func() {
@@ -721,7 +947,7 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
if waitTimeout(&wg, 30*time.Second) {
t.Fatalf("unexpected timeout")
}
- res, expectCount, err := dynamiDg.Fetch()
+ res, expectCount, err := dynamiDg.Fetch(ctx)
if err != nil && !tc.err {
t.Errorf("expected no error but got: %v", err)
}
@@ -741,13 +967,139 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
// sorting list of expected results by name
sortGatheredResources(tc.expected)
- assert.Equal(t, tc.expected, list)
+ // check lengths of lists first before we iterate to compare items
assert.Len(t, list, expectCount, "unexpected number of resources returned")
+
+ for i, item := range list {
+ got, ok := item.Resource.(*unstructured.Unstructured)
+ if !ok {
+ t.Errorf("expected resource to be of type unstructured.Unstructured but got %T", item.Resource)
+ }
+
+ expected, ok := tc.expected[i].Resource.(*unstructured.Unstructured)
+ if !ok {
+ t.Errorf("expected resource to be of type unstructured.Unstructured but got %T", tc.expected[i].Resource)
+ }
+
+ // If encryption is enabled, validate the encrypted data
+ if tc.encryptor != nil {
+ if tc.expectEncryptionFailure {
+ _, found, err := unstructured.NestedFieldNoCopy(got.Object, encryptedDataFieldName)
+ require.NoError(t, err, "error checking %s field", encryptedDataFieldName)
+ require.False(t, found, "expected %s field to not exist when encryption fails", encryptedDataFieldName)
+ } else {
+ sortResourcesByName(tc.addObjects)
+ compareEncryptedData(t, privKey, got, tc.addObjects[i])
+ }
+ }
+
+ assert.Equal(t, expected, got)
+ }
}
})
}
}
+func compareEncryptedData(t *testing.T, privKey *stdrsa.PrivateKey, got *unstructured.Unstructured, original *unstructured.Unstructured) {
+ t.Helper()
+
+ // Check that encrypted data field exists
+ encryptedDataRaw, found, err := unstructured.NestedFieldNoCopy(got.Object, encryptedDataFieldName)
+ require.NoError(t, err, "error retrieving %s field", encryptedDataFieldName)
+ require.True(t, found, "expected %s field to exist when encryption is enabled", encryptedDataFieldName)
+
+ // Convert to map and validate structure
+ encryptedDataMap, ok := encryptedDataRaw.(map[string]any)
+ require.True(t, ok, "expected %s to be a map[string]any", encryptedDataFieldName)
+
+ // Check type field
+ typeField, ok := encryptedDataMap["type"].(string)
+ require.True(t, ok, "expected type field to be a string")
+ assert.Equal(t, rsa.EncryptionType, typeField, "expected type to be %s", rsa.EncryptionType)
+
+ // Check data field exists and is valid
+ dataFieldRaw, ok := encryptedDataMap["data"]
+ require.True(t, ok, "expected data field to exist")
+
+ dataField, ok := dataFieldRaw.(string)
+ require.True(t, ok, "expected data field to be a JSON string")
+
+ jweBytes, err := base64.StdEncoding.DecodeString(dataField)
+ require.NoError(t, err, "data field should be valid base64 string")
+
+ require.NotEmpty(t, jweBytes, "expected data field to be non-empty")
+
+ // Verify JWE can be parsed
+ _, err = jwe.Parse(jweBytes)
+ require.NoError(t, err, "data should be a valid JWE")
+
+ plaintext, err := jwe.Decrypt(jweBytes, jwe.WithKey(jwa.RSA_OAEP_256(), privKey), jwe.WithContext(t.Context()))
+ require.NoError(t, err, "failed to decrypt JWE")
+
+ // Verify decrypted plaintext matches expected resource data
+ expectedData, found, err := unstructured.NestedMap(original.Object, "data")
+ require.True(t, found, "expected data field to exist in original resource")
+ require.NoError(t, err, "error retrieving data field from original resource")
+
+ var decryptedDataMap map[string]any
+ err = json.Unmarshal(plaintext, &decryptedDataMap)
+ require.NoError(t, err, "failed to unmarshal decrypted plaintext")
+
+ assert.Equal(t, expectedData, decryptedDataMap, "decrypted data does not match original data")
+
+ // Remove encrypted data so that simple comparison works for other fields
+ unstructured.RemoveNestedField(got.Object, encryptedDataFieldName)
+}
+
+// TestExcludeAnnotKeys_ExcludesResourcesFromUpload verifies that resources
+// whose annotation keys match ExcludeAnnotKeys are dropped entirely from
+// Fetch() results, not just have those keys stripped.
+func TestExcludeAnnotKeys_ExcludesResourcesFromUpload(t *testing.T) {
+ ctx := t.Context()
+
+ gvrToListKind := map[schema.GroupVersionResource]string{
+ {Group: "", Version: "v1", Resource: "secrets"}: "UnstructuredList",
+ }
+
+ // "excluded" has a matching annotation key; "included" does not.
+ excluded := getObjectAnnot("v1", "Secret", "excluded", "ns",
+ map[string]any{"openshift.io/discovery": "ignore"},
+ map[string]any{},
+ )
+ included := getObjectAnnot("v1", "Secret", "included", "ns",
+ map[string]any{"other": "kept"},
+ map[string]any{},
+ )
+
+ cl := fake.NewSimpleDynamicClientWithCustomListKinds(
+ runtime.NewScheme(), gvrToListKind, excluded, included,
+ )
+
+ cfg := ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
+ }
+ dg, err := cfg.newDataGathererWithClient(ctx, cl, nil)
+ require.NoError(t, err)
+
+ dgd := dg.(*DataGathererDynamic)
+ dgd.ExcludeAnnotKeys = []*regexp.Regexp{regexp.MustCompile(`^openshift\.io/.*$`)}
+
+ go func() { _ = dg.Run(ctx) }()
+ require.NoError(t, dgd.WaitForCacheSync(ctx))
+
+ res, count, err := dg.Fetch(ctx)
+ require.NoError(t, err)
+
+ data, ok := res.(*api.DynamicData)
+ require.True(t, ok)
+
+ assert.Equal(t, 1, count, "only the non-matching resource should be returned")
+ if assert.Len(t, data.Items, 1) {
+ got := data.Items[0].Resource.(*unstructured.Unstructured)
+ assert.Equal(t, "included", got.GetName(), "the resource with matching annotation key should be excluded")
+ }
+}
+
func TestDynamicGathererNativeResources_Fetch(t *testing.T) {
// start a k8s client
// init the datagatherer's informer with the client
@@ -1034,7 +1386,7 @@ func TestDynamicGathererNativeResources_Fetch(t *testing.T) {
if waitTimeout(&wg, 5*time.Second) {
t.Fatalf("unexpected timeout")
}
- rawRes, count, err := dynamiDg.Fetch()
+ rawRes, count, err := dynamiDg.Fetch(ctx)
if tc.err {
require.Error(t, err)
} else {
@@ -1264,3 +1616,346 @@ func toRegexps(keys []string) []*regexp.Regexp {
}
return regexps
}
+
+// TestValidate_LabelSelectors tests validation of label selectors
+func TestValidate_LabelSelectors(t *testing.T) {
+ tests := []struct {
+ name string
+ labelSelectors []string
+ expectError bool
+ errorContains string
+ }{
+ {
+ name: "valid simple label selector",
+ labelSelectors: []string{"app=myapp"},
+ expectError: false,
+ },
+ {
+ name: "valid label selector with dot notation",
+ labelSelectors: []string{"conjur.org/name=conjur-connect-configmap"},
+ expectError: false,
+ },
+ {
+ name: "valid negative label selector",
+ labelSelectors: []string{"app!=test"},
+ expectError: false,
+ },
+ {
+ name: "valid multiple label selectors",
+ labelSelectors: []string{"app=myapp", "environment=production"},
+ expectError: false,
+ },
+ {
+ name: "valid label existence check",
+ labelSelectors: []string{"app"},
+ expectError: false,
+ },
+ {
+ name: "valid label non-existence check",
+ labelSelectors: []string{"!app"},
+ expectError: false,
+ },
+ {
+ name: "valid set-based selector",
+ labelSelectors: []string{"environment in (production, staging)"},
+ expectError: false,
+ },
+ {
+ name: "valid negative set-based selector",
+ labelSelectors: []string{"environment notin (dev, test)"},
+ expectError: false,
+ },
+ {
+ name: "empty label selector",
+ labelSelectors: []string{""},
+ expectError: true,
+ errorContains: "must not be empty",
+ },
+ {
+ name: "invalid label selector syntax",
+ labelSelectors: []string{"invalid===syntax"},
+ expectError: true,
+ errorContains: "invalid label selector",
+ },
+ {
+ name: "multiple selectors with one invalid",
+ labelSelectors: []string{"app=valid", "invalid==="},
+ expectError: true,
+ errorContains: "invalid label selector 1",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ config := &ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{
+ Version: "v1",
+ Resource: "configmaps",
+ },
+ LabelSelectors: tt.labelSelectors,
+ }
+
+ err := config.validate()
+ if tt.expectError {
+ require.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(), tt.errorContains)
+ }
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+// TestValidate_FieldSelectors tests validation of field selectors.
+func TestValidate_FieldSelectors(t *testing.T) {
+ tests := []struct {
+ name string
+ fieldSelectors []string
+ expectError bool
+ errorContains string
+ }{
+ {
+ name: "valid field selector",
+ fieldSelectors: []string{"metadata.name=test"},
+ expectError: false,
+ },
+ {
+ name: "valid negative field selector",
+ fieldSelectors: []string{"type!=kubernetes.io/dockercfg"},
+ expectError: false,
+ },
+ {
+ name: "multiple valid field selectors",
+ fieldSelectors: []string{"metadata.namespace=default", "type!=Opaque"},
+ expectError: false,
+ },
+ {
+ name: "empty field selector",
+ fieldSelectors: []string{""},
+ expectError: true,
+ errorContains: "must not be empty",
+ },
+ {
+ name: "invalid field selector syntax",
+ fieldSelectors: []string{"invalid===field"},
+ expectError: true,
+ errorContains: "invalid field selector",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ config := &ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{
+ Version: "v1",
+ Resource: "secrets",
+ },
+ FieldSelectors: tt.fieldSelectors,
+ }
+
+ err := config.validate()
+ if tt.expectError {
+ require.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(), tt.errorContains)
+ }
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+// TestValidate_CombinedSelectors tests validation with both field and label selectors.
+func TestValidate_CombinedSelectors(t *testing.T) {
+ tests := []struct {
+ name string
+ fieldSelectors []string
+ labelSelectors []string
+ expectError bool
+ errorContains string
+ }{
+ {
+ name: "valid field and label selectors",
+ fieldSelectors: []string{"type!=kubernetes.io/dockercfg"},
+ labelSelectors: []string{"app=myapp"},
+ expectError: false,
+ },
+ {
+ name: "invalid field selector with valid label selector",
+ fieldSelectors: []string{"invalid==="},
+ labelSelectors: []string{"app=myapp"},
+ expectError: true,
+ errorContains: "invalid field selector",
+ },
+ {
+ name: "valid field selector with invalid label selector",
+ fieldSelectors: []string{"type!=Opaque"},
+ labelSelectors: []string{"invalid==="},
+ expectError: true,
+ errorContains: "invalid label selector",
+ },
+ {
+ name: "both selectors invalid",
+ fieldSelectors: []string{"bad===field"},
+ labelSelectors: []string{"bad===label"},
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ config := &ConfigDynamic{
+ GroupVersionResource: schema.GroupVersionResource{
+ Version: "v1",
+ Resource: "configmaps",
+ },
+ FieldSelectors: tt.fieldSelectors,
+ LabelSelectors: tt.labelSelectors,
+ }
+
+ err := config.validate()
+ if tt.expectError {
+ require.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(), tt.errorContains)
+ }
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestSetLastModifiedTime(t *testing.T) {
+ tests := []struct {
+ name string
+ resource *unstructured.Unstructured
+ expected string
+ }{
+ {
+ name: "picks latest time from multiple entries",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "my-secret",
+ "namespace": "default",
+ "managedFields": []any{
+ map[string]any{
+ "manager": "kubectl",
+ "operation": "Apply",
+ "time": "2025-01-10T10:00:00Z",
+ },
+ map[string]any{
+ "manager": "kubectl",
+ "operation": "Update",
+ "time": "2026-05-19T17:06:59Z",
+ },
+ map[string]any{
+ "manager": "kube-controller-manager",
+ "operation": "Update",
+ "time": "2025-06-01T12:00:00Z",
+ },
+ },
+ },
+ },
+ },
+ expected: "2026-05-19T17:06:59Z",
+ },
+ {
+ name: "single entry",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "my-secret",
+ "namespace": "default",
+ "managedFields": []any{
+ map[string]any{
+ "manager": "kubectl",
+ "operation": "Apply",
+ "time": "2025-03-15T08:30:00Z",
+ },
+ },
+ },
+ },
+ },
+ expected: "2025-03-15T08:30:00Z",
+ },
+ {
+ name: "no managedFields - field not set",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "my-secret",
+ "namespace": "default",
+ },
+ },
+ },
+ expected: "",
+ },
+ {
+ name: "empty managedFields - field not set",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "my-secret",
+ "namespace": "default",
+ "managedFields": []any{},
+ },
+ },
+ },
+ expected: "",
+ },
+ {
+ name: "entries without time field are skipped",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "my-secret",
+ "namespace": "default",
+ "managedFields": []any{
+ map[string]any{
+ "manager": "kubectl",
+ "operation": "Apply",
+ },
+ map[string]any{
+ "manager": "kubectl",
+ "operation": "Update",
+ "time": "2025-11-20T15:00:00Z",
+ },
+ },
+ },
+ },
+ },
+ expected: "2025-11-20T15:00:00Z",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ setLastModifiedTime(tt.resource)
+
+ val, found, err := unstructured.NestedString(tt.resource.Object, lastModifiedTimeFieldName)
+ require.NoError(t, err)
+
+ if tt.expected == "" {
+ assert.False(t, found, "expected _lastModifiedTime to not be set")
+ } else {
+ assert.True(t, found, "expected _lastModifiedTime to be set")
+ assert.Equal(t, tt.expected, val)
+ }
+ })
+ }
+}
diff --git a/pkg/datagatherer/k8sdynamic/fieldfilter.go b/pkg/datagatherer/k8sdynamic/fieldfilter.go
index 392c75fd..46e41118 100644
--- a/pkg/datagatherer/k8sdynamic/fieldfilter.go
+++ b/pkg/datagatherer/k8sdynamic/fieldfilter.go
@@ -30,6 +30,7 @@ var SecretSelectedFields = []FieldPath{
{"data", "tls.crt"},
{"data", "ca.crt"},
{"data", "conjur-map"},
+ {lastModifiedTimeFieldName},
}
// RouteSelectedFields is the list of fields sent from OpenShift Route objects to the
@@ -69,10 +70,16 @@ var RouteSelectedFields = []FieldPath{
{"status"},
}
-// RedactFields are removed from all objects
+// RedactFields are removed from all objects.
+// Includes known GitOps tool annotations that store a full copy of the original
+// manifest (including Secret .data) to prevent private key material leaking via
+// the annotation channel.
var RedactFields = []FieldPath{
{"metadata", "managedFields"},
{"metadata", "annotations", "kubectl.kubernetes.io/last-applied-configuration"},
+ {"metadata", "annotations", "kapp.k14s.io/original"},
+ {"metadata", "annotations", "objectset.rio.cattle.io/applied"},
+ {"metadata", "annotations", "banzaicloud.com/last-applied"},
}
type FieldPath []string
diff --git a/pkg/datagatherer/k8sdynamic/fieldfilter_test.go b/pkg/datagatherer/k8sdynamic/fieldfilter_test.go
index 097e735f..24cba144 100644
--- a/pkg/datagatherer/k8sdynamic/fieldfilter_test.go
+++ b/pkg/datagatherer/k8sdynamic/fieldfilter_test.go
@@ -33,7 +33,8 @@ func TestSelect(t *testing.T) {
"finalizers": []string{"example.com/fake-finalizer"},
"generation": 11,
},
- "type": "kubernetes.io/tls",
+ "type": "kubernetes.io/tls",
+ "_lastModifiedTime": "2025-08-15T00:00:01Z",
"data": map[string]any{
"tls.crt": "cert data",
"tls.key": "secret",
@@ -60,7 +61,8 @@ func TestSelect(t *testing.T) {
"creationTimestamp": "2025-08-15T00:00:01Z",
"deletionTimestamp": "2025-08-15T00:00:02Z",
},
- "type": "kubernetes.io/tls",
+ "type": "kubernetes.io/tls",
+ "_lastModifiedTime": "2025-08-15T00:00:01Z",
"data": map[string]any{
// The "tls.key" is ignored.
"tls.crt": "cert data",
@@ -329,6 +331,35 @@ func TestRedactPod(t *testing.T) {
assert.Equal(t, expectedJSON, string(bytes))
}
+func TestRedactGitOpsAnnotations(t *testing.T) {
+ resource := &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "example",
+ "namespace": "example",
+ "annotations": map[string]any{
+ "kapp.k14s.io/original": `{"data":{"tls.key":"c2VjcmV0"}}`,
+ "objectset.rio.cattle.io/applied": `{"data":{"tls.key":"c2VjcmV0"}}`,
+ "banzaicloud.com/last-applied": `{"data":{"tls.key":"c2VjcmV0"}}`,
+ "kubectl.kubernetes.io/last-applied-configuration": `{"data":{"tls.key":"c2VjcmV0"}}`,
+ "safe-annotation": "keep-me",
+ },
+ },
+ },
+ }
+
+ Redact(RedactFields, resource)
+
+ annotations := resource.GetAnnotations()
+ assert.NotContains(t, annotations, "kapp.k14s.io/original")
+ assert.NotContains(t, annotations, "objectset.rio.cattle.io/applied")
+ assert.NotContains(t, annotations, "banzaicloud.com/last-applied")
+ assert.NotContains(t, annotations, "kubectl.kubernetes.io/last-applied-configuration")
+ assert.Equal(t, "keep-me", annotations["safe-annotation"])
+}
+
func TestRedactMissingField(t *testing.T) {
resource := &unstructured.Unstructured{
Object: map[string]any{
diff --git a/pkg/datagatherer/local/local.go b/pkg/datagatherer/local/local.go
index 530cd11c..4169aca3 100644
--- a/pkg/datagatherer/local/local.go
+++ b/pkg/datagatherer/local/local.go
@@ -49,7 +49,7 @@ func (g *DataGatherer) WaitForCacheSync(ctx context.Context) error {
}
// Fetch loads and returns the data from the LocalDatagatherer's dataPath
-func (g *DataGatherer) Fetch() (any, int, error) {
+func (g *DataGatherer) Fetch(ctx context.Context) (any, int, error) {
dataBytes, err := os.ReadFile(g.dataPath)
if err != nil {
return nil, -1, err
diff --git a/pkg/datagatherer/oidc/oidc.go b/pkg/datagatherer/oidc/oidc.go
new file mode 100644
index 00000000..251fa922
--- /dev/null
+++ b/pkg/datagatherer/oidc/oidc.go
@@ -0,0 +1,168 @@
+package oidc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/rest"
+ "k8s.io/klog/v2"
+
+ "github.com/jetstack/preflight/api"
+ "github.com/jetstack/preflight/pkg/datagatherer"
+ "github.com/jetstack/preflight/pkg/kubeconfig"
+)
+
+// OIDCDiscovery contains the configuration for the oidc data-gatherer.
+type OIDCDiscovery struct {
+ // KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster.
+ KubeConfigPath string `yaml:"kubeconfig"`
+}
+
+// UnmarshalYAML unmarshals the Config resolving GroupVersionResource.
+func (c *OIDCDiscovery) UnmarshalYAML(unmarshal func(any) error) error {
+ aux := struct {
+ KubeConfigPath string `yaml:"kubeconfig"`
+ }{}
+ err := unmarshal(&aux)
+ if err != nil {
+ return err
+ }
+
+ c.KubeConfigPath = aux.KubeConfigPath
+
+ return nil
+}
+
+func (c *OIDCDiscovery) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) {
+ cl, err := kubeconfig.NewDiscoveryClient(c.KubeConfigPath)
+ if err != nil {
+ return nil, err
+ }
+
+ return &DataGathererOIDC{
+ cl: cl.RESTClient(),
+ }, nil
+}
+
+// DataGathererOIDC stores the config for an oidc datagatherer.
+type DataGathererOIDC struct {
+ cl rest.Interface
+}
+
+var _ datagatherer.DataGatherer = &DataGathererOIDC{}
+
+func (g *DataGathererOIDC) Run(ctx context.Context) error {
+ return nil
+}
+
+func (g *DataGathererOIDC) WaitForCacheSync(ctx context.Context) error {
+ // no async functionality, see Fetch
+ return nil
+}
+
+// Fetch will fetch the OIDC discovery document and JWKS from the cluster API server.
+func (g *DataGathererOIDC) Fetch(ctx context.Context) (any, int, error) {
+ oidcResponse, oidcErr := g.fetchOIDCConfig(ctx)
+ jwksResponse, jwksErr := g.fetchJWKS(ctx)
+
+ errToString := func(err error) string {
+ if err != nil {
+ return err.Error()
+ }
+ return ""
+ }
+
+ if oidcErr != nil {
+ klog.FromContext(ctx).V(4).Error(oidcErr, "Failed to fetch OIDC configuration")
+ }
+ if jwksErr != nil {
+ klog.FromContext(ctx).V(4).Error(jwksErr, "Failed to fetch JWKS")
+ }
+
+ return &api.OIDCDiscoveryData{
+ OIDCConfig: oidcResponse,
+ OIDCConfigError: errToString(oidcErr),
+ JWKS: jwksResponse,
+ JWKSError: errToString(jwksErr),
+ }, 1 /* we have 1 result, so return 1 as count */, nil
+}
+
+func (g *DataGathererOIDC) fetchOIDCConfig(ctx context.Context) (map[string]any, error) {
+ // Fetch the OIDC discovery document from the well-known endpoint.
+ result := g.cl.Get().AbsPath("/.well-known/openid-configuration").Do(ctx)
+ if err := result.Error(); err != nil {
+ return nil, fmt.Errorf("failed to get /.well-known/openid-configuration: %s", k8sErrorMessage(err))
+ }
+
+ bytes, _ := result.Raw() // we already checked result.Error(), so there is no error here
+ var oidcResponse map[string]any
+ if err := json.Unmarshal(bytes, &oidcResponse); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal OIDC discovery document: %v (raw: %q)", err, stringFirstN(string(bytes), 80))
+ }
+
+ return oidcResponse, nil
+}
+
+func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error) {
+ // Fetch the JWKS from the default /openid/v1/jwks endpoint.
+ // We are not using the jwks_uri from the OIDC config because:
+ // - on hybrid OpenShift clusters, we saw it pointed to a non-existent URL
+ // - on fully private AWS EKS clusters, the URL is still public and might not
+ // be reachable from within the cluster (https://github.com/aws/containers-roadmap/issues/2038)
+ // So we are using the default path instead, which we think should work in most cases.
+ result := g.cl.Get().AbsPath("/openid/v1/jwks").Do(ctx)
+ if err := result.Error(); err != nil {
+ return nil, fmt.Errorf("failed to get /openid/v1/jwks: %s", k8sErrorMessage(err))
+ }
+
+ bytes, _ := result.Raw() // we already checked result.Error(), so there is no error here
+ var jwksResponse map[string]any
+ if err := json.Unmarshal(bytes, &jwksResponse); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JWKS response: %v (raw: %q)", err, stringFirstN(string(bytes), 80))
+ }
+
+ return jwksResponse, nil
+}
+
+func stringFirstN(s string, n int) string {
+ if len(s) <= n {
+ return s
+ }
+ return s[:n]
+}
+
+// based on https://github.com/kubernetes/kubectl/blob/a64ceaeab69eed1f11a9e1bd91cf2c1446de811c/pkg/cmd/util/helpers.go#L244
+func k8sErrorMessage(err error) string {
+ if status, isStatus := err.(apierrors.APIStatus); isStatus {
+ switch s := status.Status(); {
+ case s.Reason == metav1.StatusReasonUnauthorized:
+ return fmt.Sprintf("error: You must be logged in to the server (%s)", s.Message)
+ case len(s.Reason) > 0:
+ return fmt.Sprintf("Error from server (%s): %s", s.Reason, err.Error())
+ default:
+ return fmt.Sprintf("Error from server: %s", err.Error())
+ }
+ }
+
+ if apierrors.IsUnexpectedObjectError(err) {
+ return fmt.Sprintf("Server returned an unexpected response: %s", err.Error())
+ }
+
+ if t, isURL := err.(*url.Error); isURL {
+ if strings.Contains(t.Err.Error(), "connection refused") {
+ host := t.URL
+ if server, err := url.Parse(t.URL); err == nil {
+ host = server.Host
+ }
+ return fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host)
+ }
+ return fmt.Sprintf("Unable to connect to the server: %v", t.Err)
+ }
+
+ return fmt.Sprintf("error: %v", err)
+}
diff --git a/pkg/datagatherer/oidc/oidc_test.go b/pkg/datagatherer/oidc/oidc_test.go
new file mode 100644
index 00000000..75f36cd4
--- /dev/null
+++ b/pkg/datagatherer/oidc/oidc_test.go
@@ -0,0 +1,216 @@
+package oidc
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/rest"
+
+ "github.com/jetstack/preflight/api"
+)
+
+func makeRESTClient(t *testing.T, ts *httptest.Server) rest.Interface {
+ t.Helper()
+ u, err := url.Parse(ts.URL)
+ if err != nil {
+ t.Fatalf("parse server url: %v", err)
+ }
+
+ cfg := &rest.Config{
+ Host: u.Host,
+ }
+
+ discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, ts.Client())
+ if err != nil {
+ t.Fatalf("new discovery client: %v", err)
+ }
+
+ return discoveryClient.RESTClient()
+}
+
+func TestFetch_Success(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/.well-known/openid-configuration":
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"issuer":"https://example"}`))
+ case "/openid/v1/jwks":
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"keys":[]}`))
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer ts.Close()
+
+ rc := makeRESTClient(t, ts)
+ g := &DataGathererOIDC{cl: rc}
+
+ anyRes, count, err := g.Fetch(t.Context())
+ require.NoError(t, err)
+ require.Equal(t, 1, count)
+
+ res, ok := anyRes.(*api.OIDCDiscoveryData)
+ require.True(t, ok, "unexpected result type")
+
+ require.NotNil(t, res.OIDCConfig)
+ require.Equal(t, "https://example", res.OIDCConfig["issuer"].(string))
+ require.Empty(t, res.OIDCConfigError)
+
+ require.NotNil(t, res.JWKS)
+ _, ok = res.JWKS["keys"].([]any)
+ require.True(t, ok, "unexpected result type")
+ require.Empty(t, res.JWKSError)
+}
+
+func TestFetch_Errors(t *testing.T) {
+ tests := []struct {
+ name string
+ openidConfigurationResponse func(w http.ResponseWriter, r *http.Request)
+ jwksResponse func(w http.ResponseWriter, r *http.Request)
+ expOIDCConfigError string
+ expJWKSError string
+ }{
+ {
+ name: "5xx errors",
+ openidConfigurationResponse: func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "boom", http.StatusInternalServerError)
+ },
+ jwksResponse: func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "boom", http.StatusInternalServerError)
+ },
+ expOIDCConfigError: `failed to get /.well-known/openid-configuration: Error from server (InternalError): an error on the server ("boom") has prevented the request from succeeding`,
+ expJWKSError: `failed to get /openid/v1/jwks: Error from server (InternalError): an error on the server ("boom") has prevented the request from succeeding`,
+ },
+ {
+ name: "malformed JSON",
+ openidConfigurationResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`}{`))
+ },
+ jwksResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`}`))
+ _, _ = w.Write(bytes.Repeat([]byte{'0'}, 5000))
+ },
+ expOIDCConfigError: `failed to unmarshal OIDC discovery document: invalid character '}' looking for beginning of value (raw: "}{")`,
+ expJWKSError: `failed to unmarshal JWKS response: invalid character '}' looking for beginning of value (raw: "}0000000000000000000000000000000000000000000000000000000000000000000000000000000")`,
+ },
+ {
+ name: "Forbidden error (no body)",
+ openidConfigurationResponse: func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ },
+ jwksResponse: func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ },
+ expOIDCConfigError: "failed to get /.well-known/openid-configuration: Error from server (Forbidden): forbidden",
+ expJWKSError: "failed to get /openid/v1/jwks: Error from server (Forbidden): forbidden",
+ },
+ {
+ name: "Forbidden error (*metav1.Status body)",
+ openidConfigurationResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`{
+ "kind":"Status",
+ "apiVersion":"v1",
+ "metadata":{},
+ "status":"Failure",
+ "message":"forbidden: User \"system:serviceaccount:default:test\" cannot get path \"/.well-known/openid-configuration\"",
+ "reason":"Forbidden",
+ "details":{},
+ "code":403
+ }`))
+ },
+ jwksResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`{
+ "kind":"Status",
+ "apiVersion":"v1",
+ "metadata":{},
+ "status":"Failure",
+ "message":"forbidden: User \"system:serviceaccount:default:test\" cannot get path \"/openid/v1/jwks\"",
+ "reason":"Forbidden",
+ "details":{},
+ "code":403
+ }`))
+ },
+ expOIDCConfigError: `failed to get /.well-known/openid-configuration: Error from server (Forbidden): forbidden: User "system:serviceaccount:default:test" cannot get path "/.well-known/openid-configuration"`,
+ expJWKSError: `failed to get /openid/v1/jwks: Error from server (Forbidden): forbidden: User "system:serviceaccount:default:test" cannot get path "/openid/v1/jwks"`,
+ },
+ {
+ name: "Unauthorized error (*metav1.Status body)",
+ openidConfigurationResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`{
+ "kind": "Status",
+ "apiVersion": "v1",
+ "metadata": {},
+ "status": "Failure",
+ "message": "Unauthorized",
+ "reason": "Unauthorized",
+ "code": 401
+ }`))
+ },
+ jwksResponse: func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`{
+ "kind": "Status",
+ "apiVersion": "v1",
+ "metadata": {},
+ "status": "Failure",
+ "message": "Unauthorized",
+ "reason": "Unauthorized",
+ "code": 401
+ }`))
+ },
+ expOIDCConfigError: `failed to get /.well-known/openid-configuration: error: You must be logged in to the server (Unauthorized)`,
+ expJWKSError: `failed to get /openid/v1/jwks: error: You must be logged in to the server (Unauthorized)`,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/.well-known/openid-configuration":
+ tc.openidConfigurationResponse(w, r)
+ return
+ case "/openid/v1/jwks":
+ tc.jwksResponse(w, r)
+ return
+ default:
+ t.Fatalf("unexpected request path: %s", r.URL.Path)
+ }
+ }))
+ defer ts.Close()
+
+ rc := makeRESTClient(t, ts)
+ g := &DataGathererOIDC{cl: rc}
+
+ anyRes, count, err := g.Fetch(t.Context())
+ require.NoError(t, err)
+ require.Equal(t, 1, count)
+
+ res, ok := anyRes.(*api.OIDCDiscoveryData)
+ require.True(t, ok, "unexpected result type")
+
+ require.Nil(t, res.OIDCConfig)
+ require.NotEmpty(t, res.OIDCConfigError)
+ require.Equal(t, tc.expOIDCConfigError, res.OIDCConfigError)
+
+ require.Nil(t, res.JWKS)
+ require.NotEmpty(t, res.JWKSError)
+ require.Equal(t, tc.expJWKSError, res.JWKSError)
+ })
+ }
+}
diff --git a/pkg/logs/logs_test.go b/pkg/logs/logs_test.go
index ce7cd42a..cf5d52ce 100644
--- a/pkg/logs/logs_test.go
+++ b/pkg/logs/logs_test.go
@@ -20,8 +20,6 @@ import (
"k8s.io/klog/v2"
"github.com/jetstack/preflight/pkg/logs"
-
- _ "github.com/Venafi/vcert/v5"
)
// TestLogs demonstrates how the logging flags affect the logging output.